From c022aa2c7870ace003346b123893cc765706c178 Mon Sep 17 00:00:00 2001 From: Zoe Hayes Date: Thu, 18 Apr 2024 00:41:31 -0400 Subject: [PATCH] feat: intial add of QueryItem.ValueSelector.Cascader --- design/LightTheme.ts | 2 +- design/MpThemeConfig.d.ts | 2 +- src/assets/svg/circle-dashed.svg | 5 + .../data-entry/QueryItem/Action.stories.tsx | 8 ++ .../data-entry/QueryItem/Action.tsx | 13 +- .../data-entry/QueryItem/Cascader.stories.tsx | 112 +++++++++++++++ .../data-entry/QueryItem/Cascader.tsx | 102 +++++++++++++ .../QueryItem/Qualifier.stories.tsx | 36 ++--- .../data-entry/QueryItem/Qualifier.tsx | 11 +- .../data-entry/QueryItem/QueryItem.tsx | 4 +- src/components/data-entry/QueryItem/Text.tsx | 8 +- .../QueryItem/ValueSelector.stories.tsx | 22 +++ .../data-entry/QueryItem/ValueSelector.tsx | 7 + .../data-entry/QueryItem/query-item.css | 135 ++++++------------ src/components/icons/index.ts | 2 + src/components/index.ts | 2 + src/styles/_variables.css | 2 +- 17 files changed, 344 insertions(+), 129 deletions(-) create mode 100644 src/assets/svg/circle-dashed.svg create mode 100644 src/components/data-entry/QueryItem/Cascader.stories.tsx create mode 100644 src/components/data-entry/QueryItem/Cascader.tsx create mode 100644 src/components/data-entry/QueryItem/ValueSelector.stories.tsx create mode 100644 src/components/data-entry/QueryItem/ValueSelector.tsx diff --git a/design/LightTheme.ts b/design/LightTheme.ts index 7812874e8..6518b03cd 100644 --- a/design/LightTheme.ts +++ b/design/LightTheme.ts @@ -94,7 +94,7 @@ export const LightTheme: IMpThemeConfig = { mpQueryItem_borderWidth_active: '0 0 2px 0', mpQueryItem_shadow_focus: '0 0 0 2px rgba(54, 0, 209, 0.1)', mpQueryItem_color_disabled: '#505249', - 'mpQueryItem|valueSelector_fontWeight': '600', + 'mpQueryItem|valueSelector_fontWeight': '500', 'mpQueryItem|valueSelector_color': '#20007a', mpQueryItem_padding: 4, mpQueryItem_gap: 4, diff --git a/design/MpThemeConfig.d.ts b/design/MpThemeConfig.d.ts index aef12ce6f..313ec4f42 100644 --- a/design/MpThemeConfig.d.ts +++ b/design/MpThemeConfig.d.ts @@ -35,7 +35,7 @@ export type IMpThemeConfig = ThemeConfig & { mpQueryItem_borderWidth_active: '0 0 2px 0' mpQueryItem_shadow_focus: '0 0 0 2px rgba(54, 0, 209, 0.1)' mpQueryItem_color_disabled: '#505249' - 'mpQueryItem|valueSelector_fontWeight': '600' + 'mpQueryItem|valueSelector_fontWeight': '500' 'mpQueryItem|valueSelector_color': '#20007a' mpQueryItem_padding: 4 mpQueryItem_gap: 4 diff --git a/src/assets/svg/circle-dashed.svg b/src/assets/svg/circle-dashed.svg new file mode 100644 index 000000000..3f5048f73 --- /dev/null +++ b/src/assets/svg/circle-dashed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/data-entry/QueryItem/Action.stories.tsx b/src/components/data-entry/QueryItem/Action.stories.tsx index 817535f03..70c0481d7 100644 --- a/src/components/data-entry/QueryItem/Action.stories.tsx +++ b/src/components/data-entry/QueryItem/Action.stories.tsx @@ -42,3 +42,11 @@ export const Disabled: Story = { isDisabled: true, }, } + +export const OnClick: Story = { + args: { + text: 'On Click Action', + isPrimary: true, + onClick: () => alert('You clicked the QueryItem.Action!') + }, +} diff --git a/src/components/data-entry/QueryItem/Action.tsx b/src/components/data-entry/QueryItem/Action.tsx index 36708612b..fdcce83c0 100644 --- a/src/components/data-entry/QueryItem/Action.tsx +++ b/src/components/data-entry/QueryItem/Action.tsx @@ -5,20 +5,19 @@ export interface IActionProps { isPrimary?: boolean isDisabled?: boolean text?: string + onClick?: () => void } -function Action(props: IActionProps) { +export const Action = (props: IActionProps) => { const buttonClassNames: string = props.isPrimary - ? 'query-item query-item-action' - : 'query-item query-item-action query-item-action--secondary' + ? 'query-item query-item--action' + : 'query-item query-item--action query-item--secondary' return ( <> - ) } - -export default Action diff --git a/src/components/data-entry/QueryItem/Cascader.stories.tsx b/src/components/data-entry/QueryItem/Cascader.stories.tsx new file mode 100644 index 000000000..e3b7dc764 --- /dev/null +++ b/src/components/data-entry/QueryItem/Cascader.stories.tsx @@ -0,0 +1,112 @@ +import { Meta, StoryObj } from '@storybook/react' +import { QueryItem } from 'src/components' + +const meta: Meta = { + title: 'Aquarium/Data Entry/QueryItem/ValueSelector/Cascader', + component: QueryItem.ValueSelector.Cascader, + parameters: { + docs: { + description: { + component: + 'This is the "Action" component of the QueryItem component group. This component is currently meant to trigger a single action, but will eventually support a list of actions via a dropdown list interface.', + }, + }, + }, + + args: {}, +} +export default meta + +type Story = StoryObj + +const exampleOptions = [ + { + value: "United States1", + label: "United States", + children: [ + { + value: "Michigan1", + label: "Michigan", + children: [ + { + value: "Detroit1", + label: "Detroit", + }, + { + value: "Lansing1", + label: "Lansing", + }, + ], + }, + { + value: "California1", + label: "California", + children: [ + { + value: "San Francisco1", + label: "San Francisco", + }, + { + value: "San Jose1", + label: "San Jose", + }, + ], + }, + ], + }, + { + value: "Canada1", + label: "Canada", + children: [ + { + value: "Ontario1", + label: "Ontario", + children: [ + { + value: "Toronto1", + label: "Toronto", + }, + ], + }, + ], + }, +]; + +export const Default: Story = { + args: { + placeholder: "QueryItem.ValueSelector.Cascader Default", + options: exampleOptions + }, +} + +export const SimpleList: Story = { + args: { + placeholder: "QueryItem.ValueSelector.Cascader Simple", + options: [ + { + value: 'United States', + label: 'United States', + }, + { + value: 'Canada', + label: 'Canada', + }, + ] + }, +} + +export const Error: Story = { + args: { + placeholder: "QueryItem.ValueSelector.Cascader Error", + options: exampleOptions, + errorMessage: 'test error', + }, +} + +export const OnSelect: Story = { + args: { + placeholder: "QueryItem.ValueSelector.Cascader Error", + options: exampleOptions, + onChange: (value) => console.log(value), + }, +} diff --git a/src/components/data-entry/QueryItem/Cascader.tsx b/src/components/data-entry/QueryItem/Cascader.tsx new file mode 100644 index 000000000..335f11c9a --- /dev/null +++ b/src/components/data-entry/QueryItem/Cascader.tsx @@ -0,0 +1,102 @@ +import './query-item.css' +import { GetProp } from 'antd' +import { ReactNode, useState } from 'react' +import { + AddIcon, + Cascader as BaseCascader, + CircleDashedIcon, + Flex, + ICascaderProps as IBaseCascaderProps, + Input, + Space, + Typography, +} from 'src/components' + +export interface CascaderOption { + value: string + label: ReactNode + selectedLabel?: string + children?: CascaderOption[] + disabled?: boolean +} + +export type CascaderIcons = "blank" | "attribute" | "user" | "event" + +const CascaderIconList = { + "blank": , + "attribute": , + "user": , + "event": , +} + +export interface ICascaderProps { + options: CascaderOption[] + icon?: CascaderIcons + errorMessage?: string + placeholder?: string + onChange?: (selectedValue: string[]) => void +} + +export const Cascader = (props: ICascaderProps) => { + type DefaultOptionType = GetProp[number] + + const options: CascaderOption[] = [] + const [items] = useState(props.options ?? options) + const [searchValue, setSearchValue] = useState('') + const [selectedValue, setSelectedValue] = useState() + const [isOpen, setIsOpen] = useState(false) + + const onSearch = (value: string) => { + setSearchValue(value) + } + + const filter = (inputValue: string, path: DefaultOptionType[]) => + path.some(option => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1) + + const baseProps: IBaseCascaderProps = { + getPopupContainer: triggerNode => triggerNode.parentElement, + searchValue: searchValue, + value: selectedValue, + onChange: (value: (string | number)[]): void => { + setSelectedValue(value as string[]) + if (props.onChange) { + props.onChange(value as string[]) + } + }, + dropdownRender: menu => ( +
+ + {menu} +
+ ), + showSearch: { filter }, + options: items, + onDropdownVisibleChange: value => setIsOpen(value), + } + + let inputClasses = `query-item` + if (isOpen) inputClasses += ' query-item--open' + if (selectedValue) inputClasses += ' query-item--selected' + if (props.errorMessage) inputClasses += ' query-item--error' + + return ( + <> + + } + /> + + {props.errorMessage && {props.errorMessage}} + + ) +} diff --git a/src/components/data-entry/QueryItem/Qualifier.stories.tsx b/src/components/data-entry/QueryItem/Qualifier.stories.tsx index 8cf7bb781..a0db68957 100644 --- a/src/components/data-entry/QueryItem/Qualifier.stories.tsx +++ b/src/components/data-entry/QueryItem/Qualifier.stories.tsx @@ -11,46 +11,34 @@ const meta: Meta = { } export default meta +const defaultOptions = [ + { value: '0', label: 'is equal to' }, + { value: '1', label: 'is not equal to' }, + { value: '2', label: 'is greater than' }, + { value: '3', label: 'is greater or equal to' }, + { value: '4', label: 'is less than' }, + { value: '5', label: 'is less or equal to' }, +] + // stories export const Empty: Story = {} export const Simple: Story = { args: { - options: [ - { value: '0', label: 'is equal to' }, - { value: '1', label: 'is not equal to' }, - { value: '2', label: 'is greater than to' }, - { value: '3', label: 'is greater or equal to' }, - { value: '4', label: 'is less than' }, - { value: '5', label: 'is less or equal to' }, - ], + options: defaultOptions }, } export const Error: Story = { args: { errorMessage: 'This is an error message for the Qualifier component', - options: [ - { value: '0', label: 'is equal to' }, - { value: '1', label: 'is not equal to' }, - { value: '2', label: 'is greater than to' }, - { value: '3', label: 'is greater or equal to' }, - { value: '4', label: 'is less than' }, - { value: '5', label: 'is less or equal to' }, - ], + options: defaultOptions, }, } export const Disabled: Story = { args: { disabled: true, - options: [ - { value: '0', label: 'is equal to' }, - { value: '1', label: 'is not equal to' }, - { value: '2', label: 'is greater than to' }, - { value: '3', label: 'is greater or equal to' }, - { value: '4', label: 'is less than' }, - { value: '5', label: 'is less or equal to' }, - ], + options: defaultOptions, }, } diff --git a/src/components/data-entry/QueryItem/Qualifier.tsx b/src/components/data-entry/QueryItem/Qualifier.tsx index 6ac774695..4fb6be3ee 100644 --- a/src/components/data-entry/QueryItem/Qualifier.tsx +++ b/src/components/data-entry/QueryItem/Qualifier.tsx @@ -1,5 +1,6 @@ import './query-item.css' import type { DefaultOptionType } from 'antd/es/select' +import { useState } from 'react' import { CheckIcon } from 'src/components/icons' import { Typography } from 'src/components/general/Typography/Typography' import { type ISelectProps, Select } from 'src/components' @@ -14,20 +15,28 @@ export interface IQueryItemQualifierProps { } export const Qualifier = (props: IQueryItemQualifierProps) => { + const [isOpen, setIsOpen] = useState(false) const selectProps: ISelectProps = { defaultValue: props.options?.length ? props.options[0].value : undefined, menuItemSelectedIcon: node => node.isSelected ? : null, onChange: props.onChange, + onDropdownVisibleChange: () => setIsOpen(true), placement: 'bottomLeft', popupMatchSelectWidth: false, status: props.errorMessage ? 'error' : undefined, suffixIcon: null, variant: 'borderless', + options: props.options, + disabled: props.disabled, } + + let className = 'query-item' + if (isOpen) className += ' query-item--open' + return ( <> - + {props.errorMessage && {props.errorMessage}} ) diff --git a/src/components/data-entry/QueryItem/QueryItem.tsx b/src/components/data-entry/QueryItem/QueryItem.tsx index 010a81857..da533b3bb 100644 --- a/src/components/data-entry/QueryItem/QueryItem.tsx +++ b/src/components/data-entry/QueryItem/QueryItem.tsx @@ -1,5 +1,6 @@ import React from 'react' -import Action from './Action' +import { ValueSelector } from 'src/components/data-entry/QueryItem/ValueSelector' +import { Action } from './Action' import { Qualifier } from './Qualifier' import { Text } from './Text' @@ -8,4 +9,5 @@ export const QueryItem = () => { } QueryItem.Action = Action QueryItem.Qualifier = Qualifier +QueryItem.ValueSelector = ValueSelector QueryItem.Text = Text diff --git a/src/components/data-entry/QueryItem/Text.tsx b/src/components/data-entry/QueryItem/Text.tsx index 543f387e3..cc5ef1e33 100644 --- a/src/components/data-entry/QueryItem/Text.tsx +++ b/src/components/data-entry/QueryItem/Text.tsx @@ -1,12 +1,12 @@ -import { Text as BaseText } from 'src/components/general/Typography/Typography' +import { Typography } from 'src/components/general/Typography/Typography' -export interface IActionProps { +export interface ITextProps { disabled?: boolean text: string } -export const Text = ({ disabled = false, text }: IActionProps) => { - return {text}; +export const Text = ({ disabled = false, text }: ITextProps) => { + return {text}; } export default Text diff --git a/src/components/data-entry/QueryItem/ValueSelector.stories.tsx b/src/components/data-entry/QueryItem/ValueSelector.stories.tsx new file mode 100644 index 000000000..dfba08a8f --- /dev/null +++ b/src/components/data-entry/QueryItem/ValueSelector.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react' +import { ValueSelector } from './ValueSelector' + +const meta: Meta = { + title: 'Aquarium/Data Entry/QueryItem/ValueSelector', + component: ValueSelector, + parameters: { + docs: { + description: { + component: + 'DO NOT USE THIS OR YOU WILL BE FIRED! This is a parent component of this component group, and is not supposed to be used on its own.', + }, + }, + }, + + args: {}, +} +export default meta + +type Story = StoryObj + +export const DontUseThisOrYouWillBeFired: Story = {} diff --git a/src/components/data-entry/QueryItem/ValueSelector.tsx b/src/components/data-entry/QueryItem/ValueSelector.tsx new file mode 100644 index 000000000..404b8e0e3 --- /dev/null +++ b/src/components/data-entry/QueryItem/ValueSelector.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import { Cascader } from './Cascader' + +export const ValueSelector = () => { + return <>DON'T USE THIS OR YOU WILL BE FIRED! +} +ValueSelector.Cascader = Cascader diff --git a/src/components/data-entry/QueryItem/query-item.css b/src/components/data-entry/QueryItem/query-item.css index dbd71c705..745be5bd6 100644 --- a/src/components/data-entry/QueryItem/query-item.css +++ b/src/components/data-entry/QueryItem/query-item.css @@ -1,18 +1,21 @@ @import url('src/styles/_variables.css'); - +/** + * QueryItem + * _defaults_ + */ .query-item { - border-width: var(--mp-query-item-border-width); + border-width: var(--mp-query-item-border-width) !important; border-color: var(--mp-query-item-border-color) !important; border-radius: var(--mp-query-item-border-radius); background-color: var(--mp-query-item-bg-color) !important; padding: var(--mp-query-item-padding) !important; - gap: var(--mp-query-item-gap); - color: var(--mp-query-item-action-primary-color) !important; + /*color: var(--mp-query-item-color) !important;*/ height: var(--mp-query-item-height) !important; display: flex; justify-content: center; align-items: center; box-shadow: none; + border-style: solid; &:hover { border-color: var(--mp-query-item-border-color-active) !important; @@ -27,7 +30,7 @@ &:active { border-color: var(--mp-query-item-border-color-active) !important; - border-width: var(--mp-query-item-border-width-active); + border-width: var(--mp-query-item-border-width-active) !important; background-color: var(--mp-query-item-bg-color-active) !important; box-shadow: var(--mp-query-item-shadow-active); } @@ -36,105 +39,59 @@ color: var(--mp-query-item-color-disabled) !important; border-color: var(--mp-query-item-border-color-disabled) !important; } -} - -/** - * QueryItem.Action - */ -.query-item-action.query-item-action--secondary { - color: var(--mp-query-item-action-secondary-color) !important; - &:hover { - background-color: var(--mp-query-item-bg-color-hover) !important; - } + &.query-item--action { + color: var(--mp-query-item-action-primary-color) !important; - &:active { - border-width: var(--mp-query-item-border-width-active); - background-color: var(--mp-query-item-bg-color-active) !important; + gap: var(--mp-query-item-gap); } -} - -/* This is temporary until the new icon component is available where sizes can be controlled without CSS */ -.query-item-action__icon { - height: 16px; - width: 16px; -} + &.query-item--secondary { + color: var(--mp-query-item-action-secondary-color) !important; -/** - * QueryItem.Qualifier - * _defaults_ - */ -.query-item-qualifier__item-selected-icon { - margin-left: var(--mp-query-item-padding); -} + &:hover { + background-color: var(--mp-query-item-bg-color-hover) !important; + } -.query-item-qualifier__select { - .ant-select-selector { - padding: calc(var(--mp-query-item-padding) / 2) !important; - border-radius: var(--mp-query-item-border-radius) !important; + &:active { + border-width: var(--mp-query-item-border-width-active); + background-color: var(--mp-query-item-bg-color-active) !important; + } } - .ant-select-selection-item { - line-height: var(--line-height) !important; - padding: calc(var(--mp-query-item-padding) / 2) !important; - border-radius: var(--mp-query-item-border-radius); - border-bottom: var(--line-width) solid var(--mp-query-item-border-color); + &.query-item--selected > input { + color: var(--mp-query-item-value-selector-color) !important; + font-weight: var(--mp-query-item-value-selector-font-weight) !important; } -} - -/** - * QueryItem.Qualifier - * :hover - */ -.query-item-qualifier__select:hover { - .ant-select-selection-item { - border-bottom-color: var(--mp-query-item-border-color-active); + &.query-item--error { + border-color: var(--mp-query-item-border-color-error) !important; } -} - -/** - * QueryItem.Qualifier - * :focus - */ -.query-item-qualifier__select:focus { - .ant-select-selection-item { - background-color: var(--color-white); - border-bottom-color: var(--mp-query-item-border-color-active); + &.query-item--open { + border-color: var(--mp-query-item-border-color-active) !important; + border-width: var(--mp-query-item-border-width-active) !important; + background-color: var(--mp-query-item-bg-color-active) !important; } -} -/** - * QueryItem.Qualifier - * :active - */ -.query-item-qualifier__select.ant-select-open { - .ant-select-selection-item { - margin-top: calc(var(--line-width) - 1); - color: var(--color-text-base); - border-bottom-width: calc(var(--line-width) + 1); - border-bottom-color: var(--mp-query-item-border-color-active); + /* Need to override the inner padding on Select... */ + & .ant-select-selector { + padding: 0 !important; } } -/** - * QueryItem.Qualifier - * Disabled - */ -.query-item-qualifier__select.ant-select-disabled { - color: var(--color-text-disabled); - background-color: var(--mp-query-item-bg-color-disabled); - .ant-select-selection-item { - border-bottom-color: var(--mp-query-item-border-color-disabled); - } +/* This is temporary until the new icon component is available where sizes can be controlled without CSS */ +.query-item__icon { + height: 16px; + width: 16px; +} +.query-item__dropdown{ + display: flex; + padding: 8px; + flex-direction: column; + align-items: flex-start; + gap: 8px; } -/** - * QueryItem.Qualifier - * Error - */ -.query-item-qualifier__select.ant-select-status-error { - .ant-select-selection-item { - border-bottom-color: var(--mp-query-item-border-color-error); - } +/* This is a fix for a pre-existing Ant issue with the spacing on input.search */ +.query-item__input-search .ant-input-outlined{ + height: var(--control-height); } diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index 668572f5e..c39af16cf 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -23,6 +23,7 @@ import MpLogoIcon from 'src/assets/svg/mpLogo.svg?react' import LockIcon from 'src/assets/svg/lock.svg?react' import SearchIcon from 'src/assets/svg/search.svg?react' import AnalyticsIcon from 'src/assets/svg/analytics.svg?react' +import CircleDashedIcon from 'src/assets/svg/circle-dashed.svg?react' export { AddIcon, @@ -50,4 +51,5 @@ export { CloudIcon, FolderClosedIcon, AnalyticsIcon, + CircleDashedIcon, } diff --git a/src/components/index.ts b/src/components/index.ts index 71f4444d6..4c7191008 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -125,4 +125,6 @@ export { CloudIcon, FolderClosedIcon, AnalyticsIcon, + CircleDashedIcon, } from './icons/index' +export { Typography } from './general/Typography/Typography' diff --git a/src/styles/_variables.css b/src/styles/_variables.css index 042c8a12c..a7bab12b1 100644 --- a/src/styles/_variables.css +++ b/src/styles/_variables.css @@ -127,7 +127,7 @@ --mp-query-item-border-width-active: 0 0 2px 0; --mp-query-item-shadow-focus: 0 0 0 2px rgba(54, 0, 209, 0.1); --mp-query-item-color-disabled: #505249; - --mp-query-item-value-selector-font-weight: 600; + --mp-query-item-value-selector-font-weight: 500; --mp-query-item-value-selector-color: #20007a; --mp-query-item-padding: 4px; --mp-query-item-gap: 4px;