From eef24a2a019ba083379f427917e331f4ace4077c Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 02:25:20 +0900 Subject: [PATCH 01/16] ComboboxControl: Rename file --- packages/components/src/combobox-control/{index.js => index.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/components/src/combobox-control/{index.js => index.tsx} (100%) diff --git a/packages/components/src/combobox-control/index.js b/packages/components/src/combobox-control/index.tsx similarity index 100% rename from packages/components/src/combobox-control/index.js rename to packages/components/src/combobox-control/index.tsx From 7b22327c8ca755cd4cc58ee0ce6170713169ba84 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 02:28:17 +0900 Subject: [PATCH 02/16] Remove tabIndex in BaseControl It doesn't do anything --- packages/components/src/combobox-control/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index f01eb7dd4cac2..4857c19273335 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -235,7 +235,6 @@ function ComboboxControl( { className, 'components-combobox-control' ) } - tabIndex="-1" label={ label } id={ `components-form-token-input-${ instanceId }` } hideLabelFromVision={ hideLabelFromVision } From 0c0ff12289cdf767eeeaad191e675662cf322393 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 03:36:29 +0900 Subject: [PATCH 03/16] ComboboxControl: Add types --- .../components/src/combobox-control/README.md | 10 +-- .../components/src/combobox-control/index.tsx | 58 ++++++++++++----- .../components/src/combobox-control/styles.ts | 5 +- .../components/src/combobox-control/types.ts | 65 +++++++++++++++++++ 4 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 packages/components/src/combobox-control/types.ts diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index f6e81d2cff351..43ad697ae21cd 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -92,35 +92,35 @@ If this property is added, a help text will be generated using help property as The options that can be chosen from. -- Type: `Array<{ value: String, label: String }>` +- Type: `Array<{ value: string, label: string }>` - Required: Yes #### onFilterValueChange Function called with the control's search input value changes. The argument contains the next input value. -- Type: `Function` +- Type: `( value: string ) => void` - Required: No #### onChange Function called with the selected value changes. -- Type: `Function` +- Type: `( value: string | null ) => void` - Required: No #### value The current value of the input. -- Type: `mixed` +- Type: `string | null` - Required: Yes #### __experimentalRenderItem Custom renderer invoked for each option in the suggestion list. The render prop receives as its argument an object containing, under the `item` key, the single option's data (directly from the array of data passed to the `options` prop). -- Type: `Function` - `( args: { item: object } ) => ReactNode` +- Type: `( args: { item: object } ) => ReactNode` - Required: No ## Related components diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index 4857c19273335..ad240af8f1e9f 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -30,23 +30,35 @@ import { FlexBlock, FlexItem } from '../flex'; import withFocusOutside from '../higher-order/with-focus-outside'; import { useControlledValue } from '../utils/hooks'; import { normalizeTextString } from '../utils/strings'; +import type { ComboboxControlOption, ComboboxControlProps } from './types'; +import type { TokenInputProps } from '../form-token-field/types'; const noop = () => {}; const DetectOutside = withFocusOutside( class extends Component { + // @ts-expect-error - TODO: Should be resolved when `withFocusOutside` is refactored to TypeScript handleFocusOutside( event ) { + // @ts-expect-error - TODO: Should be resolved when `withFocusOutside` is refactored to TypeScript this.props.onFocusOutside( event ); } render() { + // @ts-expect-error - TODO: Should be resolved when `withFocusOutside` is refactored to TypeScript return this.props.children; } } ); +const getIndexOfMatchingSuggestion = ( + selectedSuggestion: ComboboxControlOption | null, + matchingSuggestions: ComboboxControlOption[] +) => + selectedSuggestion === null + ? -1 + : matchingSuggestions.indexOf( selectedSuggestion ); + function ComboboxControl( { - /** Start opting into the new margin-free styles that will become the default in a future version. */ __nextHasNoMarginBottom = false, __next36pxDefaultSize, value: valueProp, @@ -62,7 +74,7 @@ function ComboboxControl( { selected: __( 'Item selected.' ), }, __experimentalRenderItem, -} ) { +}: ComboboxControlProps ) { const [ value, setValue ] = useControlledValue( { value: valueProp, onChange: onChangeProp, @@ -80,11 +92,11 @@ function ComboboxControl( { const [ isExpanded, setIsExpanded ] = useState( false ); const [ inputHasFocus, setInputHasFocus ] = useState( false ); const [ inputValue, setInputValue ] = useState( '' ); - const inputContainer = useRef(); + const inputContainer = useRef< HTMLInputElement >( null ); const matchingSuggestions = useMemo( () => { - const startsWithMatch = []; - const containsMatch = []; + const startsWithMatch: ComboboxControlOption[] = []; + const containsMatch: ComboboxControlOption[] = []; const match = normalizeTextString( inputValue ); options.forEach( ( option ) => { const index = normalizeTextString( option.label ).indexOf( match ); @@ -98,7 +110,9 @@ function ComboboxControl( { return startsWithMatch.concat( containsMatch ); }, [ inputValue, options ] ); - const onSuggestionSelected = ( newSelectedSuggestion ) => { + const onSuggestionSelected = ( + newSelectedSuggestion: ComboboxControlOption + ) => { setValue( newSelectedSuggestion.value ); speak( messages.selected, 'assertive' ); setSelectedSuggestion( newSelectedSuggestion ); @@ -107,7 +121,10 @@ function ComboboxControl( { }; const handleArrowNavigation = ( offset = 1 ) => { - const index = matchingSuggestions.indexOf( selectedSuggestion ); + const index = getIndexOfMatchingSuggestion( + selectedSuggestion, + matchingSuggestions + ); let nextIndex = index + offset; if ( nextIndex < 0 ) { nextIndex = matchingSuggestions.length - 1; @@ -118,7 +135,9 @@ function ComboboxControl( { setIsExpanded( true ); }; - const onKeyDown = ( event ) => { + const onKeyDown: React.KeyboardEventHandler< HTMLDivElement > = ( + event + ) => { let preventDefault = false; if ( @@ -177,7 +196,7 @@ function ComboboxControl( { setIsExpanded( false ); }; - const onInputChange = ( event ) => { + const onInputChange: TokenInputProps[ 'onChange' ] = ( event ) => { const text = event.value; setInputValue( text ); onFilterValueChange( text ); @@ -188,14 +207,17 @@ function ComboboxControl( { const handleOnReset = () => { setValue( null ); - inputContainer.current.focus(); + inputContainer.current?.focus(); }; // Update current selections when the filter input changes. useEffect( () => { const hasMatchingSuggestions = matchingSuggestions.length > 0; const hasSelectedMatchingSuggestions = - matchingSuggestions.indexOf( selectedSuggestion ) > 0; + getIndexOfMatchingSuggestion( + selectedSuggestion, + matchingSuggestions + ) > 0; if ( hasMatchingSuggestions && ! hasSelectedMatchingSuggestions ) { // If the current selection isn't present in the list of suggestions, then automatically select the first item from the list of suggestions. @@ -242,7 +264,7 @@ function ComboboxControl( { >
@@ -278,13 +301,14 @@ function ComboboxControl( { { isExpanded && ( suggestion.label } suggestions={ matchingSuggestions } - selectedIndex={ matchingSuggestions.indexOf( - selectedSuggestion + selectedIndex={ getIndexOfMatchingSuggestion( + selectedSuggestion, + matchingSuggestions ) } onHover={ setSelectedSuggestion } onSelect={ onSuggestionSelected } diff --git a/packages/components/src/combobox-control/styles.ts b/packages/components/src/combobox-control/styles.ts index 6d7164b493e84..25c71788fd602 100644 --- a/packages/components/src/combobox-control/styles.ts +++ b/packages/components/src/combobox-control/styles.ts @@ -9,8 +9,11 @@ import { css } from '@emotion/react'; */ import { Flex } from '../flex'; import { space } from '../ui/utils/space'; +import type { ComboboxControlProps } from './types'; -const deprecatedDefaultSize = ( { __next36pxDefaultSize } ) => +const deprecatedDefaultSize = ( { + __next36pxDefaultSize, +}: Pick< ComboboxControlProps, '__next36pxDefaultSize' > ) => ! __next36pxDefaultSize && css` height: 28px; // 30px - 2px vertical borders on parent container diff --git a/packages/components/src/combobox-control/types.ts b/packages/components/src/combobox-control/types.ts new file mode 100644 index 0000000000000..5de7da83d28c7 --- /dev/null +++ b/packages/components/src/combobox-control/types.ts @@ -0,0 +1,65 @@ +/** + * Internal dependencies + */ +import type { BaseControlProps } from '../base-control/types'; + +export type ComboboxControlOption = { + label: string; + value: string; +}; + +export type ComboboxControlProps = Pick< + BaseControlProps, + | '__nextHasNoMarginBottom' + | 'className' + | 'label' + | 'hideLabelFromVision' + | 'help' +> & { + /** + * Custom renderer invoked for each option in the suggestion list. + * The render prop receives as its argument an object containing, under the `item` key, + * the single option's data (directly from the array of data passed to the `options` prop). + */ + __experimentalRenderItem?: ( args: { + item: ComboboxControlOption; + } ) => React.ReactNode; + /** + * Start opting into the larger default height that will become the default size in a future version. + * + * @default false + */ + __next36pxDefaultSize?: boolean; + /** + * Show a reset button to clear the input. + * + * @default true + */ + allowReset?: boolean; + /** + * Customizable UI messages. + */ + messages?: { + /** + * The message to announce to screen readers when a suggestion is selected. + * + * @default `__( 'Item selected.' )` + */ + selected: string; + }; + /** + * Function called with the selected value changes. + */ + onChange?: ( value: ComboboxControlProps[ 'value' ] ) => void; + /** + * Function called with the control's search input value changes. The argument contains the next input value. + * + * @default noop + */ + onFilterValueChange?: ( value: string ) => void; + /** + * The options that can be chosen from. + */ + options: ComboboxControlOption[]; + value: string | null; +}; From 27600428fb641b141af949a4c2c279096d65d399 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 03:56:44 +0900 Subject: [PATCH 04/16] Mark `value` prop as optional This definitely works with the initial value undefined, as seen in the readme code snippet and in unit tests. --- packages/components/src/combobox-control/README.md | 2 +- packages/components/src/combobox-control/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index 43ad697ae21cd..2e4da53bdee6d 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -114,7 +114,7 @@ Function called with the selected value changes. The current value of the input. - Type: `string | null` -- Required: Yes +- Required: No #### __experimentalRenderItem diff --git a/packages/components/src/combobox-control/types.ts b/packages/components/src/combobox-control/types.ts index 5de7da83d28c7..8af31801e4a9a 100644 --- a/packages/components/src/combobox-control/types.ts +++ b/packages/components/src/combobox-control/types.ts @@ -61,5 +61,5 @@ export type ComboboxControlProps = Pick< * The options that can be chosen from. */ options: ComboboxControlOption[]; - value: string | null; + value?: string | null; }; From 00f8854a5517af6e2bd0a1b3cae3e515b0ca00fc Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 03:56:59 +0900 Subject: [PATCH 05/16] Convert tests --- .../test/{index.js => index.tsx} | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) rename packages/components/src/combobox-control/test/{index.js => index.tsx} (94%) diff --git a/packages/components/src/combobox-control/test/index.js b/packages/components/src/combobox-control/test/index.tsx similarity index 94% rename from packages/components/src/combobox-control/test/index.js rename to packages/components/src/combobox-control/test/index.tsx index 0a017b35bd60f..0f1a25c251366 100644 --- a/packages/components/src/combobox-control/test/index.js +++ b/packages/components/src/combobox-control/test/index.tsx @@ -12,7 +12,8 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import ComboboxControl from '../'; +import ComboboxControl from '..'; +import type { ComboboxControlOption, ComboboxControlProps } from '../types'; const timezones = [ { label: 'Greenwich Mean Time', value: 'GMT' }, @@ -49,20 +50,21 @@ const timezones = [ ]; const defaultLabelText = 'Select a timezone'; -const getLabel = ( labelText ) => screen.getByText( labelText ); -const getInput = ( name ) => screen.getByRole( 'combobox', { name } ); -const getOption = ( name ) => screen.getByRole( 'option', { name } ); +const getLabel = ( labelText: string ) => screen.getByText( labelText ); +const getInput = ( name: string ) => screen.getByRole( 'combobox', { name } ); +const getOption = ( name: string ) => screen.getByRole( 'option', { name } ); const getAllOptions = () => screen.getAllByRole( 'option' ); -const getOptionSearchString = ( option ) => option.label.substring( 0, 11 ); +const getOptionSearchString = ( option: ComboboxControlOption ) => + option.label.substring( 0, 11 ); const setupUser = () => userEvent.setup(); const ControlledComboboxControl = ( { value: valueProp, onChange, ...props -} ) => { +}: ComboboxControlProps ) => { const [ value, setValue ] = useState( valueProp ); - const handleOnChange = ( newValue ) => { + const handleOnChange: ComboboxControlProps[ 'onChange' ] = ( newValue ) => { setValue( newValue ); onChange?.( newValue ); }; From 22e5f4d514b3de07f14abbfef76b24d455d50e0b Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 04:43:58 +0900 Subject: [PATCH 06/16] Fixup --- packages/components/src/combobox-control/README.md | 4 ++-- packages/components/src/combobox-control/types.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index 2e4da53bdee6d..eb3c0a58ad78b 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -97,7 +97,7 @@ The options that can be chosen from. #### onFilterValueChange -Function called with the control's search input value changes. The argument contains the next input value. +Function called when the control's search input value changes. The argument contains the next input value. - Type: `( value: string ) => void` - Required: No @@ -111,7 +111,7 @@ Function called with the selected value changes. #### value -The current value of the input. +The current value of the control. - Type: `string | null` - Required: No diff --git a/packages/components/src/combobox-control/types.ts b/packages/components/src/combobox-control/types.ts index 8af31801e4a9a..bcc4b8b9ed03c 100644 --- a/packages/components/src/combobox-control/types.ts +++ b/packages/components/src/combobox-control/types.ts @@ -6,6 +6,7 @@ import type { BaseControlProps } from '../base-control/types'; export type ComboboxControlOption = { label: string; value: string; + [ key: string ]: any; }; export type ComboboxControlProps = Pick< @@ -52,7 +53,7 @@ export type ComboboxControlProps = Pick< */ onChange?: ( value: ComboboxControlProps[ 'value' ] ) => void; /** - * Function called with the control's search input value changes. The argument contains the next input value. + * Function called when the control's search input value changes. The argument contains the next input value. * * @default noop */ @@ -61,5 +62,8 @@ export type ComboboxControlProps = Pick< * The options that can be chosen from. */ options: ComboboxControlOption[]; + /** + * The current value of the control. + */ value?: string | null; }; From bb17a6acd85cc779eafd4b9102e605726f71d7e7 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 04:44:07 +0900 Subject: [PATCH 07/16] Convert stories --- .../src/combobox-control/stories/index.js | 339 ------------------ .../src/combobox-control/stories/index.tsx | 118 ++++++ 2 files changed, 118 insertions(+), 339 deletions(-) delete mode 100644 packages/components/src/combobox-control/stories/index.js create mode 100644 packages/components/src/combobox-control/stories/index.tsx diff --git a/packages/components/src/combobox-control/stories/index.js b/packages/components/src/combobox-control/stories/index.js deleted file mode 100644 index fda098e19e364..0000000000000 --- a/packages/components/src/combobox-control/stories/index.js +++ /dev/null @@ -1,339 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import ComboboxControl from '../'; - -const countries = [ - { name: 'Afghanistan', code: 'AF' }, - { name: 'Åland Islands', code: 'AX' }, - { name: 'Albania', code: 'AL' }, - { name: 'Algeria', code: 'DZ' }, - { name: 'American Samoa', code: 'AS' }, - { name: 'Andorra', code: 'AD' }, - { name: 'Angola', code: 'AO' }, - { name: 'Anguilla', code: 'AI' }, - { name: 'Antarctica', code: 'AQ' }, - { name: 'Antigua and Barbuda', code: 'AG' }, - { name: 'Argentina', code: 'AR' }, - { name: 'Armenia', code: 'AM' }, - { name: 'Aruba', code: 'AW' }, - { name: 'Australia', code: 'AU' }, - { name: 'Austria', code: 'AT' }, - { name: 'Azerbaijan', code: 'AZ' }, - { name: 'Bahamas', code: 'BS' }, - { name: 'Bahrain', code: 'BH' }, - { name: 'Bangladesh', code: 'BD' }, - { name: 'Barbados', code: 'BB' }, - { name: 'Belarus', code: 'BY' }, - { name: 'Belgium', code: 'BE' }, - { name: 'Belize', code: 'BZ' }, - { name: 'Benin', code: 'BJ' }, - { name: 'Bermuda', code: 'BM' }, - { name: 'Bhutan', code: 'BT' }, - { name: 'Bolivia', code: 'BO' }, - { name: 'Bosnia and Herzegovina', code: 'BA' }, - { name: 'Botswana', code: 'BW' }, - { name: 'Bouvet Island', code: 'BV' }, - { name: 'Brazil', code: 'BR' }, - { name: 'British Indian Ocean Territory', code: 'IO' }, - { name: 'Brunei Darussalam', code: 'BN' }, - { name: 'Bulgaria', code: 'BG' }, - { name: 'Burkina Faso', code: 'BF' }, - { name: 'Burundi', code: 'BI' }, - { name: 'Cambodia', code: 'KH' }, - { name: 'Cameroon', code: 'CM' }, - { name: 'Canada', code: 'CA' }, - { name: 'Cape Verde', code: 'CV' }, - { name: 'Cayman Islands', code: 'KY' }, - { name: 'Central African Republic', code: 'CF' }, - { name: 'Chad', code: 'TD' }, - { name: 'Chile', code: 'CL' }, - { name: 'China', code: 'CN' }, - { name: 'Christmas Island', code: 'CX' }, - { name: 'Cocos (Keeling) Islands', code: 'CC' }, - { name: 'Colombia', code: 'CO' }, - { name: 'Comoros', code: 'KM' }, - { name: 'Congo', code: 'CG' }, - { name: 'Congo, The Democratic Republic of the', code: 'CD' }, - { name: 'Cook Islands', code: 'CK' }, - { name: 'Costa Rica', code: 'CR' }, - { name: "Cote D'Ivoire", code: 'CI' }, - { name: 'Croatia', code: 'HR' }, - { name: 'Cuba', code: 'CU' }, - { name: 'Cyprus', code: 'CY' }, - { name: 'Czech Republic', code: 'CZ' }, - { name: 'Denmark', code: 'DK' }, - { name: 'Djibouti', code: 'DJ' }, - { name: 'Dominica', code: 'DM' }, - { name: 'Dominican Republic', code: 'DO' }, - { name: 'Ecuador', code: 'EC' }, - { name: 'Egypt', code: 'EG' }, - { name: 'El Salvador', code: 'SV' }, - { name: 'Equatorial Guinea', code: 'GQ' }, - { name: 'Eritrea', code: 'ER' }, - { name: 'Estonia', code: 'EE' }, - { name: 'Ethiopia', code: 'ET' }, - { name: 'Falkland Islands (Malvinas)', code: 'FK' }, - { name: 'Faroe Islands', code: 'FO' }, - { name: 'Fiji', code: 'FJ' }, - { name: 'Finland', code: 'FI' }, - { name: 'France', code: 'FR' }, - { name: 'French Guiana', code: 'GF' }, - { name: 'French Polynesia', code: 'PF' }, - { name: 'French Southern Territories', code: 'TF' }, - { name: 'Gabon', code: 'GA' }, - { name: 'Gambia', code: 'GM' }, - { name: 'Georgia', code: 'GE' }, - { name: 'Germany', code: 'DE' }, - { name: 'Ghana', code: 'GH' }, - { name: 'Gibraltar', code: 'GI' }, - { name: 'Greece', code: 'GR' }, - { name: 'Greenland', code: 'GL' }, - { name: 'Grenada', code: 'GD' }, - { name: 'Guadeloupe', code: 'GP' }, - { name: 'Guam', code: 'GU' }, - { name: 'Guatemala', code: 'GT' }, - { name: 'Guernsey', code: 'GG' }, - { name: 'Guinea', code: 'GN' }, - { name: 'Guinea-Bissau', code: 'GW' }, - { name: 'Guyana', code: 'GY' }, - { name: 'Haiti', code: 'HT' }, - { name: 'Heard Island and Mcdonald Islands', code: 'HM' }, - { name: 'Holy See (Vatican City State)', code: 'VA' }, - { name: 'Honduras', code: 'HN' }, - { name: 'Hong Kong', code: 'HK' }, - { name: 'Hungary', code: 'HU' }, - { name: 'Iceland', code: 'IS' }, - { name: 'India', code: 'IN' }, - { name: 'Indonesia', code: 'ID' }, - { name: 'Iran, Islamic Republic Of', code: 'IR' }, - { name: 'Iraq', code: 'IQ' }, - { name: 'Ireland', code: 'IE' }, - { name: 'Isle of Man', code: 'IM' }, - { name: 'Israel', code: 'IL' }, - { name: 'Italy', code: 'IT' }, - { name: 'Jamaica', code: 'JM' }, - { name: 'Japan', code: 'JP' }, - { name: 'Jersey', code: 'JE' }, - { name: 'Jordan', code: 'JO' }, - { name: 'Kazakhstan', code: 'KZ' }, - { name: 'Kenya', code: 'KE' }, - { name: 'Kiribati', code: 'KI' }, - { name: "Korea, Democratic People'S Republic of", code: 'KP' }, - { name: 'Korea, Republic of', code: 'KR' }, - { name: 'Kuwait', code: 'KW' }, - { name: 'Kyrgyzstan', code: 'KG' }, - { name: "Lao People'S Democratic Republic", code: 'LA' }, - { name: 'Latvia', code: 'LV' }, - { name: 'Lebanon', code: 'LB' }, - { name: 'Lesotho', code: 'LS' }, - { name: 'Liberia', code: 'LR' }, - { name: 'Libyan Arab Jamahiriya', code: 'LY' }, - { name: 'Liechtenstein', code: 'LI' }, - { name: 'Lithuania', code: 'LT' }, - { name: 'Luxembourg', code: 'LU' }, - { name: 'Macao', code: 'MO' }, - { name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK' }, - { name: 'Madagascar', code: 'MG' }, - { name: 'Malawi', code: 'MW' }, - { name: 'Malaysia', code: 'MY' }, - { name: 'Maldives', code: 'MV' }, - { name: 'Mali', code: 'ML' }, - { name: 'Malta', code: 'MT' }, - { name: 'Marshall Islands', code: 'MH' }, - { name: 'Martinique', code: 'MQ' }, - { name: 'Mauritania', code: 'MR' }, - { name: 'Mauritius', code: 'MU' }, - { name: 'Mayotte', code: 'YT' }, - { name: 'Mexico', code: 'MX' }, - { name: 'Micronesia, Federated States of', code: 'FM' }, - { name: 'Moldova, Republic of', code: 'MD' }, - { name: 'Monaco', code: 'MC' }, - { name: 'Mongolia', code: 'MN' }, - { name: 'Montserrat', code: 'MS' }, - { name: 'Morocco', code: 'MA' }, - { name: 'Mozambique', code: 'MZ' }, - { name: 'Myanmar', code: 'MM' }, - { name: 'Namibia', code: 'NA' }, - { name: 'Nauru', code: 'NR' }, - { name: 'Nepal', code: 'NP' }, - { name: 'Netherlands', code: 'NL' }, - { name: 'Netherlands Antilles', code: 'AN' }, - { name: 'New Caledonia', code: 'NC' }, - { name: 'New Zealand', code: 'NZ' }, - { name: 'Nicaragua', code: 'NI' }, - { name: 'Niger', code: 'NE' }, - { name: 'Nigeria', code: 'NG' }, - { name: 'Niue', code: 'NU' }, - { name: 'Norfolk Island', code: 'NF' }, - { name: 'Northern Mariana Islands', code: 'MP' }, - { name: 'Norway', code: 'NO' }, - { name: 'Oman', code: 'OM' }, - { name: 'Pakistan', code: 'PK' }, - { name: 'Palau', code: 'PW' }, - { name: 'Palestinian Territory, Occupied', code: 'PS' }, - { name: 'Panama', code: 'PA' }, - { name: 'Papua New Guinea', code: 'PG' }, - { name: 'Paraguay', code: 'PY' }, - { name: 'Peru', code: 'PE' }, - { name: 'Philippines', code: 'PH' }, - { name: 'Pitcairn', code: 'PN' }, - { name: 'Poland', code: 'PL' }, - { name: 'Portugal', code: 'PT' }, - { name: 'Puerto Rico', code: 'PR' }, - { name: 'Qatar', code: 'QA' }, - { name: 'Reunion', code: 'RE' }, - { name: 'Romania', code: 'RO' }, - { name: 'Russian Federation', code: 'RU' }, - { name: 'RWANDA', code: 'RW' }, - { name: 'Saint Helena', code: 'SH' }, - { name: 'Saint Kitts and Nevis', code: 'KN' }, - { name: 'Saint Lucia', code: 'LC' }, - { name: 'Saint Pierre and Miquelon', code: 'PM' }, - { name: 'Saint Vincent and the Grenadines', code: 'VC' }, - { name: 'Samoa', code: 'WS' }, - { name: 'San Marino', code: 'SM' }, - { name: 'Sao Tome and Principe', code: 'ST' }, - { name: 'Saudi Arabia', code: 'SA' }, - { name: 'Senegal', code: 'SN' }, - { name: 'Serbia and Montenegro', code: 'CS' }, - { name: 'Seychelles', code: 'SC' }, - { name: 'Sierra Leone', code: 'SL' }, - { name: 'Singapore', code: 'SG' }, - { name: 'Slovakia', code: 'SK' }, - { name: 'Slovenia', code: 'SI' }, - { name: 'Solomon Islands', code: 'SB' }, - { name: 'Somalia', code: 'SO' }, - { name: 'South Africa', code: 'ZA' }, - { name: 'South Georgia and the South Sandwich Islands', code: 'GS' }, - { name: 'Spain', code: 'ES' }, - { name: 'Sri Lanka', code: 'LK' }, - { name: 'Sudan', code: 'SD' }, - { name: 'Suriname', code: 'SR' }, - { name: 'Svalbard and Jan Mayen', code: 'SJ' }, - { name: 'Swaziland', code: 'SZ' }, - { name: 'Sweden', code: 'SE' }, - { name: 'Switzerland', code: 'CH' }, - { name: 'Syrian Arab Republic', code: 'SY' }, - { name: 'Taiwan, Province of China', code: 'TW' }, - { name: 'Tajikistan', code: 'TJ' }, - { name: 'Tanzania, United Republic of', code: 'TZ' }, - { name: 'Thailand', code: 'TH' }, - { name: 'Timor-Leste', code: 'TL' }, - { name: 'Togo', code: 'TG' }, - { name: 'Tokelau', code: 'TK' }, - { name: 'Tonga', code: 'TO' }, - { name: 'Trinidad and Tobago', code: 'TT' }, - { name: 'Tunisia', code: 'TN' }, - { name: 'Turkey', code: 'TR' }, - { name: 'Turkmenistan', code: 'TM' }, - { name: 'Turks and Caicos Islands', code: 'TC' }, - { name: 'Tuvalu', code: 'TV' }, - { name: 'Uganda', code: 'UG' }, - { name: 'Ukraine', code: 'UA' }, - { name: 'United Arab Emirates', code: 'AE' }, - { name: 'United Kingdom', code: 'GB' }, - { name: 'United States', code: 'US' }, - { name: 'United States Minor Outlying Islands', code: 'UM' }, - { name: 'Uruguay', code: 'UY' }, - { name: 'Uzbekistan', code: 'UZ' }, - { name: 'Vanuatu', code: 'VU' }, - { name: 'Venezuela', code: 'VE' }, - { name: 'Viet Nam', code: 'VN' }, - { name: 'Virgin Islands, British', code: 'VG' }, - { name: 'Virgin Islands, U.S.', code: 'VI' }, - { name: 'Wallis and Futuna', code: 'WF' }, - { name: 'Western Sahara', code: 'EH' }, - { name: 'Yemen', code: 'YE' }, - { name: 'Zambia', code: 'ZM' }, - { name: 'Zimbabwe', code: 'ZW' }, -]; - -export default { - title: 'Components/ComboboxControl', - component: ComboboxControl, - argTypes: { - __nextHasNoMarginBottom: { control: { type: 'boolean' } }, - onChange: { action: 'onChange' }, - }, -}; - -const mapCountryOption = ( country ) => ( { - value: country.code, - label: country.name, -} ); - -const countryOptions = countries.map( mapCountryOption ); - -function Template( { onChange, ...args } ) { - const [ value, setValue ] = useState( null ); - - return ( - <> - { - setValue( ...changeArgs ); - onChange?.( ...changeArgs ); - } } - /> - - ); -} -export const Default = Template.bind( {} ); -Default.args = { - __next36pxDefaultSize: false, - allowReset: false, - label: 'Select a country', - options: countryOptions, -}; - -const authorOptions = [ - { - value: 'parsley', - label: 'Parsley Montana', - age: 48, - country: 'Germany', - }, - { - value: 'cabbage', - label: 'Cabbage New York', - age: 44, - country: 'France', - }, - { - value: 'jake', - label: 'Jake Weary', - age: 41, - country: 'United Kingdom', - }, -]; - -/** - * The rendered output of each suggestion can be customized by passing a - * render function to the `__experimentalRenderItem` prop. (This is still an experimental feature - * and is subject to change.) - */ -export const WithCustomRenderItem = Template.bind( {} ); -WithCustomRenderItem.args = { - ...Default.args, - label: 'Select an author', - options: authorOptions, - __experimentalRenderItem: ( { item } ) => { - const { label, age, country } = item; - return ( -
-
{ label }
- - Age: { age }, Country: { country } - -
- ); - }, -}; diff --git a/packages/components/src/combobox-control/stories/index.tsx b/packages/components/src/combobox-control/stories/index.tsx new file mode 100644 index 0000000000000..18f5e069f196c --- /dev/null +++ b/packages/components/src/combobox-control/stories/index.tsx @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import ComboboxControl from '..'; +import type { ComboboxControlProps } from '../types'; + +const countries = [ + { name: 'Afghanistan', code: 'AF' }, + { name: 'Åland Islands', code: 'AX' }, + { name: 'Albania', code: 'AL' }, + { name: 'Algeria', code: 'DZ' }, + { name: 'American Samoa', code: 'AS' }, +]; + +const meta: ComponentMeta< typeof ComboboxControl > = { + title: 'Components/ComboboxControl', + component: ComboboxControl, + argTypes: { + value: { control: { type: null } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + }, +}; +export default meta; + +const mapCountryOption = ( country: typeof countries[ number ] ) => ( { + value: country.code, + label: country.name, +} ); + +const countryOptions = countries.map( mapCountryOption ); + +const Template: ComponentStory< typeof ComboboxControl > = ( { + onChange, + ...args +} ) => { + const [ value, setValue ] = + useState< ComboboxControlProps[ 'value' ] >( null ); + + return ( + <> + { + setValue( ...changeArgs ); + onChange?.( ...changeArgs ); + } } + /> + + ); +}; +export const Default = Template.bind( {} ); +Default.args = { + __next36pxDefaultSize: false, + allowReset: false, + label: 'Select a country', + options: countryOptions, +}; + +const authorOptions = [ + { + value: 'parsley', + label: 'Parsley Montana', + age: 48, + country: 'Germany', + }, + { + value: 'cabbage', + label: 'Cabbage New York', + age: 44, + country: 'France', + }, + { + value: 'jake', + label: 'Jake Weary', + age: 41, + country: 'United Kingdom', + }, +]; + +/** + * The rendered output of each suggestion can be customized by passing a + * render function to the `__experimentalRenderItem` prop. (This is still an experimental feature + * and is subject to change.) + */ +export const WithCustomRenderItem = Template.bind( {} ); +WithCustomRenderItem.args = { + ...Default.args, + label: 'Select an author', + options: authorOptions.map( ( item ) => ( { + label: item.label, + value: item.value, + } ) ), + __experimentalRenderItem: ( { item } ) => { + const { label, age, country } = item; + return ( +
+
{ label }
+ + Age: { age }, Country: { country } + +
+ ); + }, +}; From ae5eb6ae9cfe7b013417ede2a6ea3ef98570528d Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 04:44:23 +0900 Subject: [PATCH 08/16] Remove from tsconfig --- packages/components/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index e88f685fdbf42..a163ff0dc0baf 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -43,7 +43,6 @@ "src/angle-picker-control", "src/autocomplete", "src/box-control", - "src/combobox-control", "src/custom-gradient-picker", "src/dimension-control", "src/duotone-picker", From 497e33f05fa3b1d57b7197f717d2d8cf9f023f3c Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 04:47:43 +0900 Subject: [PATCH 09/16] Add changelog --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 739fc1a160db6..3f9e3ef692ea9 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -23,6 +23,7 @@ ([#47384](https://github.com/WordPress/gutenberg/pull/47384)). - `Button`: Convert to TypeScript ([#46997](https://github.com/WordPress/gutenberg/pull/46997)). - `QueryControls`: Convert to TypeScript ([#46721](https://github.com/WordPress/gutenberg/pull/46721)). +- `ComboboxControl`: Convert to TypeScript ([#47581](https://github.com/WordPress/gutenberg/pull/47581)). - `Notice`: refactor to TypeScript ([47118](https://github.com/WordPress/gutenberg/pull/47118)). ### Bug Fix From 98b647189fffbc69d88a1ccbb080bbf43dc5bdb3 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 04:56:00 +0900 Subject: [PATCH 10/16] Add main JSDoc --- .../components/src/combobox-control/README.md | 3 -- .../components/src/combobox-control/index.tsx | 50 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index eb3c0a58ad78b..3e66f65836a94 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -17,9 +17,6 @@ These are the same as [the ones for `SelectControl`s](/packages/components/src/s ### Usage ```jsx -/** - * WordPress dependencies - */ import { ComboboxControl } from '@wordpress/components'; import { useState } from '@wordpress/element'; diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index ad240af8f1e9f..bf3dea7d9de97 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -58,6 +58,56 @@ const getIndexOfMatchingSuggestion = ( ? -1 : matchingSuggestions.indexOf( selectedSuggestion ); +/** + * `ComboboxControl` is an enhanced version of a [`SelectControl`](../select-control/README.md) with the addition of + * being able to search for options using a search input. + * + * ```jsx + * import { ComboboxControl } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const options = [ + * { + * value: 'small', + * label: 'Small', + * }, + * { + * value: 'normal', + * label: 'Normal', + * }, + * { + * value: 'large', + * label: 'Large', + * }, + * { + * value: 'huge', + * label: 'Huge', + * }, + * ]; + * + * function MyComboboxControl() { + * const [ fontSize, setFontSize ] = useState(); + * const [ filteredOptions, setFilteredOptions ] = useState( options ); + * return ( + * + * setFilteredOptions( + * options.filter( ( option ) => + * option.label + * .toLowerCase() + * .startsWith( inputValue.toLowerCase() ) + * ) + * ) + * } + * /> + * ); + * } + * ``` + */ function ComboboxControl( { __nextHasNoMarginBottom = false, __next36pxDefaultSize, From 8241d6dca0279f7197d07e7b81a227628c31ebbc Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 31 Jan 2023 05:08:11 +0900 Subject: [PATCH 11/16] Make code snippets more concise --- packages/components/src/combobox-control/README.md | 4 ---- packages/components/src/combobox-control/index.tsx | 4 ---- 2 files changed, 8 deletions(-) diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index 3e66f65836a94..f03f7a7cf4952 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -33,10 +33,6 @@ const options = [ value: 'large', label: 'Large', }, - { - value: 'huge', - label: 'Huge', - }, ]; function MyComboboxControl() { diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index bf3dea7d9de97..56047dbc31b1d 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -79,10 +79,6 @@ const getIndexOfMatchingSuggestion = ( * value: 'large', * label: 'Large', * }, - * { - * value: 'huge', - * label: 'Huge', - * }, * ]; * * function MyComboboxControl() { From 0365ccb3c92eeff8b5a0940194a96e6148b7e310 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 3 Feb 2023 23:55:51 +0900 Subject: [PATCH 12/16] Add code comment for empty string `value` --- packages/components/src/combobox-control/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index 56047dbc31b1d..72d7e4d19a07d 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -347,6 +347,9 @@ function ComboboxControl( { { isExpanded && ( suggestion.label From 565982137bdc723bea6f27a8b11d53a8a07c95b1 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 4 Feb 2023 02:00:46 +0900 Subject: [PATCH 13/16] Show code snippets --- packages/components/src/combobox-control/stories/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/combobox-control/stories/index.tsx b/packages/components/src/combobox-control/stories/index.tsx index 18f5e069f196c..2ba2d1d44b19f 100644 --- a/packages/components/src/combobox-control/stories/index.tsx +++ b/packages/components/src/combobox-control/stories/index.tsx @@ -31,6 +31,7 @@ const meta: ComponentMeta< typeof ComboboxControl > = { parameters: { actions: { argTypesRegex: '^on.*' }, controls: { expanded: true }, + docs: { source: { state: 'open' } }, }, }; export default meta; From ca5b3b2d41613fcaeb1e3a4f99a9c09425f57d97 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 4 Feb 2023 02:03:18 +0900 Subject: [PATCH 14/16] Fix bug in Storybook --- .../src/combobox-control/stories/index.tsx | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/packages/components/src/combobox-control/stories/index.tsx b/packages/components/src/combobox-control/stories/index.tsx index 2ba2d1d44b19f..b722d8245921d 100644 --- a/packages/components/src/combobox-control/stories/index.tsx +++ b/packages/components/src/combobox-control/stories/index.tsx @@ -71,27 +71,6 @@ Default.args = { options: countryOptions, }; -const authorOptions = [ - { - value: 'parsley', - label: 'Parsley Montana', - age: 48, - country: 'Germany', - }, - { - value: 'cabbage', - label: 'Cabbage New York', - age: 44, - country: 'France', - }, - { - value: 'jake', - label: 'Jake Weary', - age: 41, - country: 'United Kingdom', - }, -]; - /** * The rendered output of each suggestion can be customized by passing a * render function to the `__experimentalRenderItem` prop. (This is still an experimental feature @@ -101,10 +80,26 @@ export const WithCustomRenderItem = Template.bind( {} ); WithCustomRenderItem.args = { ...Default.args, label: 'Select an author', - options: authorOptions.map( ( item ) => ( { - label: item.label, - value: item.value, - } ) ), + options: [ + { + value: 'parsley', + label: 'Parsley Montana', + age: 48, + country: 'Germany', + }, + { + value: 'cabbage', + label: 'Cabbage New York', + age: 44, + country: 'France', + }, + { + value: 'jake', + label: 'Jake Weary', + age: 41, + country: 'United Kingdom', + }, + ], __experimentalRenderItem: ( { item } ) => { const { label, age, country } = item; return ( From 550db09f2b805afad4cbec0cdec7e36c3db62f55 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 4 Feb 2023 02:06:43 +0900 Subject: [PATCH 15/16] Add explicit default value --- packages/components/src/combobox-control/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index 72d7e4d19a07d..07bc89a5df482 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -106,7 +106,7 @@ const getIndexOfMatchingSuggestion = ( */ function ComboboxControl( { __nextHasNoMarginBottom = false, - __next36pxDefaultSize, + __next36pxDefaultSize = false, value: valueProp, label, options, From 9d39fbcc51a85961b33f3e5aebec891af53d276a Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 7 Feb 2023 04:35:12 +0900 Subject: [PATCH 16/16] Fixup --- packages/components/CHANGELOG.md | 5 ++++- packages/components/src/combobox-control/README.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8c8bc22b1a055..875a61745446c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- `ComboboxControl`: Convert to TypeScript ([#47581](https://github.com/WordPress/gutenberg/pull/47581)). + ## 23.3.0 (2023-02-01) ### Deprecations @@ -27,7 +31,6 @@ - `Button`: Convert to TypeScript ([#46997](https://github.com/WordPress/gutenberg/pull/46997)). - `BoxControl`: Convert to TypeScript ([#47622](https://github.com/WordPress/gutenberg/pull/47622)). - `QueryControls`: Convert to TypeScript ([#46721](https://github.com/WordPress/gutenberg/pull/46721)). -- `ComboboxControl`: Convert to TypeScript ([#47581](https://github.com/WordPress/gutenberg/pull/47581)). - `TreeGrid`: Convert to TypeScript ([#47516](https://github.com/WordPress/gutenberg/pull/47516)). - `Notice`: refactor to TypeScript ([47118](https://github.com/WordPress/gutenberg/pull/47118)). diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index f03f7a7cf4952..86b760e694a9a 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -99,7 +99,7 @@ Function called when the control's search input value changes. The argument cont Function called with the selected value changes. -- Type: `( value: string | null ) => void` +- Type: `( value: string | null | undefined ) => void` - Required: No #### value