diff --git a/UNRELEASED.md b/UNRELEASED.md index facef084526..a11c7d8a097 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -40,7 +40,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Code quality -- Rebuilt `Autocomplete` internals using new `ComboBox` and `ListBox` components built on the ARIA 1.2 spec for improved accessibility ([#3910](https://github.com/Shopify/polaris-react/pull/3910)) +- Rebuilt `Autocomplete` internals using new `Combobox` and `Listbox` components built on the ARIA 1.2 spec for improved accessibility ([#3910](https://github.com/Shopify/polaris-react/pull/3910)) - Modernized tests for Avatar, Backdrop, Badge, Banner components([#4306](https://github.com/Shopify/polaris-react/pull/4306)) - Modernized test for Card: Subsection, Header, Sections and Card ([#4325](https://github.com/Shopify/polaris-react/pull/4325)). - Modernized tests for Item, Panel, List, Tab, TabMeasurer (from Tabs components). ([#4313](https://github.com/Shopify/polaris-react/pull/4313)) diff --git a/src/components/Autocomplete/Autocomplete.tsx b/src/components/Autocomplete/Autocomplete.tsx index b74ea61c59f..7aa72583589 100644 --- a/src/components/Autocomplete/Autocomplete.tsx +++ b/src/components/Autocomplete/Autocomplete.tsx @@ -8,8 +8,8 @@ import type { import type {PopoverProps} from '../Popover'; import {isSection} from '../../utilities/options'; import {useI18n} from '../../utilities/i18n'; -import {ComboBox} from '../ComboBox'; -import {ListBox} from '../ListBox'; +import {Combobox} from '../Combobox'; +import {Listbox} from '../Listbox'; import {MappedAction, MappedOption} from './components'; import styles from './Autocomplete.scss'; @@ -49,7 +49,7 @@ export interface AutocompleteProps { // generated *.d.ts files. export const Autocomplete: React.FunctionComponent & { - TextField: typeof ComboBox.TextField; + TextField: typeof Combobox.TextField; } = function Autocomplete({ options, selected, @@ -100,13 +100,13 @@ export const Autocomplete: React.FunctionComponent & { const optionMarkup = buildMappedOptionFromOption(options); return ( - {title}} + title={{title}} key={title} > {optionMarkup} - + ); }); @@ -120,12 +120,12 @@ export const Autocomplete: React.FunctionComponent & { if (listTitle) { return ( - {listTitle}} + title={{listTitle}} > {optionList} - + ); } @@ -139,7 +139,7 @@ export const Autocomplete: React.FunctionComponent & { ]); const loadingMarkup = loading ? ( - & { ); return ( - {actionMarkup || optionsMarkup || loadingMarkup || emptyStateMarkup ? ( - + {actionMarkup} {optionsMarkup && (!loading || willLoadMoreResults) ? optionsMarkup : null} {loadingMarkup} {emptyStateMarkup} - + ) : null} - + ); }; -Autocomplete.TextField = ComboBox.TextField; +Autocomplete.TextField = Combobox.TextField; diff --git a/src/components/Autocomplete/README.md b/src/components/Autocomplete/README.md index b7546132ff2..fc692cf1d79 100644 --- a/src/components/Autocomplete/README.md +++ b/src/components/Autocomplete/README.md @@ -11,7 +11,7 @@ keywords: # Autocomplete -The autocomplete component is an input field that provides selectable suggestions as a merchant types into it. It allows merchants to quickly search through and select from large collections of options. It's a convenience wrapper around the `ComboBox` and `ListBox` components with minor UI differences. +The autocomplete component is an input field that provides selectable suggestions as a merchant types into it. It allows merchants to quickly search through and select from large collections of options. It's a convenience wrapper around the `Combobox` and `Listbox` components with minor UI differences. --- @@ -814,7 +814,7 @@ See Apple’s Human Interface Guidelines and API documentation about accessibili ### Structure -The autocomplete component is based on the [ARIA 1.2 combobox pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox) and the [Aria 1.2 ListBox pattern](https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox). +The autocomplete component is based on the [ARIA 1.2 combobox pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox) and the [Aria 1.2 Listbox pattern](https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox). The autocomplete list displays below the text field or other control by default so it is easy for merchants to discover and use. However, you can change the position with the `preferredPosition` prop. diff --git a/src/components/Autocomplete/components/MappedAction/MappedAction.tsx b/src/components/Autocomplete/components/MappedAction/MappedAction.tsx index 86d42218f68..104a306cbd4 100644 --- a/src/components/Autocomplete/components/MappedAction/MappedAction.tsx +++ b/src/components/Autocomplete/components/MappedAction/MappedAction.tsx @@ -4,7 +4,7 @@ import type {ActionListItemDescriptor} from '../../../../types'; import {Badge} from '../../../Badge'; import {classNames} from '../../../../utilities/css'; import {MappedActionContext} from '../../../../utilities/autocomplete'; -import {ListBox} from '../../../ListBox'; +import {Listbox} from '../../../Listbox'; import {Icon} from '../../../Icon'; import {TextStyle} from '../../../TextStyle'; import {useI18n} from '../../../../utilities/i18n'; @@ -102,7 +102,7 @@ export function MappedAction({ return (
-
- +
); diff --git a/src/components/Autocomplete/components/MappedAction/tests/MappedAction.test.tsx b/src/components/Autocomplete/components/MappedAction/tests/MappedAction.test.tsx index cbd2256e83e..2806a95a190 100644 --- a/src/components/Autocomplete/components/MappedAction/tests/MappedAction.test.tsx +++ b/src/components/Autocomplete/components/MappedAction/tests/MappedAction.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {mountWithListBoxProvider} from 'test-utilities/list-box'; +import {mountWithListboxProvider} from 'test-utilities/listbox'; -import {ListBox} from '../../../../ListBox'; +import {Listbox} from '../../../../Listbox'; import {MappedAction} from '../MappedAction'; import {MappedActionContext} from '../../../../../utilities/autocomplete'; import {Badge} from '../../../../Badge'; @@ -13,7 +13,7 @@ describe('MappedAction', () => { status: 'new' as const, content: 'new', }; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); @@ -24,7 +24,7 @@ describe('MappedAction', () => { }); it('renders suffix when provided', () => { - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( } />, ); @@ -33,7 +33,7 @@ describe('MappedAction', () => { it('renders helpText when provided', () => { const helpText = 'help text'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); @@ -41,7 +41,7 @@ describe('MappedAction', () => { }); it('renders ellipsis when true', () => { - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); @@ -56,7 +56,7 @@ describe('MappedAction', () => { onAction: () => {}, destructive: false, }; - const mappedAction = mountWithListBoxProvider(); + const mappedAction = mountWithListboxProvider(); expect(mappedAction).toContainReactComponent(MappedActionContext.Provider, { value: { @@ -66,48 +66,48 @@ describe('MappedAction', () => { }); }); - describe('ListBox.Action', () => { + describe('Listbox.Action', () => { it('renders', () => { - const mappedAction = mountWithListBoxProvider(); + const mappedAction = mountWithListboxProvider(); - expect(mappedAction).toContainReactComponent(ListBox.Action); + expect(mappedAction).toContainReactComponent(Listbox.Action); }); it('passes active', () => { - const mappedAction = mountWithListBoxProvider(); + const mappedAction = mountWithListboxProvider(); - expect(mappedAction).toContainReactComponent(ListBox.Action, { + expect(mappedAction).toContainReactComponent(Listbox.Action, { selected: true, }); }); it('passes disabled', () => { const disabled = true; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); - expect(mappedAction).toContainReactComponent(ListBox.Action, { + expect(mappedAction).toContainReactComponent(Listbox.Action, { disabled, }); }); it('passes value', () => { const value = 'value'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); - expect(mappedAction).toContainReactComponent(ListBox.Action, { + expect(mappedAction).toContainReactComponent(Listbox.Action, { value, }); }); it('defaults value to an empty string', () => { const value = ''; - const mappedAction = mountWithListBoxProvider(); + const mappedAction = mountWithListboxProvider(); - expect(mappedAction).toContainReactComponent(ListBox.Action, { + expect(mappedAction).toContainReactComponent(Listbox.Action, { value, }); }); @@ -116,7 +116,7 @@ describe('MappedAction', () => { describe('prefix markup', () => { it('renders images', () => { const image = 'image'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); @@ -127,7 +127,7 @@ describe('MappedAction', () => { it('renders icon', () => { const source = 'icon'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); @@ -135,7 +135,7 @@ describe('MappedAction', () => { }); it('renders prefix', () => { - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( } />, ); @@ -145,7 +145,7 @@ describe('MappedAction', () => { it('renders icon instead of image', () => { const source = 'icon'; const image = 'image'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( , ); @@ -157,7 +157,7 @@ describe('MappedAction', () => { it('renders prefix instead of image', () => { const image = 'image'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( } image={image} />, ); @@ -169,7 +169,7 @@ describe('MappedAction', () => { it('renders prefix instead of icon', () => { const source = 'icon'; - const mappedAction = mountWithListBoxProvider( + const mappedAction = mountWithListboxProvider( } icon={source} />, ); diff --git a/src/components/Autocomplete/components/MappedOption/MappedOption.tsx b/src/components/Autocomplete/components/MappedOption/MappedOption.tsx index 09dee16cc73..be95bdd3b47 100644 --- a/src/components/Autocomplete/components/MappedOption/MappedOption.tsx +++ b/src/components/Autocomplete/components/MappedOption/MappedOption.tsx @@ -1,7 +1,7 @@ import React, {memo} from 'react'; import type {OptionDescriptor, ArrayElement} from '../../../../types'; -import {ListBox} from '../../../ListBox'; +import {Listbox} from '../../../Listbox'; import {classNames} from '../../../../utilities/css'; import styles from './MappedOption.scss'; @@ -32,19 +32,19 @@ export const MappedOption = memo(function MappedOption({ const accessibilityLabel = typeof label === 'string' ? label : undefined; return ( - - +
{mediaMarkup} {label}
-
-
+ + ); }); diff --git a/src/components/Autocomplete/components/MappedOption/tests/MappedOption.test.tsx b/src/components/Autocomplete/components/MappedOption/tests/MappedOption.test.tsx index 1d5ec593c8d..b32780fb040 100644 --- a/src/components/Autocomplete/components/MappedOption/tests/MappedOption.test.tsx +++ b/src/components/Autocomplete/components/MappedOption/tests/MappedOption.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {mountWithListBoxProvider} from 'test-utilities/list-box'; +import {mountWithListboxProvider} from 'test-utilities/listbox'; -import {ListBox} from '../../../../ListBox'; +import {Listbox} from '../../../../Listbox'; import {MappedOption} from '../MappedOption'; describe('MappedOption', () => { @@ -14,7 +14,7 @@ describe('MappedOption', () => { it('renders label markup', () => { const label = 'Test label'; - const mappedOption = mountWithListBoxProvider( + const mappedOption = mountWithListboxProvider( , ); @@ -24,37 +24,37 @@ describe('MappedOption', () => { describe('accessibility', () => { it('does not apply an accessibility label when label is not a string', () => { const label =
test label
; - const mappedOption = mountWithListBoxProvider( + const mappedOption = mountWithListboxProvider( , ); - expect(mappedOption).toContainReactComponent(ListBox.Option, { + expect(mappedOption).toContainReactComponent(Listbox.Option, { accessibilityLabel: undefined, }); }); }); - describe('ListBox', () => { - it('renders ListBox.Option', () => { - const mappedOption = mountWithListBoxProvider( + describe('Listbox', () => { + it('renders Listbox.Option', () => { + const mappedOption = mountWithListboxProvider( , ); - expect(mappedOption).toContainReactComponent(ListBox.Option); + expect(mappedOption).toContainReactComponent(Listbox.Option); }); - it('renders ListBox.TextOption', () => { - const mappedOption = mountWithListBoxProvider( + it('renders Listbox.TextOption', () => { + const mappedOption = mountWithListboxProvider( , ); - expect(mappedOption).toContainReactComponent(ListBox.TextOption); + expect(mappedOption).toContainReactComponent(Listbox.TextOption); }); }); describe('media', () => { it('renders markup when provided', () => { - const mappedOption = mountWithListBoxProvider( + const mappedOption = mountWithListboxProvider( } />, ); @@ -62,7 +62,7 @@ describe('MappedOption', () => { }); it('renders with disabled styles when disabled', () => { - const mappedOption = mountWithListBoxProvider( + const mappedOption = mountWithListboxProvider( } />, ); @@ -72,7 +72,7 @@ describe('MappedOption', () => { }); it('renders with single selection styles when singleSelection is true', () => { - const mappedOption = mountWithListBoxProvider( + const mappedOption = mountWithListboxProvider( ', () => { const options = [ @@ -21,7 +21,7 @@ describe('', () => { options, selected: [], textField: ( - + ), onSelect: noop, }; @@ -35,7 +35,7 @@ describe('', () => { onSelect={noop} />, ); - expect(autocomplete).toContainReactComponent(ComboBox); + expect(autocomplete).toContainReactComponent(Combobox); }); it('displays a spinner when loading is true', () => { @@ -49,9 +49,9 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); - expect(autocomplete).toContainReactComponent(ListBox.Loading); + expect(autocomplete).toContainReactComponent(Listbox.Loading); }); describe('', () => { @@ -69,7 +69,7 @@ describe('', () => { }); describe('options', () => { - it('renders a ListBox.Option for each option', () => { + it('renders a Listbox.Option for each option', () => { const options = [ {value: 'cheese_pizza', label: 'Cheese Pizza'}, {value: 'macaroni_pizza', label: 'Macaroni Pizza'}, @@ -80,15 +80,15 @@ describe('', () => { , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponentTimes( - ListBox.Option, + Listbox.Option, options.length, ); }); - it('passes selected to ListBox.Option', () => { + it('passes selected to Listbox.Option', () => { const selected = 'cheese_pizza'; const options = [ {value: selected, label: 'Cheese Pizza'}, @@ -104,7 +104,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponent(MappedOption, { ...options[0], @@ -134,7 +134,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponent(MappedOption, { ...selectedOption, @@ -144,9 +144,9 @@ describe('', () => { }); describe('textField', () => { - it('is passed to ComboBox', () => { + it('is passed to Combobox', () => { const textField = ( - ', () => { , ); - expect(autocomplete).toContainReactComponent(ComboBox, { + expect(autocomplete).toContainReactComponent(Combobox, { activator: textField, }); }); }); describe('preferredPosition', () => { - it('is passed to ComboBox', () => { + it('is passed to Combobox', () => { const preferredPosition = 'above'; const autocomplete = mountWithApp( ', () => { />, ); - expect(autocomplete).toContainReactComponent(ComboBox, { + expect(autocomplete).toContainReactComponent(Combobox, { preferredPosition, }); }); }); describe('listTitle', () => { - it('renders a ListBoxSection with a ListBoxHeader', () => { + it('renders a ListboxSection with a ListboxHeader', () => { const listTitle = 'title'; const autocomplete = mountWithApp( , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); - expect(autocomplete).toContainReactComponent(ListBox.Section, { + expect(autocomplete).toContainReactComponent(Listbox.Section, { divider: false, }); }); }); describe('allowMultiple', () => { - it('is passed to ComboBox', () => { + it('is passed to Combobox', () => { const allowMultiple = true; const autocomplete = mountWithApp( , ); - expect(autocomplete).toContainReactComponent(ComboBox, { + expect(autocomplete).toContainReactComponent(Combobox, { allowMultiple, }); }); @@ -229,21 +229,21 @@ describe('', () => { , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponent(MappedAction); }); }); describe('loading', () => { - it('renders ListBox.Loading', () => { + it('renders Listbox.Loading', () => { const autocomplete = mountWithApp( , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); - expect(autocomplete).toContainReactComponent(ListBox.Loading); + expect(autocomplete).toContainReactComponent(Listbox.Loading); }); }); @@ -264,10 +264,10 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponentTimes( - ListBox.Option, + Listbox.Option, options.length, ); }); @@ -286,7 +286,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).not.toContainReactComponent(EmptyState); }); @@ -302,7 +302,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).not.toContainReactComponent(EmptyState); }); @@ -313,7 +313,7 @@ describe('', () => { , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).not.toContainReactComponent(EmptyState); }); @@ -329,7 +329,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponent(EmptyState); }); @@ -351,7 +351,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); triggerOnSelect(autocomplete, value); expect(onSelectSpy).toHaveBeenLastCalledWith([value]); @@ -374,7 +374,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); triggerOnSelect(autocomplete, value); expect(onSelectSpy).toHaveBeenLastCalledWith([]); @@ -398,7 +398,7 @@ describe('', () => { />, ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); triggerOnSelect(autocomplete, valueTwo); expect(onSelectSpy).toHaveBeenLastCalledWith([valueOne, valueTwo]); @@ -406,7 +406,7 @@ describe('', () => { }); describe('onLoadMoreResults', () => { - it('is passed to ComboBox', () => { + it('is passed to Combobox', () => { const onLoadMoreResults = jest.fn(); const autocomplete = mountWithApp( ', () => { />, ); - expect(autocomplete).toContainReactComponent(ComboBox, { + expect(autocomplete).toContainReactComponent(Combobox, { onScrolledToBottom: onLoadMoreResults, }); }); @@ -437,10 +437,10 @@ describe('', () => { , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); autocomplete - .find(ComboBox.TextField) + .find(Combobox.TextField) ?.find(TextField) ?.trigger('onFocus'); autocomplete @@ -466,7 +466,7 @@ describe('', () => { />, ); - expect(autocomplete).not.toContainReactComponent(ListBox.Option); + expect(autocomplete).not.toContainReactComponent(Listbox.Option); }); }); @@ -483,7 +483,7 @@ describe('', () => { />, ); - autocomplete.find(ComboBox)?.trigger('onScrolledToBottom'); + autocomplete.find(Combobox)?.trigger('onScrolledToBottom'); expect(spy).toHaveBeenCalledTimes(1); }); @@ -508,7 +508,7 @@ describe('', () => { }, ]; - it('renders one ListBox.Option for each option provided on all sections', () => { + it('renders one Listbox.Option for each option provided on all sections', () => { const allOptionsLength = multipleSectionsOptions.reduce( (lengthAccumulated, {options}) => { return lengthAccumulated + options.length; @@ -520,22 +520,22 @@ describe('', () => { , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponentTimes( - ListBox.Option, + Listbox.Option, allOptionsLength, ); }); - it('renders one ListBox.Section for each section', () => { + it('renders one Listbox.Section for each section', () => { const autocomplete = mountWithApp( , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponentTimes( - ListBox.Section, + Listbox.Section, multipleSectionsOptions.length, ); }); @@ -552,10 +552,10 @@ describe('', () => { , ); - triggerFocus(autocomplete.find(ComboBox)); + triggerFocus(autocomplete.find(Combobox)); expect(autocomplete).toContainReactComponentTimes( - ListBox.Section, + Listbox.Section, newOptions.length - 1, ); }); @@ -570,10 +570,10 @@ describe('', () => { } }); -function triggerFocus(combobox: ReactTestingElement | null) { +function triggerFocus(combobox: ReactTestingElement | null) { combobox && combobox - .find(ComboBoxTextFieldContext.Provider)! + .find(ComboboxTextFieldContext.Provider)! .triggerKeypath('value.onTextFieldFocus'); } @@ -581,6 +581,6 @@ function triggerOnSelect( autocomplete: CustomRoot | null, values: string, ) { - const listbox = autocomplete!.find(ListBox); + const listbox = autocomplete!.find(Listbox); listbox!.trigger('onSelect', values); } diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index dd5f25296eb..d4a410201bd 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -14,7 +14,7 @@ import {Choice, helpTextID} from '../Choice'; import {errorTextID} from '../InlineError'; import {Icon} from '../Icon'; import {Error, Key, CheckboxHandles} from '../../types'; -import {WithinListBoxContext} from '../../utilities/list-box/context'; +import {WithinListboxContext} from '../../utilities/listbox/context'; import styles from './Checkbox.scss'; @@ -74,7 +74,7 @@ export const Checkbox = forwardRef( setFalse: handleMouseOut, } = useToggle(false); const [keyFocused, setKeyFocused] = useState(false); - const isWithinListBox = useContext(WithinListBoxContext); + const isWithinListbox = useContext(WithinListboxContext); useImperativeHandle(ref, () => ({ focus: () => { @@ -170,7 +170,7 @@ export const Checkbox = forwardRef( onChange={noop} aria-invalid={error != null} aria-describedby={ariaDescribedBy} - role={isWithinListBox ? 'presentation' : 'checkbox'} + role={isWithinListbox ? 'presentation' : 'checkbox'} {...indeterminateAttributes} /> diff --git a/src/components/ComboBox/index.ts b/src/components/ComboBox/index.ts deleted file mode 100644 index f50eeedf616..00000000000 --- a/src/components/ComboBox/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ComboBox'; -export {TextField as ComboBoxTextField} from './components'; diff --git a/src/components/ComboBox/ComboBox.scss b/src/components/Combobox/Combobox.scss similarity index 88% rename from src/components/ComboBox/ComboBox.scss rename to src/components/Combobox/Combobox.scss index 977141bcbff..e71ffa204b7 100644 --- a/src/components/ComboBox/ComboBox.scss +++ b/src/components/Combobox/Combobox.scss @@ -1,6 +1,6 @@ @import '../../styles/common'; -.ListBox { +.Listbox { padding: spacing(tight) 0; overflow: visible; } diff --git a/src/components/ComboBox/ComboBox.tsx b/src/components/Combobox/Combobox.tsx similarity index 69% rename from src/components/ComboBox/ComboBox.tsx rename to src/components/Combobox/Combobox.tsx index 1941e903b91..325a9e07c6e 100644 --- a/src/components/ComboBox/ComboBox.tsx +++ b/src/components/Combobox/Combobox.tsx @@ -3,38 +3,38 @@ import React, {useState, useCallback, useMemo, Children} from 'react'; import {Popover} from '../Popover'; import type {PopoverProps} from '../Popover'; import type {TextFieldProps} from '../TextField'; -import type {ListBoxProps} from '../ListBox'; +import type {ListboxProps} from '../Listbox'; import { - ComboBoxTextFieldContext, - ComboBoxTextFieldType, - ComboBoxListBoxContext, - ComboBoxListBoxType, - ComboBoxListBoxOptionType, - ComboBoxListBoxOptionContext, -} from '../../utilities/combo-box'; + ComboboxTextFieldContext, + ComboboxTextFieldType, + ComboboxListboxContext, + ComboboxListboxType, + ComboboxListboxOptionType, + ComboboxListboxOptionContext, +} from '../../utilities/combobox'; -import styles from './ComboBox.scss'; +import styles from './Combobox.scss'; import {TextField} from './components'; -export interface ComboBoxProps { - children?: React.ReactElement | null; +export interface ComboboxProps { + children?: React.ReactElement | null; activator: React.ReactElement; allowMultiple?: boolean; onScrolledToBottom?(): void; preferredPosition?: PopoverProps['preferredPosition']; } -export function ComboBox({ +export function Combobox({ children, activator, allowMultiple, onScrolledToBottom, preferredPosition = 'below', -}: ComboBoxProps) { +}: ComboboxProps) { const [popoverActive, setPopoverActive] = useState(false); const [activeOptionId, setActiveOptionId] = useState(); const [textFieldLabelId, setTextFieldLabelId] = useState(); - const [listBoxId, setListBoxId] = useState(); + const [listboxId, setListboxId] = useState(); const [textFieldFocused, setTextFieldFocused] = useState(false); const shouldOpen = Boolean(!popoverActive && Children.count(children) > 0); @@ -69,11 +69,11 @@ export function ComboBox({ } }, [popoverActive]); - const textFieldContextValue: ComboBoxTextFieldType = useMemo( + const textFieldContextValue: ComboboxTextFieldType = useMemo( () => ({ activeOptionId, expanded: popoverActive, - listBoxId, + listboxId, setTextFieldFocused, setTextFieldLabelId, onTextFieldFocus: handleFocus, @@ -83,7 +83,7 @@ export function ComboBox({ [ activeOptionId, popoverActive, - listBoxId, + listboxId, setTextFieldFocused, setTextFieldLabelId, handleFocus, @@ -92,18 +92,18 @@ export function ComboBox({ ], ); - const listBoxOptionContextValue: ComboBoxListBoxOptionType = useMemo( + const listboxOptionContextValue: ComboboxListboxOptionType = useMemo( () => ({ allowMultiple, }), [allowMultiple], ); - const listBoxContextValue: ComboBoxListBoxType = useMemo( + const listboxContextValue: ComboboxListboxType = useMemo( () => ({ setActiveOptionId, - setListBoxId, - listBoxId, + setListboxId, + listboxId, textFieldLabelId, onOptionSelected, textFieldFocused, @@ -111,8 +111,8 @@ export function ComboBox({ }), [ setActiveOptionId, - setListBoxId, - listBoxId, + setListboxId, + listboxId, textFieldLabelId, onOptionSelected, textFieldFocused, @@ -125,9 +125,9 @@ export function ComboBox({ active={popoverActive} onClose={handleClose} activator={ - + {activator} - + } autofocusTarget="none" preventFocusOnClose @@ -137,17 +137,17 @@ export function ComboBox({ > {Children.count(children) > 0 ? ( - - + -
{children}
-
-
+
{children}
+ + ) : null}
); } -ComboBox.TextField = TextField; +Combobox.TextField = TextField; diff --git a/src/components/ComboBox/README.md b/src/components/Combobox/README.md similarity index 88% rename from src/components/ComboBox/README.md rename to src/components/Combobox/README.md index b6e8c0cfd4e..8c032976fbc 100644 --- a/src/components/ComboBox/README.md +++ b/src/components/Combobox/README.md @@ -1,5 +1,5 @@ --- -name: ComboBox +name: Combobox category: Forms keywords: - autocomplete @@ -9,15 +9,15 @@ keywords: - listbox --- -# ComboBox +# Combobox -The `ComboBox` component implements part of the [Aria 1.2 combobox](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox) specs on a TextField and a popover containing a ListBox. Like `Autocomplete`, `ComboBox` allows merchants to quickly search through and select from large collections of options. +The `Combobox` component implements part of the [Aria 1.2 combobox](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox) specs on a TextField and a popover containing a Listbox. Like `Autocomplete`, `Combobox` allows merchants to quickly search through and select from large collections of options. --- ## Best practices -The `ComboBox` component should: +The `Combobox` component should: - Be clearly labeled so it’s noticeable to the merchant what type of options will be available - Not be used within a popover @@ -27,7 +27,7 @@ The `ComboBox` component should: ## Content guidelines -The input field for `ComboBox` should follow the [content guidelines](https://polaris.shopify.com/components/forms/text-field) for text fields. +The input field for `Combobox` should follow the [content guidelines](https://polaris.shopify.com/components/forms/text-field) for text fields. --- @@ -90,23 +90,23 @@ function ComboboxExample() { const {label, value} = option; return ( - {label} - + ); }) : null; return (
- } onChange={updateText} label="Search customers" @@ -117,9 +117,9 @@ function ComboboxExample() { } > {options.length > 0 ? ( - {optionsMarkup} + {optionsMarkup} ) : null} - +
); } @@ -208,24 +208,24 @@ function MultiComboboxExample() { const {label, value} = option; return ( - {label} - + ); }) : null; return (
- } onChange={updateText} label="Search customers" @@ -236,9 +236,9 @@ function MultiComboboxExample() { } > {optionsMarkup ? ( - {optionsMarkup} + {optionsMarkup} ) : null} - + {tagsMarkup} @@ -320,32 +320,32 @@ function LoadingAutocompleteExample() { const {label, value} = option; return ( - {label} - + ); }) : null; - const loadingMarkup = loading ? : null; + const loadingMarkup = loading ? : null; - const listBoxMarkup = + const listboxMarkup = optionsMarkup || loadingMarkup ? ( - + {optionsMarkup && !loading ? optionsMarkup : null} {loadingMarkup} - + ) : null; return ( - } onChange={updateText} label="Search customers" @@ -355,8 +355,8 @@ function LoadingAutocompleteExample() { /> } > - {listBoxMarkup} - + {listboxMarkup} + ); } ``` @@ -366,8 +366,8 @@ function LoadingAutocompleteExample() { ## Related components - For an input field without suggested options, [use the text field component](https://polaris.shopify.com/components/forms/text-field) -- For a list of selectable options not linked to an input field, [use the list box component](https://polaris.shopify.com/components/lists-and-tables/list-box) -- [Autocomplete](https://polaris.shopify.com/components/forms/autocomplete) can be used as a convenience wrapper in lieu of `ComboBox` and `ListBox`. +- For a list of selectable options not linked to an input field, [use the list box component](https://polaris.shopify.com/components/lists-and-tables/listbox) +- [Autocomplete](https://polaris.shopify.com/components/forms/autocomplete) can be used as a convenience wrapper in lieu of `Combobox` and `Listbox`. --- @@ -395,11 +395,11 @@ See Apple’s Human Interface Guidelines and API documentation about accessibili ### Structure -The `ComboBox` component is based on the [ARIA 1.2 combobox pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox). It is a combination of a single-line `TextField` and a `Popover`. The current implementation expects a [`ListBox`] component to be used. +The `Combobox` component is based on the [ARIA 1.2 combobox pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox). It is a combination of a single-line `TextField` and a `Popover`. The current implementation expects a [`Listbox`] component to be used. -The `ComboBox` popover displays below the text field or other control by default so it is easy for merchants to discover and use. However, you can change the position with the `preferredPosition` prop. +The `Combobox` popover displays below the text field or other control by default so it is easy for merchants to discover and use. However, you can change the position with the `preferredPosition` prop. -`ComboBox` features can be challenging for merchants with visual, motor, and cognitive disabilities. Even when they’re built using best practices, these features can be difficult to use with some assistive technologies. Merchants should always be able to search, enter data, or perform other activities without relying on the combobox. +`Combobox` features can be challenging for merchants with visual, motor, and cognitive disabilities. Even when they’re built using best practices, these features can be difficult to use with some assistive technologies. Merchants should always be able to search, enter data, or perform other activities without relying on the combobox. diff --git a/src/components/ComboBox/components/TextField/TextField.tsx b/src/components/Combobox/components/TextField/TextField.tsx similarity index 89% rename from src/components/ComboBox/components/TextField/TextField.tsx rename to src/components/Combobox/components/TextField/TextField.tsx index 14c9831b492..ac5c88f2087 100644 --- a/src/components/ComboBox/components/TextField/TextField.tsx +++ b/src/components/Combobox/components/TextField/TextField.tsx @@ -4,7 +4,7 @@ import {labelID} from '../../../Label'; import {useUniqueId} from '../../../../utilities/unique-id'; import {TextField as PolarisTextField} from '../../../TextField'; import type {TextFieldProps} from '../../../TextField'; -import {useComboBoxTextField} from '../../../../utilities/combo-box'; +import {useComboboxTextField} from '../../../../utilities/combobox'; export function TextField({ value, @@ -14,11 +14,11 @@ export function TextField({ onChange, ...rest }: TextFieldProps) { - const comboboxTextFieldContext = useComboBoxTextField(); + const comboboxTextFieldContext = useComboboxTextField(); const { activeOptionId, - listBoxId, + listboxId, expanded, setTextFieldFocused, setTextFieldLabelId, @@ -27,7 +27,7 @@ export function TextField({ onTextFieldBlur, } = comboboxTextFieldContext; - const uniqueId = useUniqueId('ComboBoxTextField'); + const uniqueId = useUniqueId('ComboboxTextField'); const textFieldId = useMemo(() => idProp || uniqueId, [uniqueId, idProp]); const labelId = useMemo(() => labelID(idProp || uniqueId), [ @@ -70,7 +70,7 @@ export function TextField({ ariaAutocomplete="list" aria-haspopup="listbox" ariaActiveDescendant={activeOptionId} - ariaControls={listBoxId} + ariaControls={listboxId} role="combobox" ariaExpanded={expanded} /> diff --git a/src/components/ComboBox/components/TextField/index.ts b/src/components/Combobox/components/TextField/index.ts similarity index 100% rename from src/components/ComboBox/components/TextField/index.ts rename to src/components/Combobox/components/TextField/index.ts diff --git a/src/components/ComboBox/components/TextField/tests/TextField.test.tsx b/src/components/Combobox/components/TextField/tests/TextField.test.tsx similarity index 91% rename from src/components/ComboBox/components/TextField/tests/TextField.test.tsx rename to src/components/Combobox/components/TextField/tests/TextField.test.tsx index 3f3ca4b9fc9..c3e40bc0c75 100644 --- a/src/components/ComboBox/components/TextField/tests/TextField.test.tsx +++ b/src/components/Combobox/components/TextField/tests/TextField.test.tsx @@ -6,13 +6,13 @@ import type {TextFieldProps} from '../../../../TextField'; import {TextField} from '../TextField'; import {labelID} from '../../../../Label'; import { - ComboBoxTextFieldContext, - ComboBoxTextFieldType, -} from '../../../../../utilities/combo-box'; + ComboboxTextFieldContext, + ComboboxTextFieldType, +} from '../../../../../utilities/combobox'; const textFieldContextDefaultValue = { activeOptionId: undefined, - listBoxId: undefined, + listboxId: undefined, expanded: false, setTextFieldLabelId: noop, setTextFieldFocused: noop, @@ -24,7 +24,7 @@ const textFieldContextDefaultValue = { function mountWithProvider( props: { textFieldProps?: Partial; - textFieldProviderValue?: Partial; + textFieldProviderValue?: Partial; } = {}, ) { const providerValue = { @@ -33,21 +33,21 @@ function mountWithProvider( }; const textField = mountWithApp( - + - , + , ); return textField; } -describe('ComboBox.TextField', () => { - it('throws if not wrapped in ComboBoxTextFieldContext', () => { +describe('Combobox.TextField', () => { + it('throws if not wrapped in ComboboxTextFieldContext', () => { const consoleErrorSpy = jest .spyOn(console, 'error') .mockImplementation(() => {}); @@ -56,7 +56,7 @@ describe('ComboBox.TextField', () => { mountWithApp( , ), - ).toThrow('No ComboBox was provided.'); + ).toThrow('No Combobox was provided.'); consoleErrorSpy.mockRestore(); }); @@ -97,16 +97,16 @@ describe('ComboBox.TextField', () => { }); }); - it('passes the listBoxId to the aria-controls of the PolarisTextField', () => { - const listBoxId = 'listBoxId'; + it('passes the listboxId to the aria-controls of the PolarisTextField', () => { + const listboxId = 'listboxId'; const combobox = mountWithProvider({ textFieldProviderValue: { - listBoxId, + listboxId, }, }); expect(combobox).toContainReactComponent(PolarisTextField, { - ariaControls: listBoxId, + ariaControls: listboxId, }); }); diff --git a/src/components/ComboBox/components/index.ts b/src/components/Combobox/components/index.ts similarity index 100% rename from src/components/ComboBox/components/index.ts rename to src/components/Combobox/components/index.ts diff --git a/src/components/Combobox/index.ts b/src/components/Combobox/index.ts new file mode 100644 index 00000000000..7613d2b6f19 --- /dev/null +++ b/src/components/Combobox/index.ts @@ -0,0 +1,2 @@ +export * from './Combobox'; +export {TextField as ComboboxTextField} from './components'; diff --git a/src/components/ComboBox/tests/ComboBox.test.tsx b/src/components/Combobox/tests/Combobox.test.tsx similarity index 63% rename from src/components/ComboBox/tests/ComboBox.test.tsx rename to src/components/Combobox/tests/Combobox.test.tsx index e98356ff697..92d1984e984 100644 --- a/src/components/ComboBox/tests/ComboBox.test.tsx +++ b/src/components/Combobox/tests/Combobox.test.tsx @@ -2,28 +2,28 @@ import React from 'react'; import {mountWithApp} from 'test-utilities'; import {TextField} from '../../TextField'; -import {ComboBox} from '../ComboBox'; -import {ListBox} from '../../ListBox'; +import {Combobox} from '../Combobox'; +import {Listbox} from '../../Listbox'; import {Popover} from '../../Popover'; import { - ComboBoxTextFieldContext, - ComboBoxListBoxContext, -} from '../../../utilities/combo-box'; + ComboboxTextFieldContext, + ComboboxListboxContext, +} from '../../../utilities/combobox'; import {Key} from '../../../types'; -describe('', () => { +describe('', () => { const activator = ( ); - const listBox = ( - - - + const listbox = ( + + + ); it('renders a Popover in the providers', () => { const combobox = mountWithApp( - {listBox}, + {listbox}, ); expect(combobox).toContainReactComponent(Popover, { @@ -35,30 +35,30 @@ describe('', () => { }); }); - it('renders the activator in ComboBoxTextFieldContext provider', () => { + it('renders the activator in ComboboxTextFieldContext provider', () => { const combobox = mountWithApp( - {listBox}, + {listbox}, ); - expect(combobox.find(ComboBoxTextFieldContext.Provider)).toHaveReactProps({ + expect(combobox.find(ComboboxTextFieldContext.Provider)).toHaveReactProps({ children: activator, }); }); - it('renders the popover children in a ComboBoxListBoxContext provider', () => { + it('renders the popover children in a ComboboxListboxContext provider', () => { const combobox = mountWithApp( - {listBox}, + {listbox}, ); triggerFocus(combobox); expect( - combobox.find(ComboBoxListBoxContext.Provider), - ).toContainReactComponent(ListBox); + combobox.find(ComboboxListboxContext.Provider), + ).toContainReactComponent(Listbox); }); - it('does not open Popover when the ComboBoxTextFieldContext onTextFieldFocus and there are no children', () => { - const combobox = mountWithApp(); + it('does not open Popover when the ComboboxTextFieldContext onTextFieldFocus and there are no children', () => { + const combobox = mountWithApp(); triggerFocus(combobox); @@ -69,11 +69,11 @@ describe('', () => { it('renders an active Popover when the activator is focused and there are children', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); @@ -85,11 +85,11 @@ describe('', () => { it('closes the Popover when onOptionSelected is triggered and allowMultiple is false', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); @@ -107,11 +107,11 @@ describe('', () => { it('does not close the Popover when onOptionSelected is triggered and allowMultiple is true and there are children', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); @@ -121,7 +121,7 @@ describe('', () => { }); combobox - .find(ComboBoxListBoxContext.Provider)! + .find(ComboboxListboxContext.Provider)! .triggerKeypath('value.onOptionSelected'); expect(combobox).toContainReactComponent(Popover, { @@ -132,15 +132,15 @@ describe('', () => { it('calls the onScrolledToBottom when the Popovers onScrolledToBottom is triggered', () => { const onScrolledToBottomSpy = jest.fn(); const combobox = mountWithApp( - - - - - , + + + + , ); triggerFocus(combobox); @@ -152,11 +152,11 @@ describe('', () => { it('closes the Popover when onClose is called', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); @@ -169,7 +169,7 @@ describe('', () => { it('opens the Popover when the TextField activator is changed', () => { const activator = ( - ', () => { /> ); const combobox = mountWithApp( - - - - - , + + + + + , ); combobox.find(TextField)?.trigger('onChange'); @@ -193,7 +193,7 @@ describe('', () => { it('closes the Popover when TextField is blurred', () => { const activator = ( - ', () => { /> ); const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); @@ -218,7 +218,7 @@ describe('', () => { describe('popover', () => { it('defaults active to false', () => { - const combobox = mountWithApp(); + const combobox = mountWithApp(); expect(combobox).toContainReactComponent(Popover, { active: false, @@ -226,7 +226,7 @@ describe('', () => { }); it('has fullWidth', () => { - const combobox = mountWithApp(); + const combobox = mountWithApp(); expect(combobox).toContainReactComponent(Popover, { fullWidth: true, @@ -234,7 +234,7 @@ describe('', () => { }); it('has autofocusTarget of none', () => { - const combobox = mountWithApp(); + const combobox = mountWithApp(); expect(combobox).toContainReactComponent(Popover, { autofocusTarget: 'none', @@ -243,7 +243,7 @@ describe('', () => { it('sets active to false when escape is pressed', () => { const activator = ( - ', () => { /> ); const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); @@ -272,7 +272,7 @@ describe('', () => { it('passes the preferredPosition', () => { const preferredPosition = 'above'; const combobox = mountWithApp( - , @@ -285,68 +285,68 @@ describe('', () => { }); describe('Context', () => { - it('sets expanded to true on the ComboBoxTextFieldContext when the popover is active', () => { + it('sets expanded to true on the ComboboxTextFieldContext when the popover is active', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); expect( - combobox.find(ComboBoxTextFieldContext.Provider)!.prop('value')! + combobox.find(ComboboxTextFieldContext.Provider)!.prop('value')! .expanded, ).toBe(true); }); - it('sets expanded to false on the ComboBoxTextFieldContext when the popover is not active', () => { + it('sets expanded to false on the ComboboxTextFieldContext when the popover is not active', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); combobox - .find(ComboBoxListBoxContext.Provider)! + .find(ComboboxListboxContext.Provider)! .triggerKeypath('value.onOptionSelected'); expect( - combobox.find(ComboBoxTextFieldContext.Provider)!.prop('value')! + combobox.find(ComboboxTextFieldContext.Provider)!.prop('value')! .expanded, ).toBe(false); }); - it('sets the activeOptionId on the ComboBoxTextFieldContext to undefined the popover is not closed', () => { + it('sets the activeOptionId on the ComboboxTextFieldContext to undefined the popover is not closed', () => { const combobox = mountWithApp( - - - - - , + + + + + , ); triggerFocus(combobox); combobox - .find(ComboBoxListBoxContext.Provider)! + .find(ComboboxListboxContext.Provider)! .triggerKeypath('value.setActiveOptionId', 'id'); expect( - combobox.find(ComboBoxTextFieldContext.Provider)!.prop('value')! + combobox.find(ComboboxTextFieldContext.Provider)!.prop('value')! .activeOptionId, ).toBe('id'); triggerOptionSelected(combobox); expect( - combobox.find(ComboBoxTextFieldContext.Provider)!.prop('value')! + combobox.find(ComboboxTextFieldContext.Provider)!.prop('value')! .activeOptionId, ).toBeUndefined(); }); @@ -355,13 +355,13 @@ describe('', () => { function triggerFocus(combobox: any) { combobox - .find(ComboBoxTextFieldContext.Provider)! + .find(ComboboxTextFieldContext.Provider)! .triggerKeypath('value.onTextFieldFocus'); } function triggerOptionSelected(combobox: any) { combobox - .find(ComboBoxListBoxContext.Provider)! + .find(ComboboxListboxContext.Provider)! .triggerKeypath('value.onOptionSelected'); } diff --git a/src/components/ListBox/components/Section/selectors.ts b/src/components/ListBox/components/Section/selectors.ts deleted file mode 100644 index 21bd5e8ba75..00000000000 --- a/src/components/ListBox/components/Section/selectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const listBoxSectionDataSelector = { - props: {'data-polaris-list-box-section-item': true}, - selector: '[data-polaris-list-box-section-item]', -}; - -export const listBoxWithinSectionDataSelector = { - attribute: 'data-polaris-list-box-within-section-item', -}; diff --git a/src/components/ListBox/index.ts b/src/components/ListBox/index.ts deleted file mode 100644 index 2908eba313a..00000000000 --- a/src/components/ListBox/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ListBox'; diff --git a/src/components/ListBox/ListBox.scss b/src/components/Listbox/Listbox.scss similarity index 90% rename from src/components/ListBox/ListBox.scss rename to src/components/Listbox/Listbox.scss index 7355fd7b068..b6fc0bdda3f 100644 --- a/src/components/ListBox/ListBox.scss +++ b/src/components/Listbox/Listbox.scss @@ -1,4 +1,4 @@ -.ListBox { +.Listbox { padding: 0; margin: 0; list-style: none; diff --git a/src/components/ListBox/ListBox.tsx b/src/components/Listbox/Listbox.tsx similarity index 82% rename from src/components/ListBox/ListBox.tsx rename to src/components/Listbox/Listbox.tsx index c2b062c6482..ad240ddd073 100644 --- a/src/components/ListBox/ListBox.tsx +++ b/src/components/Listbox/Listbox.tsx @@ -14,11 +14,11 @@ import {useUniqueId} from '../../utilities/unique-id'; import {Key} from '../../types'; import {KeypressListener} from '../KeypressListener'; import {VisuallyHidden} from '../VisuallyHidden'; -import {useComboBoxListBox} from '../../utilities/combo-box'; +import {useComboboxListbox} from '../../utilities/combobox'; import {closestParentMatch} from '../../utilities/closest-parent-match'; import {scrollIntoView} from '../../utilities/scroll-into-view'; -import {ListBoxContext, WithinListBoxContext} from '../../utilities/list-box'; -import type {NavigableOption} from '../../utilities/list-box'; +import {ListboxContext, WithinListboxContext} from '../../utilities/listbox'; +import type {NavigableOption} from '../../utilities/listbox'; import { Option, @@ -27,11 +27,11 @@ import { Action, Loading, TextOption, - listBoxSectionDataSelector, + listboxSectionDataSelector, } from './components'; -import styles from './ListBox.scss'; +import styles from './Listbox.scss'; -export interface ListBoxProps { +export interface ListboxProps { /** Inner content of the listbox */ children: ReactNode; /** Explicitly enable keyboard control */ @@ -54,42 +54,42 @@ const LISTBOX_OPTION_VALUE_ATTRIBUTE = 'data-listbox-option-value'; const DATA_ATTRIBUTE = 'data-focused'; -export function ListBox({ +export function Listbox({ children, enableKeyboardControl, accessibilityLabel, onSelect, -}: ListBoxProps) { - const listBoxClassName = classNames(styles.ListBox); +}: ListboxProps) { + const listboxClassName = classNames(styles.Listbox); const { value: keyboardEventsEnabled, setTrue: enableKeyboardEvents, setFalse: disableKeyboardEvents, } = useToggle(Boolean(enableKeyboardControl)); - const listId = useUniqueId('ListBox'); + const listId = useUniqueId('Listbox'); const scrollableRef = useRef(null); - const listBoxRef = useRef(null); + const listboxRef = useRef(null); const [loading, setLoading] = useState(); const [currentActiveOption, setCurrentActiveOption] = useState< NavigableOption >(); const { setActiveOptionId, - setListBoxId, - listBoxId, + setListboxId, + listboxId, textFieldLabelId, onOptionSelected, onKeyToBottom, textFieldFocused, - } = useComboBoxListBox(); + } = useComboboxListbox(); - const inComboBox = Boolean(setActiveOptionId); + const inCombobox = Boolean(setActiveOptionId); useEffect(() => { - if (setListBoxId && !listBoxId) { - setListBoxId(listId); + if (setListboxId && !listboxId) { + setListboxId(listId); } - }, [setListBoxId, listBoxId, listId]); + }, [setListboxId, listboxId, listId]); useEffect(() => { if (!currentActiveOption || !setActiveOptionId) return; @@ -102,7 +102,7 @@ export function ListBox({ if (scrollableRef.current) { const {element} = option; const focusTarget = first - ? closestParentMatch(element, listBoxSectionDataSelector.selector) || + ? closestParentMatch(element, listboxSectionDataSelector.selector) || element : element; @@ -139,8 +139,8 @@ export function ListBox({ ); useEffect(() => { - if (listBoxRef.current) { - scrollableRef.current = listBoxRef.current.closest(scrollable.selector); + if (listboxRef.current) { + scrollableRef.current = listboxRef.current.closest(scrollable.selector); } }, []); @@ -162,7 +162,7 @@ export function ListBox({ [handleChangeActiveOption, onSelect, onOptionSelected], ); - const listBoxContext = useMemo( + const listboxContext = useMemo( () => ({ onOptionSelect, setLoading, @@ -285,36 +285,36 @@ export function ListBox({
{loading ? loading : null}
- - + + {children ? (
    {children}
) : null} -
-
+
+
); function getNavigableOptions() { return [ ...new Set( - listBoxRef.current?.querySelectorAll( + listboxRef.current?.querySelectorAll( LISTBOX_OPTION_SELECTOR, ), ), @@ -322,9 +322,9 @@ export function ListBox({ } } -ListBox.Option = Option; -ListBox.TextOption = TextOption; -ListBox.Loading = Loading; -ListBox.Section = Section; -ListBox.Header = Header; -ListBox.Action = Action; +Listbox.Option = Option; +Listbox.TextOption = TextOption; +Listbox.Loading = Loading; +Listbox.Section = Section; +Listbox.Header = Header; +Listbox.Action = Action; diff --git a/src/components/ListBox/README.md b/src/components/Listbox/README.md similarity index 61% rename from src/components/ListBox/README.md rename to src/components/Listbox/README.md index c742a952b6c..dbcdb8c2a88 100644 --- a/src/components/ListBox/README.md +++ b/src/components/Listbox/README.md @@ -1,5 +1,5 @@ --- -name: ListBox +name: Listbox category: Lists and tables keywords: - list @@ -7,9 +7,9 @@ keywords: - interactive list --- -# ListBox +# Listbox -The `ListBox` component is a list component that implements part of the [Aria 1.2 ListBox specs](https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox). It presents a list of options and allows users to select one or more of them. If you need more structure than the standard component offers, use composition to customize the presentation of these lists by using headers or custom elements. +The `Listbox` component is a list component that implements part of the [Aria 1.2 Listbox specs](https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox). It presents a list of options and allows users to select one or more of them. If you need more structure than the standard component offers, use composition to customize the presentation of these lists by using headers or custom elements. --- @@ -27,7 +27,7 @@ Listboxes should: ### Option lists -Each item in a `ListBox` should be clear and descriptive. +Each item in a `Listbox` should be clear and descriptive. @@ -45,79 +45,79 @@ Each item in a `ListBox` should be clear and descriptive. ## Examples -### Basic ListBox +### Basic Listbox Basic implementation of a control element used to let merchants select options ```jsx -function BaseListBoxExample() { +function BaseListboxExample() { return ( - - Item 1 - Item 2 - Item 3 - + + Item 1 + Item 2 + Item 3 + ); } ``` -### ListBox with Loading +### Listbox with Loading Implementation of a control element showing a loading indicator to let merchants know more options are being loaded ```jsx -function ListBoxWithLoadingExample() { +function ListboxWithLoadingExample() { return ( - - Item 1 - Item 2 - Item 3 - - + + Item 1 + Item 2 + Item 3 + + ); } ``` -### ListBox with Action +### Listbox with Action Implementation of a control element used to let merchants take an action ```jsx -function ListBoxWithActionExample() { +function ListboxWithActionExample() { return ( - - + +
Add item
-
- Item 1 - Item 2 -
+
+ Item 1 + Item 2 +
); } ``` -### ListBox with custom element +### Listbox with custom element Implementation of a control with custom rendering of options ```jsx -function ListBoxWithCustomElementExample() { +function ListboxWithCustomElementExample() { return ( - - + + Add item - - + +
Item 1
-
- + +
Item 2
-
- + +
Item 3
-
- -
+ + + ); } ``` @@ -127,7 +127,7 @@ function ListBoxWithCustomElementExample() { ## Related components - For a text field and popover container, [use the combobox component](https://polaris.shopify.com/components/forms/combobox) -- [Autocomplete](https://polaris.shopify.com/components/forms/autocomplete) can be used as a convenience wrapper in lieu of ComboBox and ListBox. +- [Autocomplete](https://polaris.shopify.com/components/forms/autocomplete) can be used as a convenience wrapper in lieu of Combobox and Listbox. --- @@ -155,7 +155,7 @@ See Apple’s Human Interface Guidelines and API documentation about accessibili ### Structure -The `ListBox` component is based on the [Aria 1.2 ListBox pattern](https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox). +The `Listbox` component is based on the [Aria 1.2 Listbox pattern](https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox). It is important to not present interactive elements inside of list box options as they can interfere with navigation for assistive technology users. diff --git a/src/components/ListBox/components/Action/Action.scss b/src/components/Listbox/components/Action/Action.scss similarity index 100% rename from src/components/ListBox/components/Action/Action.scss rename to src/components/Listbox/components/Action/Action.scss diff --git a/src/components/ListBox/components/Action/Action.tsx b/src/components/Listbox/components/Action/Action.tsx similarity index 100% rename from src/components/ListBox/components/Action/Action.tsx rename to src/components/Listbox/components/Action/Action.tsx diff --git a/src/components/ListBox/components/Action/index.ts b/src/components/Listbox/components/Action/index.ts similarity index 100% rename from src/components/ListBox/components/Action/index.ts rename to src/components/Listbox/components/Action/index.ts diff --git a/src/components/ListBox/components/Action/tests/Action.test.tsx b/src/components/Listbox/components/Action/tests/Action.test.tsx similarity index 80% rename from src/components/ListBox/components/Action/tests/Action.test.tsx rename to src/components/Listbox/components/Action/tests/Action.test.tsx index 4f6cdbc6da1..cfae208a2c2 100644 --- a/src/components/ListBox/components/Action/tests/Action.test.tsx +++ b/src/components/Listbox/components/Action/tests/Action.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {CirclePlusMinor, AddMajor} from '@shopify/polaris-icons'; -import {mountWithListBoxProvider} from 'test-utilities/list-box'; +import {mountWithListboxProvider} from 'test-utilities/listbox'; import {Action} from '../Action'; import {Option} from '../../Option'; @@ -16,13 +16,13 @@ describe('Action', () => { }; it('passes props to Option', () => { - const action = mountWithListBoxProvider(); + const action = mountWithListboxProvider(); expect(action).toContainReactComponent(Option, defaultProps); }); it('passes select, disabled from props to text option', () => { - const action = mountWithListBoxProvider( + const action = mountWithListboxProvider( , ); @@ -33,7 +33,7 @@ describe('Action', () => { }); it('does not renders a default Icon', () => { - const action = mountWithListBoxProvider(); + const action = mountWithListboxProvider(); expect(action).not.toContainReactComponent(Icon, { source: CirclePlusMinor, @@ -41,7 +41,7 @@ describe('Action', () => { }); it('renders the Icon from the prop', () => { - const action = mountWithListBoxProvider( + const action = mountWithListboxProvider( , ); @@ -52,7 +52,7 @@ describe('Action', () => { it('renders the children', () => { const label = 'test label'; - const action = mountWithListBoxProvider( + const action = mountWithListboxProvider( {label}, ); diff --git a/src/components/ListBox/components/Header/Header.scss b/src/components/Listbox/components/Header/Header.scss similarity index 100% rename from src/components/ListBox/components/Header/Header.scss rename to src/components/Listbox/components/Header/Header.scss diff --git a/src/components/ListBox/components/Header/Header.tsx b/src/components/Listbox/components/Header/Header.tsx similarity index 100% rename from src/components/ListBox/components/Header/Header.tsx rename to src/components/Listbox/components/Header/Header.tsx diff --git a/src/components/ListBox/components/Header/index.ts b/src/components/Listbox/components/Header/index.ts similarity index 100% rename from src/components/ListBox/components/Header/index.ts rename to src/components/Listbox/components/Header/index.ts diff --git a/src/components/ListBox/components/Header/tests/Header.test.tsx b/src/components/Listbox/components/Header/tests/Header.test.tsx similarity index 100% rename from src/components/ListBox/components/Header/tests/Header.test.tsx rename to src/components/Listbox/components/Header/tests/Header.test.tsx diff --git a/src/components/ListBox/components/Loading/Loading.scss b/src/components/Listbox/components/Loading/Loading.scss similarity index 100% rename from src/components/ListBox/components/Loading/Loading.scss rename to src/components/Listbox/components/Loading/Loading.scss diff --git a/src/components/ListBox/components/Loading/Loading.tsx b/src/components/Listbox/components/Loading/Loading.tsx similarity index 88% rename from src/components/ListBox/components/Loading/Loading.tsx rename to src/components/Listbox/components/Loading/Loading.tsx index 1e0f43c8160..ee1f4b50261 100644 --- a/src/components/ListBox/components/Loading/Loading.tsx +++ b/src/components/Listbox/components/Loading/Loading.tsx @@ -1,7 +1,7 @@ import React, {memo, useEffect} from 'react'; import {Spinner} from '../../../Spinner'; -import {useListBox} from '../../../../utilities/list-box'; +import {useListbox} from '../../../../utilities/listbox'; import styles from './Loading.scss'; @@ -14,7 +14,7 @@ export const Loading = memo(function LoadingOption({ children, accessibilityLabel: label, }: LoadingProps) { - const {setLoading} = useListBox(); + const {setLoading} = useListbox(); useEffect(() => { setLoading(label); diff --git a/src/components/ListBox/components/Loading/index.ts b/src/components/Listbox/components/Loading/index.ts similarity index 100% rename from src/components/ListBox/components/Loading/index.ts rename to src/components/Listbox/components/Loading/index.ts diff --git a/src/components/ListBox/components/Loading/tests/Loading.test.tsx b/src/components/Listbox/components/Loading/tests/Loading.test.tsx similarity index 78% rename from src/components/ListBox/components/Loading/tests/Loading.test.tsx rename to src/components/Listbox/components/Loading/tests/Loading.test.tsx index 7e56192d8c1..416b44c24cb 100644 --- a/src/components/ListBox/components/Loading/tests/Loading.test.tsx +++ b/src/components/Listbox/components/Loading/tests/Loading.test.tsx @@ -1,11 +1,11 @@ import React from 'react'; import {mountWithApp} from 'test-utilities'; -import {ListBoxContext} from '../../../../../utilities/list-box'; +import {ListboxContext} from '../../../../../utilities/listbox'; import {Loading} from '../Loading'; import {Spinner} from '../../../../Spinner'; -const listBoxContext = { +const listboxContext = { addNavigableOption: noop, updateNavigableOption: noop, removeNavigableOption: noop, @@ -16,13 +16,13 @@ const listBoxContext = { describe('Loading', () => { const defaultProps = {accessibilityLabel: 'accessibility label'}; - it('throws if not inside a listBox Context', () => { + it('throws if not inside a listbox Context', () => { const consoleErrorSpy = jest .spyOn(console, 'error') .mockImplementation(() => {}); expect(() => mountWithApp()).toThrow( - 'No ListBox was provided. ListBox components must be wrapped in a Listbox', + 'No Listbox was provided. Listbox components must be wrapped in a Listbox', ); consoleErrorSpy.mockRestore(); @@ -32,11 +32,11 @@ describe('Loading', () => { const accessibilityLabel = 'label'; const customLoadingState = 'customLoadingState'; const loading = mountWithApp( - +
{customLoadingState}
-
, +
, ); expect(loading).toContainReactComponent('div', { @@ -49,13 +49,13 @@ describe('Loading', () => { const accessibilityLabel = 'label'; const setLoadingSpy = jest.fn(); const contextValue = { - ...listBoxContext, + ...listboxContext, setLoading: setLoadingSpy, }; mountWithApp( - + - , + , ); expect(setLoadingSpy).toHaveBeenCalledWith(accessibilityLabel); @@ -65,13 +65,13 @@ describe('Loading', () => { const accessibilityLabel = 'label'; const setLoadingSpy = jest.fn(); const contextValue = { - ...listBoxContext, + ...listboxContext, setLoading: setLoadingSpy, }; mountWithApp( - + - , + , ); expect(setLoadingSpy).toHaveBeenCalledWith(accessibilityLabel); @@ -80,13 +80,13 @@ describe('Loading', () => { it('calls setLoading with undefined when it unmounts', () => { const setLoadingSpy = jest.fn(); const contextValue = { - ...listBoxContext, + ...listboxContext, setLoading: setLoadingSpy, }; const listbox = mountWithApp( - + - , + , ); listbox.find(Loading)!.root.unmount(); diff --git a/src/components/ListBox/components/Option/Option.scss b/src/components/Listbox/components/Option/Option.scss similarity index 100% rename from src/components/ListBox/components/Option/Option.scss rename to src/components/Listbox/components/Option/Option.scss diff --git a/src/components/ListBox/components/Option/Option.tsx b/src/components/Listbox/components/Option/Option.tsx similarity index 91% rename from src/components/ListBox/components/Option/Option.tsx rename to src/components/Listbox/components/Option/Option.tsx index d8a6b0d2e04..a67d6ca57b1 100644 --- a/src/components/ListBox/components/Option/Option.tsx +++ b/src/components/Listbox/components/Option/Option.tsx @@ -2,8 +2,8 @@ import React, {useRef, useCallback, memo, useContext} from 'react'; import {classNames} from '../../../../utilities/css'; import {useUniqueId} from '../../../../utilities/unique-id'; -import {useListBox} from '../../../../utilities/list-box'; -import {useSection, listBoxWithinSectionDataSelector} from '../Section'; +import {useListbox} from '../../../../utilities/listbox'; +import {useSection, listboxWithinSectionDataSelector} from '../Section'; import {TextOption} from '../TextOption'; import {UnstyledLink} from '../../../UnstyledLink'; import {MappedActionContext} from '../../../../utilities/autocomplete'; @@ -33,12 +33,12 @@ export const Option = memo(function Option({ accessibilityLabel, divider, }: OptionProps) { - const {onOptionSelect} = useListBox(); + const {onOptionSelect} = useListbox(); const {role, url, external, onAction, destructive, isAction} = useContext( MappedActionContext, ); const listItemRef = useRef(null); - const domId = useUniqueId('ListBoxOption'); + const domId = useUniqueId('ListboxOption'); const sectionId = useSection(); const isWithinSection = Boolean(sectionId); @@ -73,7 +73,7 @@ export const Option = memo(function Option({ ); const sectionAttributes = { - [listBoxWithinSectionDataSelector.attribute]: isWithinSection, + [listboxWithinSectionDataSelector.attribute]: isWithinSection, }; const legacyRoleSupport = role || 'option'; diff --git a/src/components/ListBox/components/Option/index.ts b/src/components/Listbox/components/Option/index.ts similarity index 100% rename from src/components/ListBox/components/Option/index.ts rename to src/components/Listbox/components/Option/index.ts diff --git a/src/components/ListBox/components/Option/tests/Option.test.tsx b/src/components/Listbox/components/Option/tests/Option.test.tsx similarity index 86% rename from src/components/ListBox/components/Option/tests/Option.test.tsx rename to src/components/Listbox/components/Option/tests/Option.test.tsx index 4459b816904..2e934546526 100644 --- a/src/components/ListBox/components/Option/tests/Option.test.tsx +++ b/src/components/Listbox/components/Option/tests/Option.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {mount} from 'test-utilities'; -import {mountWithListBoxProvider} from 'test-utilities/list-box'; +import {mountWithListboxProvider} from 'test-utilities/listbox'; -import type {ListBoxContext} from '../../../../../utilities/list-box'; +import type {ListboxContext} from '../../../../../utilities/listbox'; import {Option} from '../Option'; import {TextOption} from '../../TextOption'; import {MappedActionContext} from '../../../../../utilities/autocomplete'; @@ -20,13 +20,13 @@ const defaultProps = { value: 'value', }; -const defaultContext: React.ContextType = { +const defaultContext: React.ContextType = { onOptionSelect: noop, setLoading: noop, }; describe('Option', () => { - it("throws when the Option does not have 'ListBoxContext'", () => { + it("throws when the Option does not have 'ListboxContext'", () => { const consoleErrorSpy = jest .spyOn(console, 'error') .mockImplementation(() => {}); @@ -50,7 +50,7 @@ describe('Option', () => { onClick: expect.any(Function), }; - const option = mountWithListBoxProvider(