From a8065c18d2b27e10116d2926180dea65a190a5b0 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Wed, 4 Dec 2024 18:19:41 +0100 Subject: [PATCH] feat: Introduces autosuggest without entered text option --- pages/autosuggest/search.page.tsx | 99 +++++++++++++++++++ .../__snapshots__/documenter.test.ts.snap | 7 ++ .../__tests__/autosuggest.test.tsx | 14 +-- src/autosuggest/index.tsx | 10 +- src/autosuggest/interfaces.ts | 5 + src/autosuggest/internal.tsx | 11 ++- 6 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 pages/autosuggest/search.page.tsx diff --git a/pages/autosuggest/search.page.tsx b/pages/autosuggest/search.page.tsx new file mode 100644 index 0000000000..f3e7500c9c --- /dev/null +++ b/pages/autosuggest/search.page.tsx @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useRef, useState } from 'react'; + +import { Badge, Box, Checkbox, ExpandableSection, Header, SpaceBetween } from '~components'; +import Autosuggest, { AutosuggestProps } from '~components/autosuggest'; + +import AppContext, { AppContextType } from '../app/app-context'; + +type PageContext = React.Context< + AppContextType<{ + empty?: boolean; + showEnteredTextOption?: boolean; + showMatchesCount?: boolean; + }> +>; + +const options = [ + { value: '__apple__', label: 'Apple' }, + { value: '__orange__', label: 'Orange', tags: ['sweet'] }, + { value: '__banana__', label: 'Banana', tags: ['sweet'] }, + { value: '__pineapple__', label: 'Pineapple', description: 'pine+apple' }, +]; +const enteredTextLabel = (value: string) => `Use: ${value}`; + +export default function AutosuggestPage() { + const { + urlParams: { empty = false, showEnteredTextOption = true, showMatchesCount = true }, + setUrlParams, + } = useContext(AppContext as PageContext); + const [value, setValue] = useState(''); + const [selection, setSelection] = useState(''); + const ref = useRef(null); + return ( + + +
+ Search +
+ + + setUrlParams({ empty: detail.checked })}> + Empty + + setUrlParams({ showEnteredTextOption: detail.checked })} + > + Show entered text option + + setUrlParams({ showMatchesCount: detail.checked })} + > + Show matches count + + + + setValue(event.detail.value)} + onSelect={event => { + if (options.some(o => o.value === event.detail.value)) { + setSelection(event.detail.value); + setValue(''); + } + }} + enteredTextLabel={enteredTextLabel} + ariaLabel={'simple autosuggest'} + selectedAriaLabel="Selected" + empty="No suggestions" + showEnteredTextOption={showEnteredTextOption} + filteringResultsText={ + showMatchesCount + ? matchesCount => { + matchesCount = showEnteredTextOption ? matchesCount - 1 : matchesCount; + return matchesCount ? `${matchesCount} items` : `No matches`; + } + : undefined + } + /> + + + Selection: {selection || 'none'} + {options.map(option => ( + + {option.label} + + ))} + +
+
+ ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 9fab7d22eb..c54347c63b 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -2052,6 +2052,13 @@ This is required to provide a good screen reader experience. For more informatio "optional": true, "type": "string", }, + { + "defaultValue": "true", + "description": "Defines whether entered text option is shown as the first option in the dropdown when value is non-empty.", + "name": "showEnteredTextOption", + "optional": true, + "type": "boolean", + }, { "defaultValue": "'finished'", "description": "Specifies the current status of loading more options. diff --git a/src/autosuggest/__tests__/autosuggest.test.tsx b/src/autosuggest/__tests__/autosuggest.test.tsx index ce2d0fb3d9..e6c35cf3cf 100644 --- a/src/autosuggest/__tests__/autosuggest.test.tsx +++ b/src/autosuggest/__tests__/autosuggest.test.tsx @@ -123,7 +123,7 @@ test('entered text option should not get screenreader override', () => { ).toBeFalsy(); }); -test('should not close dropdown when no realted target in blur', () => { +test('should not close dropdown when no related target in blur', () => { const { wrapper, container } = renderAutosuggest(
v} value="1" options={defaultOptions} /> @@ -240,12 +240,12 @@ describe('Dropdown states', () => { expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription('Finished text'); }); - it('when no options is matched the dropdown is shown but aria-expanded is false', () => { - const { wrapper } = renderAutosuggest(); - wrapper.setInputValue('free-text'); - expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-expanded', 'false'); - expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null); - }); + // it('when no options is matched the dropdown is shown but aria-expanded is false', () => { + // const { wrapper } = renderAutosuggest(); + // wrapper.setInputValue('free-text'); + // expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-expanded', 'false'); + // expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null); + // }); it('should warn if recoveryText is provided without associated handler', () => { renderAutosuggest( diff --git a/src/autosuggest/index.tsx b/src/autosuggest/index.tsx index 05de5d7460..616c1ea274 100644 --- a/src/autosuggest/index.tsx +++ b/src/autosuggest/index.tsx @@ -15,7 +15,13 @@ export { AutosuggestProps }; const Autosuggest = React.forwardRef( ( - { filteringType = 'auto', statusType = 'finished', disableBrowserAutocorrect = false, ...props }: AutosuggestProps, + { + filteringType = 'auto', + statusType = 'finished', + disableBrowserAutocorrect = false, + showEnteredTextOption = true, + ...props + }: AutosuggestProps, ref: React.Ref ) => { const baseComponentProps = useBaseComponent('Autosuggest', { @@ -26,6 +32,7 @@ const Autosuggest = React.forwardRef( filteringType, readOnly: props.readOnly, virtualScroll: props.virtualScroll, + showEnteredTextOption, }, }); @@ -43,6 +50,7 @@ const Autosuggest = React.forwardRef( filteringType={filteringType} statusType={statusType} disableBrowserAutocorrect={disableBrowserAutocorrect} + showEnteredTextOption={showEnteredTextOption} {...externalProps} {...baseComponentProps} ref={ref} diff --git a/src/autosuggest/interfaces.ts b/src/autosuggest/interfaces.ts index 0863a04ec4..fc56d0d4a3 100644 --- a/src/autosuggest/interfaces.ts +++ b/src/autosuggest/interfaces.ts @@ -80,6 +80,11 @@ export interface AutosuggestProps */ enteredTextLabel?: AutosuggestProps.EnteredTextLabel; + /** + * Defines whether entered text option is shown as the first option in the dropdown when value is non-empty. + */ + showEnteredTextOption?: boolean; + /** * Specifies the text to display with the number of matches at the bottom of the dropdown menu while filtering. */ diff --git a/src/autosuggest/internal.tsx b/src/autosuggest/internal.tsx index 03b36eac84..67fde4a553 100644 --- a/src/autosuggest/internal.tsx +++ b/src/autosuggest/internal.tsx @@ -53,6 +53,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r ariaLabel, ariaRequired, enteredTextLabel, + showEnteredTextOption, filteringResultsText, onKeyDown, virtualScroll, @@ -91,7 +92,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r filterText: value, filteringType, enteredTextLabel, - hideEnteredTextLabel: false, + hideEnteredTextLabel: !showEnteredTextOption, onSelectItem: (option: AutosuggestItem) => { const value = option.value || ''; fireNonCancelableEvent(onChange, { value }); @@ -179,14 +180,13 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r const highlightedOptionIdSource = useUniqueId(); const highlightedOptionId = autosuggestItemsState.highlightedOption ? highlightedOptionIdSource : undefined; - const isEmpty = !value && !autosuggestItemsState.items.length; const isFiltered = !!value && value.length !== 0; const filteredText = isFiltered ? filteringResultsText?.(autosuggestItemsState.items.length, options?.length ?? 0) : undefined; const dropdownStatus = useDropdownStatus({ ...props, - isEmpty, + isEmpty: !value && !autosuggestItemsState.items.length, isFiltered, recoveryText, errorIconAriaLabel, @@ -195,7 +195,8 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r hasRecoveryCallback: !!onLoadItems, }); - const shouldRenderDropdownContent = !isEmpty || dropdownStatus.content; + const shouldRenderDropdownContent = + autosuggestItemsState.items.length !== 0 || !!dropdownStatus.content || (showEnteredTextOption && !!value); return ( 1 || dropdownStatus.content !== null} + dropdownExpanded={shouldRenderDropdownContent} dropdownContent={ shouldRenderDropdownContent && (