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 */ }
+
+
+
+ { isOpen &&
+ items.map( ( item, index ) => (
+ // eslint-disable-next-line react/jsx-key
+ -
+ { item === selectedItem && (
+
+ ) }
+ { item.name }
+
+ ) ) }
+
+
+ );
+}
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 (