diff --git a/package-lock.json b/package-lock.json index 8f777c41671e1a..742b334e394bc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7712,6 +7712,7 @@ "classnames": "^2.2.5", "clipboard": "^2.0.1", "dom-scroll-into-view": "^1.2.1", + "downshift": "^3.3.4", "lodash": "^4.17.15", "memize": "^1.0.5", "moment": "^2.22.1", @@ -12053,6 +12054,11 @@ } } }, + "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==" + }, "computed-style": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz", @@ -14093,6 +14099,24 @@ "dotenv-defaults": "^1.0.2" } }, + "downshift": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-3.4.3.tgz", + "integrity": "sha512-lk0Q1VF4eTDe4EMzYtdVCPdu58ZRFyK3wxEAGUeKqPRDoHDgoS9/TaxW2w+hEbeh9yBMU2IKX8lQkNn6YTfZ4w==", + "requires": { + "@babel/runtime": "^7.4.5", + "compute-scroll-into-view": "^1.0.9", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + }, + "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==" + } + } + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -22079,7 +22103,7 @@ "dependencies": { "clone-deep": { "version": "0.2.4", - "resolved": "http://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", "dev": true, "requires": { @@ -22113,7 +22137,7 @@ "dependencies": { "kind-of": { "version": "2.0.1", - "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", "dev": true, "requires": { diff --git a/packages/components/package.json b/packages/components/package.json index 3b1ebe1dd81252..15f5c06e598a43 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -37,6 +37,7 @@ "classnames": "^2.2.5", "clipboard": "^2.0.1", "dom-scroll-into-view": "^1.2.1", + "downshift": "^3.3.4", "lodash": "^4.17.15", "memize": "^1.0.5", "moment": "^2.22.1", diff --git a/packages/components/src/custom-select/index.js b/packages/components/src/custom-select/index.js new file mode 100644 index 00000000000000..1bf99600db1f54 --- /dev/null +++ b/packages/components/src/custom-select/index.js @@ -0,0 +1,149 @@ +/** + * External dependencies + */ +import { useSelect } from 'downshift'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import { Button, Dashicon } from '../'; + +const itemToString = ( item ) => item && item.name; +// This is needed so that in Windows, where +// the menu does not necessarily open on +// key up/down, you can still switch between +// options with the menu closed. +const stateReducer = ( + { selectedItem }, + { type, changes, props: { items } } +) => { + // TODO: Remove this. + // eslint-disable-next-line no-console + console.debug( + 'Selected Item: ', + selectedItem, + 'Type: ', + type, + 'Changes: ', + changes, + 'Items: ', + items + ); + switch ( type ) { + case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown: + // If we already have a selected item, try to select the next one, + // without circular navigation. Otherwise, select the first item. + return { + selectedItem: + items[ + selectedItem ? + Math.min( items.indexOf( selectedItem ) + 1, items.length - 1 ) : + 0 + ], + }; + case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp: + // If we already have a selected item, try to select the previous one, + // without circular navigation. Otherwise, select the last item. + return { + selectedItem: + items[ + selectedItem ? + Math.max( items.indexOf( selectedItem ) - 1, 0 ) : + items.length - 1 + ], + }; + default: + return changes; + } +}; +export default function CustomSelect( { + className, + hideLabelFromVision, + label, + items, + onSelectedItemChange, + selectedItem: _selectedItem, +} ) { + const { + getLabelProps, + getToggleButtonProps, + getMenuProps, + getItemProps, + isOpen, + highlightedIndex, + selectedItem, + } = useSelect( { + initialSelectedItem: items[ 0 ], + items, + itemToString, + onSelectedItemChange, + selectedItem: _selectedItem, + stateReducer, + } ); + const menuProps = getMenuProps( { + className: 'components-custom-select__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 */ } + + + +
+ ); +} diff --git a/packages/components/src/custom-select/stories/index.js b/packages/components/src/custom-select/stories/index.js new file mode 100644 index 00000000000000..83310d9e261215 --- /dev/null +++ b/packages/components/src/custom-select/stories/index.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import CustomSelect from '../'; + +export default { title: 'CustomSelect', component: CustomSelect }; + +const items = [ + { + 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%' }, + }, +]; +export const _default = () => ; diff --git a/packages/components/src/custom-select/style.scss b/packages/components/src/custom-select/style.scss new file mode 100644 index 00000000000000..dfbd9fc55a9dc1 --- /dev/null +++ b/packages/components/src/custom-select/style.scss @@ -0,0 +1,56 @@ +.components-custom-select { + color: $dark-gray-500; + position: relative; +} + +.components-custom-select__label { + display: block; + margin-bottom: 5px; +} + +.components-custom-select__button { + border: 1px solid $dark-gray-200; + border-radius: 4px; + color: $dark-gray-500; + display: inline; + min-height: 30px; + min-width: 130px; + position: relative; + text-align: left; + + &:focus { + border-color: $blue-medium-500; + } + + &-icon { + height: 100%; + padding: 0 4px; + position: absolute; + right: 0; + top: 0; + } +} + +.components-custom-select__menu { + background: $white; + padding: 0; + position: absolute; + width: 100%; + z-index: z-index(".components-popover"); +} + +.components-custom-select__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/font-size-picker/index.js b/packages/components/src/font-size-picker/index.js index 9fc6567865dbe4..726812a5c4feb5 100644 --- a/packages/components/src/font-size-picker/index.js +++ b/packages/components/src/font-size-picker/index.js @@ -4,13 +4,14 @@ */ import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { withInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ import Button from '../button'; import RangeControl from '../range-control'; -import SelectControl from '../select-control'; +import CustomSelect from '../custom-select'; function getSelectValueFromFontSize( fontSizes, value ) { if ( value ) { @@ -22,8 +23,12 @@ function getSelectValueFromFontSize( fontSizes, value ) { function getSelectOptions( optionsArray ) { return [ - ...optionsArray.map( ( option ) => ( { value: option.slug, label: option.name } ) ), - { value: 'custom', label: __( 'Custom' ) }, + ...optionsArray.map( ( option ) => ( { + key: option.slug, + name: option.name, + style: { fontSize: option.size }, + } ) ), + { key: 'custom', name: __( 'Custom' ) }, ]; } @@ -34,6 +39,7 @@ function FontSizePicker( { onChange, value, withSlider = false, + instanceId, } ) { const [ currentSelectValue, setCurrentSelectValue ] = useState( getSelectValueFromFontSize( fontSizes, value ) ); @@ -51,38 +57,40 @@ function FontSizePicker( { onChange( Number( newValue ) ); }; - const onSelectChangeValue = ( eventValue ) => { - setCurrentSelectValue( eventValue ); - const selectedFont = fontSizes.find( ( font ) => font.slug === eventValue ); - if ( selectedFont ) { - onChange( selectedFont.size ); - } + const onSelectChangeValue = ( { selectedItem } ) => { + setCurrentSelectValue( selectedItem.key ); + onChange( selectedItem.style && selectedItem.style.fontSize ); }; + const items = getSelectOptions( fontSizes ); + const rangeControlNumberId = `components-range-control__number#${ instanceId }`; return (
- + { __( 'Font Size' ) }
{ ( fontSizes.length > 0 ) && - item.key === currentSelectValue ) || items[ 0 ] } + onSelectedItemChange={ onSelectChangeValue } /> } { ( ! withSlider && ! disableCustomFontSizes ) && - +
+ + +
} +
    +
+
+ +
- +
    +
      +
        + +`;