diff --git a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.mdx b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.mdx index dfaa940df5..031c505c91 100644 --- a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.mdx +++ b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.mdx @@ -35,3 +35,7 @@ Dersom du ønsker å skjule kontodetaljer, kan du bruke `hideAccountDetails`. Dersom du ønsker å ha ekstra tekst på bunnen av lista, kan du bruke `postListElement`. + +Dersom du ønsker å vise en annen informasjon enn kontonavn i inputen, så kan du velge hvilken attribute som brukes med `displayAttribute`. + + diff --git a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.spec.tsx b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.spec.tsx index 38c5e9fed8..85c21be46e 100644 --- a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.spec.tsx +++ b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.spec.tsx @@ -618,4 +618,70 @@ describe('', () => { expect(a11yStatusMessage).toHaveTextContent(''); jest.useRealTimers(); }); + + it('allows passing a custom display attribute', async () => { + const handleAccountSelectedMock = jest.fn(); + + render( + , + ); + + const input = screen.getByRole('combobox'); + + expect(input.getAttribute('value')).toBe(''); + fireEvent.change(input, { target: { value: 'Gr' } }); + + fireEvent.click(screen.getByText('Gris')); + + expect(handleAccountSelectedMock).toHaveBeenCalledTimes(1); + expect(handleAccountSelectedMock).toHaveBeenCalledWith(accounts[3]); + expect(input.getAttribute('value')).toEqual('1253 47 789102'); + }); + + it('passing displayAttribute should make it searchable', async () => { + type FunkyAccounts = Account & { funkySmell: string }; + const funkyAccounts: FunkyAccounts[] = accounts.map((account, idx) => ({ + ...account, + funkySmell: `Smells like money${idx}`, + })); + + render( + + id="id" + labelledById="labelId" + accounts={funkyAccounts} + displayAttribute={'funkySmell'} + locale="nb" + onAccountSelected={handleAccountSelected} + onReset={onReset} + selectedAccount={funkyAccounts[0]} + ariaInvalid={false} + />, + ); + + const input = screen.getByRole('combobox'); + fireEvent.click(input); + + expect(screen.queryByText('Ingen samsvarende konto')).toBeNull(); + fireEvent.change(input, { + target: { value: 'Dette skal få ingen match' }, + }); + expect( + screen.queryByText('Ingen samsvarende konto'), + ).toBeInTheDocument(); + + fireEvent.change(input, { target: { value: 'money3' } }); + fireEvent.click(screen.getByText('Gris')); + expect(input.getAttribute('value')).toEqual('Smells like money3'); + }); }); diff --git a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.stories.tsx b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.stories.tsx index 1c1f457879..905c0a322b 100644 --- a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.stories.tsx +++ b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.stories.tsx @@ -3,6 +3,7 @@ import { AccountSelector } from './AccountSelector'; import { InputGroup } from '@sb1/ffe-form-react'; import type { StoryObj, Meta } from '@storybook/react'; import { SmallText } from '@sb1/ffe-core-react'; +import { accountFormatter } from '../format'; const meta: Meta = { title: 'Komponenter/Account-selector/AccountSelector', @@ -254,3 +255,41 @@ export const InitialValue: Story = { ); }, }; + +type PrettyAccount = Account & { prettyName: string }; + +const prettyAccounts: PrettyAccount[] = accounts.map(account => ({ + ...account, + prettyName: `${account.name} - ${accountFormatter(account.accountNumber)}`, +})); +export const CustomDisplayAttribute: StoryObj< + typeof AccountSelector +> = { + args: { + id: 'input-id', + labelledById: 'label-id', + locale: 'nb', + formatAccountNumber: true, + allowCustomAccount: false, + displayAttribute: 'prettyName', + accounts: prettyAccounts, + }, + render: function Render(args) { + const [selectedAccount, setSelectedAccount] = useState( + prettyAccounts[2], + ); + return ( + + + {...args} + selectedAccount={selectedAccount} + onAccountSelected={setSelectedAccount} + /> + + ); + }, +}; diff --git a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.tsx b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.tsx index e2d6ecb0c7..eb41191f7a 100644 --- a/packages/ffe-account-selector-react/src/account-selector/AccountSelector.tsx +++ b/packages/ffe-account-selector-react/src/account-selector/AccountSelector.tsx @@ -40,6 +40,8 @@ export interface AccountSelectorProps { formatAccountNumber?: boolean; /** id of element that labels input field */ labelledById?: string; + /** Attribute used in the input when an item is selected. **/ + displayAttribute?: keyof T; /** * Allows selecting the text the user writes even if it does not match anything in the accounts array. * Useful e.g. if you want to pay to account that is not in yur recipients list. @@ -91,6 +93,7 @@ export const AccountSelector = ({ ariaInvalid, onOpen, onClose, + displayAttribute, ...rest }: AccountSelectorProps) => { const [inputValue, setInputValue] = useState(selectedAccount?.name || ''); @@ -119,6 +122,7 @@ export const AccountSelector = ({ onAccountSelected({ name: value.name, accountNumber: value.name, + ...(displayAttribute ? { [displayAttribute]: value.name } : {}), } as T); setInputValue(value.name); } else { @@ -137,6 +141,7 @@ export const AccountSelector = ({ id={id} labelledById={labelledById} + displayAttribute={displayAttribute} inputProps={{ ...inputProps, onChange: onInputChange, @@ -165,6 +170,13 @@ export const AccountSelector = ({ ? formatter(inputValue) : inputValue, accountNumber: '', + ...(displayAttribute + ? { + [displayAttribute]: formatter + ? formatter(inputValue) + : inputValue, + } + : {}), } as T, ], } @@ -172,7 +184,11 @@ export const AccountSelector = ({ } formatter={formatter} onChange={handleAccountSelected} - searchAttributes={['name', 'accountNumber']} + searchAttributes={[ + 'name', + 'accountNumber', + ...(displayAttribute ? [displayAttribute] : []), + ]} locale={locale} optionBody={({ item, isHighlighted, ...restOptionBody }) => { if (OptionBody) { diff --git a/packages/ffe-searchable-dropdown-react/src/getListToRender.ts b/packages/ffe-searchable-dropdown-react/src/getListToRender.ts index e742e45409..15d006a613 100644 --- a/packages/ffe-searchable-dropdown-react/src/getListToRender.ts +++ b/packages/ffe-searchable-dropdown-react/src/getListToRender.ts @@ -35,7 +35,7 @@ export const getListToRender = >({ searchMatcher?: SearchMatcher; showAllItemsInDropdown: boolean; }): { noMatch: boolean; listToRender: Item[] } => { - const trimmedInput = inputValue ? inputValue.trim() : ''; + const trimmedInput = inputValue ? String(inputValue).trim() : ''; const shouldFilter = trimmedInput.length > 0; diff --git a/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.spec.tsx b/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.spec.tsx index 4ec07f2dd0..044f5d422f 100644 --- a/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.spec.tsx +++ b/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.spec.tsx @@ -1026,4 +1026,33 @@ describe('SearchableDropdown', () => { await user.click(input); await screen.findByText('Dette er et postListElement!'); }); + + it('allows passing a custom display attribute', async () => { + const onChange = jest.fn(); + const user = userEvent.setup(); + + render( + , + ); + + const input = screen.getByRole('combobox'); + + expect(input.getAttribute('value')).toBe(''); + await user.type(input, 'Be'); + + await user.click(screen.getByText('Beslag skytter')); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(companies[2]); + expect(input.getAttribute('value')).toEqual('812602552'); + }); }); diff --git a/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.stories.tsx b/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.stories.tsx index 6785ee2b41..f78b7510bd 100644 --- a/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.stories.tsx +++ b/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.stories.tsx @@ -227,3 +227,27 @@ export const PostListElement: Story = { ); }, }; + +export const CustomDisplayAttribute: Story = { + args: { + ...Standard.args, + displayAttribute: 'organizationNumber', + dropdownAttributes: ['organizationName', 'organizationNumber'], + searchAttributes: ['organizationNumber', 'organizationName'], + }, + render: function Render({ id, labelledById, ...args }) { + return ( + + + + ); + }, +}; diff --git a/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.tsx b/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.tsx index 48a6d53995..0076ba7fb1 100644 --- a/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.tsx +++ b/packages/ffe-searchable-dropdown-react/src/single/SearchableDropdown.tsx @@ -51,6 +51,8 @@ export interface SearchableDropdownProps> { dropdownAttributes: (keyof Item)[]; /** Array of attributes used when filtering search */ searchAttributes: (keyof Item)[]; + /** Attribute used in the input when an item is selected. Defaults to first in searchAttributes **/ + displayAttribute?: keyof Item; /** Props used on input field */ inputProps?: React.ComponentProps<'input'>; /** Limits number of rendered dropdown elements */ @@ -105,6 +107,7 @@ function SearchableDropdownWithForwardRef>( dropdownList, dropdownAttributes, searchAttributes, + displayAttribute = searchAttributes[0], maxRenderedDropdownElements = Number.MAX_SAFE_INTEGER, onChange, inputProps, @@ -127,6 +130,7 @@ function SearchableDropdownWithForwardRef>( const [state, dispatch] = useReducer( createReducer({ dropdownList, + displayAttribute: displayAttribute, searchAttributes, maxRenderedDropdownElements, noMatchDropdownList: noMatch?.dropdownList, @@ -137,7 +141,8 @@ function SearchableDropdownWithForwardRef>( isExpanded: false, selectedItems: [], highlightedIndex: -1, - inputValue: selectedItem ? selectedItem[dropdownAttributes[0]] : '', + formattedInputValue: '', + inputValue: selectedItem ? selectedItem[displayAttribute] : '', }, initialState => { return { @@ -189,7 +194,7 @@ function SearchableDropdownWithForwardRef>( isLoading, locale, resultCount: state.listToRender.length, - selectedValue: state.selectedItem?.[searchAttributes[0]], + selectedValue: state.selectedItem?.[displayAttribute], }); useLayoutEffect(() => { diff --git a/packages/ffe-searchable-dropdown-react/src/single/reducer.ts b/packages/ffe-searchable-dropdown-react/src/single/reducer.ts index efe0c6fc8d..8b85f51bca 100644 --- a/packages/ffe-searchable-dropdown-react/src/single/reducer.ts +++ b/packages/ffe-searchable-dropdown-react/src/single/reducer.ts @@ -24,6 +24,7 @@ export const createReducer = >({ searchAttributes, dropdownList, + displayAttribute, noMatchDropdownList, maxRenderedDropdownElements, searchMatcher, @@ -31,6 +32,7 @@ export const createReducer = }: { dropdownList: Item[]; searchAttributes: Array; + displayAttribute: keyof Item; noMatchDropdownList: Item[] | undefined; maxRenderedDropdownElements: number; searchMatcher: SearchMatcher | undefined; @@ -38,16 +40,17 @@ export const createReducer = }) => (state: State, action: Action): State => { switch (action.type) { - case 'InputKeyDownEscape': + case 'InputKeyDownEscape': { return { ...state, noMatch: false, isExpanded: false, highlightedIndex: -1, inputValue: state.selectedItem - ? state.selectedItem[searchAttributes[0]] + ? state.selectedItem[displayAttribute] : '', }; + } case 'InputClick': { const { noMatch, listToRender } = getListToRender({ inputValue: state.inputValue, @@ -90,23 +93,24 @@ export const createReducer = noMatch, }; } - case 'ToggleButtonPressed': + case 'ToggleButtonPressed': { return { ...state, isExpanded: !state.isExpanded, }; + } case 'ItemSelectedProgrammatically': case 'ItemOnClick': - case 'InputKeyDownEnter': + case 'InputKeyDownEnter': { return { ...state, isExpanded: false, highlightedIndex: -1, selectedItem: action.payload?.selectedItem, inputValue: - action.payload?.selectedItem?.[searchAttributes[0]] || - '', + action.payload?.selectedItem?.[displayAttribute] || '', }; + } case 'InputKeyDownArrowDown': case 'InputKeyDownArrowUp': { @@ -153,7 +157,7 @@ export const createReducer = } const inputValue = selectedItem - ? selectedItem[searchAttributes[0]] + ? selectedItem[displayAttribute] : ''; return { ...state,