From 6be603084ad1d70f69cb4c0b81891dae0d470409 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 15 Jan 2020 13:51:12 -0500 Subject: [PATCH 1/3] Components: Implement a combobox control. --- docs/manifest.json | 6 + package-lock.json | 4 +- .../components/src/combobox-control/README.md | 146 ++++++++++++++++++ .../components/src/combobox-control/index.js | 126 +++++++++++++++ .../src/combobox-control/stories/index.js | 53 +++++++ .../src/combobox-control/style.scss | 70 +++++++++ .../src/custom-select-control/README.md | 44 +++--- packages/components/src/style.scss | 1 + 8 files changed, 430 insertions(+), 20 deletions(-) create mode 100644 packages/components/src/combobox-control/README.md create mode 100644 packages/components/src/combobox-control/index.js create mode 100644 packages/components/src/combobox-control/stories/index.js create mode 100644 packages/components/src/combobox-control/style.scss diff --git a/docs/manifest.json b/docs/manifest.json index 4d9e0ebdd9e71..65eb5eec3839f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -743,6 +743,12 @@ "markdown_source": "../packages/components/src/color-picker/README.md", "parent": "components" }, + { + "title": "ComboboxControl", + "slug": "combobox-control", + "markdown_source": "../packages/components/src/combobox-control/README.md", + "parent": "components" + }, { "title": "CustomSelectControl", "slug": "custom-select-control", diff --git a/package-lock.json b/package-lock.json index 2d5113844c748..b0ad2980749a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29505,7 +29505,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "resolved": false, "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { @@ -29534,7 +29534,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md new file mode 100644 index 0000000000000..f888a3211e364 --- /dev/null +++ b/packages/components/src/combobox-control/README.md @@ -0,0 +1,146 @@ +# ComboboxControl + +`ComboboxControl` is an enhanced version of a [`CustomSelectControl`](/packages/components/src/custom-select-control/readme.md), with the addition of being able to search for options using a search input. + +## Table of contents + +1. [Design guidelines](#design-guidelines) +2. [Development guidelines](#development-guidelines) +3. [Related components](#related-components) + +## Design guidelines + +These are the same as [the ones for `CustomSelectControl`s](/packages/components/src/select-control/readme.md#design-guidelines), but this component is better suited for when there are too many items to scroll through or load at once so you need to filter them based on user input. + +## Development guidelines + +### Usage + +```jsx +/** + * WordPress dependencies + */ +import { ComboboxControl } from "@wordpress/components"; +import { useState } from "@wordpress/compose"; + +const options = [ + { + key: "small", + name: "Small", + style: { fontSize: "50%" } + }, + { + key: "normal", + name: "Normal", + style: { fontSize: "100%" } + }, + { + key: "large", + name: "Large", + style: { fontSize: "200%" } + }, + { + key: "huge", + name: "Huge", + style: { fontSize: "300%" } + } +]; + +function MyComboboxControl() { + const [, setFontSize] = useState(); + const [filteredOptions, setFilteredOptions] = useState(options); + return ( + + setFilteredOptions( + options.filter(option => + option.name.toLowerCase().startsWith(inputValue.toLowerCase()) + ) + ) + } + onChange={({ selectedItem }) => setFontSize(selectedItem)} + /> + ); +} + +function MyControlledComboboxControl() { + const [fontSize, setFontSize] = useState(options[0]); + const [filteredOptions, setFilteredOptions] = useState(options); + return ( + + setFilteredOptions( + options.filter(option => + option.name.toLowerCase().startsWith(inputValue.toLowerCase()) + ) + ) + } + onChange={({ selectedItem }) => setFontSize(selectedItem)} + value={options.find(option => option.key === fontSize.key)} + /> + ); +} +``` + +### Props + +#### className + +A custom class name to append to the outer `
`. + +- Type: `String` +- Required: No + +#### hideLabelFromVision + +Used to visually hide the label. It will always be visible to screen readers. + +- Type: `Boolean` +- Required: No + +#### label + +The label for the control. + +- Type: `String` +- Required: Yes + +#### options + +The options that can be chosen from. + +- Type: `Array<{ key: String, name: String, style: ?{}, ...rest }>` +- Required: Yes + +#### onInputValueChange + +Function called with the control's search input value changes. The `inputValue` property contains the next input value. + +- Type: `Function` +- Required: No + +#### onChange + +Function called with the control's internal state changes. The `selectedItem` property contains the next selected item. + +- Type: `Function` +- Required: No + +#### value + +Can be used to externally control the value of the control, like in the `MyControlledComboboxControl` example above. + +- Type: `Object` +- Required: No + +## Related components + +- Like this component, but without a search input, the `CustomSelectControl` component. + +- To select one option from a set, when you want to show all the available options at once, use the `Radio` component. +- To select one or more items from a set, use the `CheckboxControl` component. +- To toggle a single setting on or off, use the `ToggleControl` component. diff --git a/packages/components/src/combobox-control/index.js b/packages/components/src/combobox-control/index.js new file mode 100644 index 0000000000000..a8befe7416e5a --- /dev/null +++ b/packages/components/src/combobox-control/index.js @@ -0,0 +1,126 @@ +/** + * External dependencies + */ +import { useCombobox } from 'downshift'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { Button, Dashicon } from '../'; + +const itemToString = ( item ) => item && item.name; +export default function ComboboxControl( { + className, + hideLabelFromVision, + label, + options: items, + onInputValueChange: onInputValueChange, + onChange: onSelectedItemChange, + value: _selectedItem, +} ) { + const { + getLabelProps, + getToggleButtonProps, + getComboboxProps, + getInputProps, + getMenuProps, + getItemProps, + isOpen, + highlightedIndex, + selectedItem, + } = useCombobox( { + initialSelectedItem: items[ 0 ], + items, + itemToString, + onInputValueChange, + onSelectedItemChange, + selectedItem: _selectedItem, + } ); + const menuProps = getMenuProps( { + className: 'components-combobox-control__menu', + } ); + // We need this here, because the null active descendant is not + // fully ARIA compliant. + if ( + menuProps[ 'aria-activedescendant' ] && + menuProps[ 'aria-activedescendant' ].slice( + 0, + 'downshift-null'.length + ) === 'downshift-null' + ) { + delete menuProps[ 'aria-activedescendant' ]; + } + return ( +
+ { /* eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */ } + +
+ + +
+
    + { isOpen && + items.map( ( item, index ) => ( + // eslint-disable-next-line react/jsx-key +
  • + { item === selectedItem && ( + + ) } + { item.name } +
  • + ) ) } +
+
+ ); +} diff --git a/packages/components/src/combobox-control/stories/index.js b/packages/components/src/combobox-control/stories/index.js new file mode 100644 index 0000000000000..aa621666466fe --- /dev/null +++ b/packages/components/src/combobox-control/stories/index.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import ComboboxControl from '../'; + +export default { title: 'ComboboxControl', component: ComboboxControl }; + +const options = [ + { + key: 'small', + name: 'Small', + style: { fontSize: '50%' }, + }, + { + key: 'normal', + name: 'Normal', + style: { fontSize: '100%' }, + }, + { + key: 'large', + name: 'Large', + style: { fontSize: '200%' }, + }, + { + key: 'huge', + name: 'Huge', + style: { fontSize: '300%' }, + }, +]; +function ComboboxControlWithState() { + const [ filteredOptions, setFilteredOptions ] = useState( options ); + return ( + + setFilteredOptions( + options.filter( ( option ) => + option.name + .toLowerCase() + .startsWith( inputValue.toLowerCase() ) + ) + ) + } + /> + ); +} +export const _default = () => ; diff --git a/packages/components/src/combobox-control/style.scss b/packages/components/src/combobox-control/style.scss new file mode 100644 index 0000000000000..655221c2b9c2d --- /dev/null +++ b/packages/components/src/combobox-control/style.scss @@ -0,0 +1,70 @@ +.components-combobox-control { + color: $dark-gray-500; + position: relative; +} + +.components-combobox-control__label { + display: block; + margin-bottom: 5px; +} + +.components-combobox-control__button { + border: 1px solid $dark-gray-200; + border-radius: 4px; + color: $dark-gray-500; + display: inline-block; + min-height: 30px; + min-width: 130px; + position: relative; + text-align: left; + + &:focus { + border-color: $blue-medium-500; + } + + &-input { + border: none; + height: calc(100% - 2px); + left: 1px; + padding: 0 4px; + position: absolute; + top: 1px; + width: calc(100% - 2px); + } + + &-button:hover { + box-shadow: none !important; + } + + &-icon { + height: 100%; + padding: 0 4px; + position: absolute; + right: 0; + top: 0; + } +} + +.components-combobox-control__menu { + background: $white; + min-width: 100%; + padding: 0; + position: absolute; + z-index: z-index(".components-popover"); +} + +.components-combobox-control__item { + align-items: center; + display: flex; + list-style-type: none; + padding: 10px 5px 10px 25px; + + &.is-highlighted { + background: $light-gray-500; + } + + &-icon { + margin-left: -20px; + margin-right: 0; + } +} diff --git a/packages/components/src/custom-select-control/README.md b/packages/components/src/custom-select-control/README.md index 7c2a14fe276da..8826701604277 100644 --- a/packages/components/src/custom-select-control/README.md +++ b/packages/components/src/custom-select-control/README.md @@ -20,8 +20,8 @@ These are the same as [the ones for `SelectControl`s](/packages/components/src/s /** * WordPress dependencies */ -import { CustomSelectControl } from "@wordpress/components"; -import { useState } from "@wordpress/compose"; +import { CustomSelectControl } from '@wordpress/components'; +import { useState } from '@wordpress/compose'; const options = [ { @@ -75,43 +75,51 @@ function MyControlledCustomSelectControl() { #### className A custom class name to append to the outer `
`. -- Type: `String` -- Required: No + +- Type: `String` +- Required: No #### hideLabelFromVision Used to visually hide the label. It will always be visible to screen readers. -- Type: `Boolean` -- Required: No + +- Type: `Boolean` +- Required: No #### label The label for the control. -- Type: `String` -- Required: Yes + +- Type: `String` +- Required: Yes #### options The options that can be chosen from. -- Type: `Array<{ key: String, name: String, style: ?{}, className: ?String, ...rest }>` -- Required: Yes + +- Type: `Array<{ key: String, name: String, style: ?{}, className: ?String, ...rest }>` +- Required: Yes #### onChange Function called with the control's internal state changes. The `selectedItem` property contains the next selected item. -- Type: `Function` -- Required: No + +- Type: `Function` +- Required: No #### value Can be used to externally control the value of the control, like in the `MyControlledCustomSelectControl` example above. -- Type: `Object` -- Required: No + +- Type: `Object` +- Required: No ## Related components -- Like this component, but implemented using a native `` for when custom styling is not necessary, the `SelectControl` component. + +- To select one option from a set, when you want to show all the available options at once, use the `Radio` component. +- To select one or more items from a set, use the `CheckboxControl` component. +- To toggle a single setting on or off, use the `ToggleControl` component. -- To select one option from a set, when you want to show all the available options at once, use the `Radio` component. -- To select one or more items from a set, use the `CheckboxControl` component. -- To toggle a single setting on or off, use the `ToggleControl` component. +- If you have a lot of items, `ComboboxControl` might be a better fit. diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 83e26e37cb09a..5fe4afbb8305e 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -8,6 +8,7 @@ @import "./circular-option-picker/style.scss"; @import "./color-indicator/style.scss"; @import "./color-picker/style.scss"; +@import "./combobox-control/style.scss"; @import "./custom-gradient-picker/style.scss"; @import "./custom-select-control/style.scss"; @import "./dashicon/style.scss"; From ac050ac6433adcfc9028c6c0e6541e1c9e20b2d4 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Wed, 19 Feb 2020 22:07:15 -0800 Subject: [PATCH 2/3] Components: Update Downshift. --- package-lock.json | 2 +- packages/components/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0ad2980749a1..52c5e440de8e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10982,7 +10982,7 @@ "@wordpress/warning": "file:packages/warning", "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", - "downshift": "^4.0.5", + "downshift": "^5.0.3", "gradient-parser": "^0.1.5", "lodash": "^4.17.15", "memize": "^1.1.0", diff --git a/packages/components/package.json b/packages/components/package.json index 88a982b0a193a..76b64a1b857d7 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -45,7 +45,7 @@ "@wordpress/warning": "file:../warning", "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", - "downshift": "^4.0.5", + "downshift": "^5.0.3", "gradient-parser": "^0.1.5", "lodash": "^4.17.15", "memize": "^1.1.0", From 20b2cbfe1642f2b3c70148d5297b802f5ed2e612 Mon Sep 17 00:00:00 2001 From: epiqueras Date: Mon, 18 May 2020 14:11:02 -0700 Subject: [PATCH 3/3] Components: Update Downshift. --- package-lock.json | 30 +++++++++++++++--------------- packages/components/package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52c5e440de8e3..e2de1411170fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10982,7 +10982,7 @@ "@wordpress/warning": "file:packages/warning", "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", - "downshift": "^5.0.3", + "downshift": "^5.4.0", "gradient-parser": "^0.1.5", "lodash": "^4.17.15", "memize": "^1.1.0", @@ -23607,9 +23607,9 @@ } }, "compute-scroll-into-view": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.11.tgz", - "integrity": "sha512-uUnglJowSe0IPmWOdDtrlHXof5CTIJitfJEyITHBW6zDVOGu9Pjk5puaLM73SLcwak0L4hEjO7Td88/a6P5i7A==" + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.13.tgz", + "integrity": "sha512-o+w9w7A98aAFi/GjK8cxSV+CdASuPa2rR5UWs3+yHkJzWqaKoBEufFNWYaXInCSmUfDCVhesG+v9MTWqOjsxFg==" }, "computed-style": { "version": "0.1.4", @@ -25714,20 +25714,20 @@ "integrity": "sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=" }, "downshift": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-4.0.7.tgz", - "integrity": "sha512-w6KFbpnMZrO53Lcbh21lRTSokEvz+FCdv7fAtN8+Oxvst+qUTIy/2FQCX6AQUncRb/gOqG4aBqm2fGgbsmAiGg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-5.4.0.tgz", + "integrity": "sha512-r3ikikP6H/2c+WSWcP/1nOwBSXwmyiuH3LeEmsjKumWjqd+FAazNUIa/ox2VA+qQ86JuaAIaA4xw79G3Sz/XMA==", "requires": { - "@babel/runtime": "^7.4.5", - "compute-scroll-into-view": "^1.0.9", + "@babel/runtime": "^7.9.1", + "compute-scroll-into-view": "^1.0.13", "prop-types": "^15.7.2", - "react-is": "^16.9.0" + "react-is": "^16.13.1" }, "dependencies": { "react-is": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", - "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -29505,7 +29505,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "optional": true, "requires": { @@ -29534,7 +29534,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { diff --git a/packages/components/package.json b/packages/components/package.json index 76b64a1b857d7..1c4b604797373 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -45,7 +45,7 @@ "@wordpress/warning": "file:../warning", "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", - "downshift": "^5.0.3", + "downshift": "^5.4.0", "gradient-parser": "^0.1.5", "lodash": "^4.17.15", "memize": "^1.1.0",