diff --git a/.changeset/young-fishes-bow.md b/.changeset/young-fishes-bow.md new file mode 100644 index 0000000000..6da0d10b98 --- /dev/null +++ b/.changeset/young-fishes-bow.md @@ -0,0 +1,9 @@ +--- +'@commercetools-uikit/select-input': minor +'@commercetools-uikit/select-utils': minor +'@commercetools-uikit/tag': minor +'@commercetools-uikit/i18n': minor +--- + +As the filters component is being built, there are some visual modifications that need to happen in the select input to support the designs and ux of the filters pattern. +This changes provide the updates needed for the select-input to support the visual styling of filter patterns. diff --git a/packages/components/inputs/select-input/README.md b/packages/components/inputs/select-input/README.md index 8c285f0454..ff39728670 100644 --- a/packages/components/inputs/select-input/README.md +++ b/packages/components/inputs/select-input/README.md @@ -60,7 +60,7 @@ export default Example; | Props | Type | Required | Default | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------- | :------: | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `appearance` | `union`
Possible values:
`'default' , 'quiet'` | | `'default'` | Indicates the appearance of the input. Quiet appearance is meant to be used with the `horizontalConstraint="auto"`. | +| `appearance` | `union`
Possible values:
`'default' , 'quiet' , 'filter'` | | `'default'` | Indicates the appearance of the input. `quiet` appearance is meant to be used with the `horizontalConstraint="auto"`. `filter` appearance provides a different look and feel for the select input when it is used as part of a filter component. | | `horizontalConstraint` | `union`
Possible values:
`, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 'scale', 'auto'` | | | | | `hasError` | `boolean` | | | Indicates that input has errors | | `isReadOnly` | `boolean` | | | Is the select read-only | @@ -98,6 +98,7 @@ export default Example; | `onFocus` | `ReactSelectProps['onFocus']` | | | Handle focus events on the control
[Props from React select was used](https://react-select.com/props) | | `onInputChange` | `ReactSelectProps['onInputChange']` | | | Handle change events on the input
[Props from React select was used](https://react-select.com/props) | | `options` | `union`
Possible values:
`TOption[] , TOptionObject[]` | | `[]` | Array of options that populate the select menu | +| `optionStyle` | `union`
Possible values:
`'list' , 'checkbox'` | | `'list'` | defines how options are rendered | | `showOptionGroupDivider` | `boolean` | | | | | `placeholder` | `ReactSelectProps['placeholder']` | | | Placeholder text for the select value
[Props from React select was used](https://react-select.com/props) | | `tabIndex` | `ReactSelectProps['tabIndex']` | | | Sets the tabIndex attribute on the input
[Props from React select was used](https://react-select.com/props) | @@ -105,6 +106,7 @@ export default Example; | `value` | `ReactSelectProps['value']` | | | The value of the select; reflected by the selected option
[Props from React select was used](https://react-select.com/props) | | `minMenuWidth` | `union`
Possible values:
`, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 'scale', 'auto'` | | | The min width (a range of values from the horizontalConrtaint set of values) for which the select-input menu is allowed to shrink. If unset, the menu will shrink to a default value. | | `maxMenuWidth` | `union`
Possible values:
`, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 'scale', 'auto'` | | | The max width (a range of values from the horizontalConrtaint set of values) for which the select-input menu is allowed to grow. If unset, the menu will grow horrizontally to fill its parent. | +| `count` | `number` | | | An additional value displayed on the select options menu. This value is only available in the checkbox option style when appearance is set to filter. | ## Signatures diff --git a/packages/components/inputs/select-input/src/select-input.stories.tsx b/packages/components/inputs/select-input/src/select-input.stories.tsx index ad521fc320..5b5ef0f26d 100644 --- a/packages/components/inputs/select-input/src/select-input.stories.tsx +++ b/packages/components/inputs/select-input/src/select-input.stories.tsx @@ -52,24 +52,29 @@ const options = [ { label: 'Animals 1', options: [ - { value: 'platypus', label: 'Platypus' }, - { value: 'goat', label: 'Goat' }, + { value: 'platypus', label: 'Platypus', count: 103 }, + { value: 'goat', label: 'Goat', count: 12.365 }, { value: 'giraffe', label: 'Giraffe' }, - { value: 'whale', label: 'Whale' }, - { value: 'killer-whale', label: 'Killer Whale', isDisabled: true }, - { value: 'otter', label: 'Otter' }, + { value: 'whale', label: 'Whale', count: 1123 }, + { + value: 'killer-whale', + label: 'Killer Whale', + isDisabled: true, + count: 1, + }, + { value: 'otter', label: 'Otter', count: 10.356 }, { value: 'elephant', label: 'Elephant' }, - { value: 'rat', label: 'Rat' }, - { value: 'anteater', label: 'Anteater' }, - { value: 'alligator', label: 'Alligator' }, - { value: 'dog', label: 'Dog' }, + { value: 'rat', label: 'Rat', count: 0 }, + { value: 'anteater', label: 'Anteater', count: 100335456413 }, + { value: 'alligator', label: 'Alligator', count: 1 }, + { value: 'dog', label: 'Dog', count: 5 }, { value: 'pig', label: 'Pig' }, - { value: 'hippopotamus', label: 'Hippopotamus' }, - { value: 'lion', label: 'Lion' }, - { value: 'monkey', label: 'Monkey' }, + { value: 'hippopotamus', label: 'Hippopotamus', count: 10 }, + { value: 'lion', label: 'Lion', count: 111 }, + { value: 'monkey', label: 'Monkey', count: 57 }, { value: 'kangaroo', label: 'Kangaroo' }, - { value: 'flamingo', label: 'Flamingo' }, - { value: 'moose', label: 'Moose' }, + { value: 'flamingo', label: 'Flamingo', count: 3 }, + { value: 'moose', label: 'Moose', count: 1003 }, ], }, { @@ -99,19 +104,19 @@ const options = [ label: 'Animals 3', options: [ { value: 'llama', label: 'Llama' }, - { value: 'seal', label: 'Seal' }, - { value: 'hawk', label: 'Hawk' }, - { value: 'wolf', label: 'Wolf' }, - { value: 'yak', label: 'Yak' }, - { value: 'rhinoceros', label: 'Rhinoceros' }, - { value: 'alpaca', label: 'Alpaca' }, - { value: 'zebra', label: 'Zebra' }, - { value: 'cat', label: 'Cat' }, + { value: 'seal', label: 'Seal', count: 245 }, + { value: 'hawk', label: 'Hawk', count: 23 }, + { value: 'wolf', label: 'Wolf', count: 89 }, + { value: 'yak', label: 'Yak', count: 6 }, + { value: 'rhinoceros', label: 'Rhinoceros', count: 9 }, + { value: 'alpaca', label: 'Alpaca', count: 54 }, + { value: 'zebra', label: 'Zebra', count: 302 }, + { value: 'cat', label: 'Cat', count: 1 }, { value: 'rabbit', label: 'Rabbit' }, { value: 'turtle', label: 'Turtle' }, { value: 'cow', label: 'Cow' }, { value: 'turkey', label: 'Turkey' }, - { value: 'deer', label: 'Deer' }, + { value: 'deer', label: 'Deer', count: 12 }, ], }, ]; @@ -140,3 +145,32 @@ BasicExample.args = { options, horizontalConstraint: 7, }; + +export const CheckboxOptionStyle: Story = (args) => { + const [value, setValue] = useState([]); + + useEffect(() => { + setValue([]); + }, [args.isMulti]); + + return ( +
+ { + setValue(e.target.value); + }} + /> +
{JSON.stringify(value, null, 2)}
+
+ ); +}; + +CheckboxOptionStyle.args = { + options, + horizontalConstraint: 7, + optionStyle: 'checkbox', + isMulti: true, + appearance: 'filter', +}; diff --git a/packages/components/inputs/select-input/src/select-input.tsx b/packages/components/inputs/select-input/src/select-input.tsx index 4b5cfd6d98..19b82cd62e 100644 --- a/packages/components/inputs/select-input/src/select-input.tsx +++ b/packages/components/inputs/select-input/src/select-input.tsx @@ -15,8 +15,11 @@ import { createSelectStyles, messages, warnIfMenuPortalPropsAreMissing, + optionStyleCheckboxComponents, + optionsStyleCheckboxSelectProps, } from '@commercetools-uikit/select-utils'; import { filterDataAttributes } from '@commercetools-uikit/utils'; +import { SearchIcon } from '@commercetools-uikit/icons'; const customizedComponents = { DropdownIndicator, @@ -27,6 +30,7 @@ const customizedComponents = { export type TOption = { value: string; label?: ReactNode; + isDisabled?: boolean; }; export type TOptionObject = { @@ -47,9 +51,10 @@ export type TCustomEvent = { export type TSelectInputProps = { /** * Indicates the appearance of the input. - * Quiet appearance is meant to be used with the `horizontalConstraint="auto"`. + * `quiet` appearance is meant to be used with the `horizontalConstraint="auto"`. + * `filter` appearance provides a different look and feel for the select input when it is used as part of a filter component. */ - appearance?: 'default' | 'quiet'; + appearance?: 'default' | 'quiet' | 'filter'; horizontalConstraint?: | 3 | 4 @@ -308,6 +313,8 @@ export type TSelectInputProps = { * Array of options that populate the select menu */ options: TOptions; + /** defines how options are rendered */ + optionStyle: 'list' | 'checkbox'; showOptionGroupDivider?: boolean; // pageSize: PropTypes.number, /** @@ -379,16 +386,25 @@ export type TSelectInputProps = { | 16 | 'scale' | 'auto'; + /** + * An additional value displayed on the select options menu. This value is only available in the checkbox option style when appearance is set to filter. + */ + count?: number; }; const defaultProps: Pick< TSelectInputProps, - 'appearance' | 'maxMenuHeight' | 'menuPortalZIndex' | 'options' + | 'appearance' + | 'maxMenuHeight' + | 'menuPortalZIndex' + | 'options' + | 'optionStyle' > = { appearance: 'default', maxMenuHeight: 220, menuPortalZIndex: 1, options: [], + optionStyle: 'list', }; const isOptionObject = ( @@ -405,7 +421,9 @@ const SelectInput = (props: TSelectInputProps) => { }); const placeholder = - props.placeholder || intl.formatMessage(messages.placeholder); + props.appearance === 'filter' && !props.placeholder + ? intl.formatMessage(messages.selectInputAsFilterPlaceholder) + : props.placeholder || intl.formatMessage(messages.placeholder); // Options can be grouped as // const colourOptions = [{ value: 'green', label: 'Green' }]; // const flavourOptions = [{ value: 'vanilla', label: 'Vanilla' }]; @@ -462,10 +480,23 @@ const SelectInput = (props: TSelectInputProps) => { ), } : {}), + ...(props.appearance === 'filter' && { + DropdownIndicator: () => , + ClearIndicator: null, + }), + ...(props.optionStyle === 'checkbox' + ? optionStyleCheckboxComponents(props.appearance) + : {}), ...props.components, } as ReactSelectProps['components'] } - menuIsOpen={props.isReadOnly ? false : props.menuIsOpen} + menuIsOpen={ + props.isReadOnly + ? false + : props.appearance === 'filter' + ? true + : props.menuIsOpen + } styles={ createSelectStyles({ hasWarning: props.hasWarning, @@ -475,7 +506,8 @@ const SelectInput = (props: TSelectInputProps) => { appearance: props.appearance, isDisabled: props.isDisabled, isReadOnly: props.isReadOnly, - isCondensed: props.isCondensed, + isCondensed: + props.appearance === 'filter' ? true : props.isCondensed, iconLeft: props.iconLeft, isMulti: props.isMulti, hasValue: !isEmpty(selectedOptions), @@ -496,7 +528,9 @@ const SelectInput = (props: TSelectInputProps) => { isClearable={props.isReadOnly ? false : props.isClearable} isDisabled={props.isDisabled} isOptionDisabled={props.isOptionDisabled} - hideSelectedOptions={props.hideSelectedOptions} + {...(props.optionStyle === 'checkbox' + ? optionsStyleCheckboxSelectProps() + : { hideSelectedOptions: props.hideSelectedOptions })} // @ts-ignore isReadOnly={props.isReadOnly} isMulti={props.isMulti} @@ -505,6 +539,7 @@ const SelectInput = (props: TSelectInputProps) => { maxMenuHeight={props.maxMenuHeight} menuPortalTarget={props.menuPortalTarget} menuShouldBlockScroll={props.menuShouldBlockScroll} + // @ts-expect-error: optionStyle 'checkbox' will override this property (if set) closeMenuOnSelect={props.closeMenuOnSelect} name={props.name} noOptionsMessage={ @@ -572,8 +607,15 @@ const SelectInput = (props: TSelectInputProps) => { tabSelectsValue={props.tabSelectsValue} value={selectedOptions} iconLeft={props.iconLeft} - controlShouldRenderValue={props.controlShouldRenderValue} + controlShouldRenderValue={ + props.appearance === 'filter' + ? false + : props.controlShouldRenderValue + } menuPlacement="auto" + {...(props.optionStyle === 'checkbox' + ? optionsStyleCheckboxSelectProps() + : {})} /> diff --git a/packages/components/inputs/select-utils/package.json b/packages/components/inputs/select-utils/package.json index 13ec019322..84dd305cb8 100644 --- a/packages/components/inputs/select-utils/package.json +++ b/packages/components/inputs/select-utils/package.json @@ -22,6 +22,7 @@ "@babel/runtime": "^7.20.13", "@babel/runtime-corejs3": "^7.20.13", "@commercetools-uikit/accessible-button": "19.13.0", + "@commercetools-uikit/checkbox-input": "workspace:^", "@commercetools-uikit/design-system": "19.13.0", "@commercetools-uikit/icons": "19.13.0", "@commercetools-uikit/spacings": "19.13.0", diff --git a/packages/components/inputs/select-utils/src/create-select-styles.ts b/packages/components/inputs/select-utils/src/create-select-styles.ts index 85d0afbd05..ee1c424a9f 100644 --- a/packages/components/inputs/select-utils/src/create-select-styles.ts +++ b/packages/components/inputs/select-utils/src/create-select-styles.ts @@ -22,7 +22,7 @@ type TProps = { hasValue?: boolean; isCondensed?: boolean; controlShouldRenderValue?: boolean; - appearance?: 'default' | 'quiet'; + appearance?: 'default' | 'quiet' | 'filter'; minMenuWidth?: | 2 | 3 @@ -246,7 +246,10 @@ const menuStyles = (props: TProps) => (base: TBase) => { border: `1px solid ${designTokens.colorSurface}`, borderRadius: designTokens.borderRadiusForInput, backgroundColor: designTokens.backgroundColorForInput, - boxShadow: '0 2px 5px 0px rgba(0, 0, 0, 0.15)', + boxShadow: + props.appearance === 'filter' + ? 'none' + : '0 2px 5px 0px rgba(0, 0, 0, 0.15)', fontSize: designTokens.fontSize30, fontFamily: 'inherit', margin: `${designTokens.spacing10} 0 0 0`, @@ -325,6 +328,7 @@ const optionStyles = (props: TProps) => (base: TBase, state: TState) => { if (state.isDisabled) return designTokens.fontColorForInputWhenDisabled; return designTokens.fontColorForInput; })(), + borderRadius: props.appearance === 'filter' && '4px', backgroundColor: (() => { if (state.isSelected) return designTokens.colorPrimary95; if (state.isFocused) @@ -369,6 +373,23 @@ const placeholderStyles = (props: TProps) => (base: TBase) => { }; }; +const getInputValueLayout = (props: TProps) => { + switch (true) { + case props.appearance === 'filter': + return 'grid'; + // Display property should be grid when isMulti and has no value so the Placeholder component is positioned correctly with the Input + // Display property should be Flex when there is an iconLeft, also when the input has some values when isMulti. + // See PR from react select for more insight https://github.com/JedWatson/react-select/pull/4833 + case (props.iconLeft && !props.isMulti) || + (props.isMulti && + props.hasValue && + (props.controlShouldRenderValue ?? true)): + return 'flex'; + default: + return 'grid'; + } +}; + const valueContainerStyles = (props: TProps) => (base: TBase) => { return { ...base, @@ -376,16 +397,7 @@ const valueContainerStyles = (props: TProps) => (base: TBase) => { padding: '0', backgroundColor: 'none', overflow: 'hidden', - // Display property should be grid when isMulti and has no value so the Placeholder component is positioned correctly with the Input - // Display property should be Flex when there is an iconLeft, also when the input has some values when isMulti. - // See PR from react select for more insight https://github.com/JedWatson/react-select/pull/4833 - display: - (props.iconLeft && !props.isMulti) || - (props.isMulti && - props.hasValue && - (props.controlShouldRenderValue ?? true)) - ? 'flex' - : 'grid', + display: getInputValueLayout(props), fill: props.isDisabled || props.isReadOnly ? designTokens.fontColorForInputWhenDisabled diff --git a/packages/components/inputs/select-utils/src/custom-styled-select-options/index.ts b/packages/components/inputs/select-utils/src/custom-styled-select-options/index.ts index bf4cf2c403..327d1c92f5 100644 --- a/packages/components/inputs/select-utils/src/custom-styled-select-options/index.ts +++ b/packages/components/inputs/select-utils/src/custom-styled-select-options/index.ts @@ -1,2 +1,3 @@ export { SELECT_DROPDOWN_OPTION_TYPES } from './constants'; export * from './custom-styled-select-options'; +export * from './options-style-checkbox-components'; diff --git a/packages/components/inputs/select-utils/src/custom-styled-select-options/options-style-checkbox-components.tsx b/packages/components/inputs/select-utils/src/custom-styled-select-options/options-style-checkbox-components.tsx new file mode 100644 index 0000000000..d76dc0af00 --- /dev/null +++ b/packages/components/inputs/select-utils/src/custom-styled-select-options/options-style-checkbox-components.tsx @@ -0,0 +1,88 @@ +import { ReactNode } from 'react'; +import { type Props as ReactSelectProps, OptionProps } from 'react-select'; +import CheckboxInput from '@commercetools-uikit/checkbox-input'; +import type { TSelectInputProps } from '@commercetools-uikit/select-input'; +import { css } from '@emotion/react'; +import { designTokens } from '@commercetools-uikit/design-system'; + +/** + * Returns custom components to be used with react-select, when optionStyle is set to "checkbox" + */ +type OptionType = { + label: ReactNode; + value: string; + count: number; +}; + +export const optionStyleCheckboxComponents = ( + appearance: TSelectInputProps['appearance'] +) => { + return { + Option: (props: OptionProps) => { + const { + innerRef, + innerProps, + label, + isDisabled, + isFocused, + isSelected, + className, + data, + } = props; + + return ( +
+ {}} + > + {label} + + {appearance === 'filter' && ( +
+ {data.count} +
+ )} +
+ ); + }, + } as ReactSelectProps['components']; +}; + +/** + * Returns react-select props to be used with the