From b080ddd51008272610fd00e7afa1986714348140 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 11 Dec 2024 13:05:11 +0100 Subject: [PATCH 1/3] fix: Simplify autocomplete input for V3 --- .../StudioNativeSelect/StudioNativeSelect.tsx | 4 +- .../components/StudioNativeSelect/index.ts | 2 +- .../config/EditFormComponent.test.tsx | 7 +- .../src/components/config/componentConfig.tsx | 4 +- .../editModal/EditAutoComplete.test.tsx | 107 ------------- .../config/editModal/EditAutoComplete.tsx | 148 ------------------ .../EditAutocomplete.test.tsx | 59 +++++++ .../EditAutocomplete/EditAutocomplete.tsx | 78 +++++++++ .../EditAutocomplete/autocompleteOptions.ts | 55 +++++++ .../editModal/EditAutocomplete/index.ts | 1 + .../updateAutocomplete.test.ts | 21 +++ .../EditAutocomplete/updateAutocomplete.ts | 12 ++ 12 files changed, 237 insertions(+), 261 deletions(-) delete mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.test.tsx delete mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx create mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.test.tsx create mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.tsx create mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/autocompleteOptions.ts create mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/index.ts create mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts create mode 100644 frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts diff --git a/frontend/libs/studio-components/src/components/StudioNativeSelect/StudioNativeSelect.tsx b/frontend/libs/studio-components/src/components/StudioNativeSelect/StudioNativeSelect.tsx index cda916ac136..7ad55b34669 100644 --- a/frontend/libs/studio-components/src/components/StudioNativeSelect/StudioNativeSelect.tsx +++ b/frontend/libs/studio-components/src/components/StudioNativeSelect/StudioNativeSelect.tsx @@ -1,7 +1,9 @@ import React, { forwardRef, useId } from 'react'; import { NativeSelect, type NativeSelectProps } from '@digdir/designsystemet-react'; -export const StudioNativeSelect = forwardRef( +export type StudioNativeSelectProps = NativeSelectProps; + +export const StudioNativeSelect = forwardRef( ({ children, description, label, id, size, ...rest }, ref): React.JSX.Element => { const defaultId = useId(); id = id ?? defaultId; diff --git a/frontend/libs/studio-components/src/components/StudioNativeSelect/index.ts b/frontend/libs/studio-components/src/components/StudioNativeSelect/index.ts index d7cbd872cfa..0457a032a88 100644 --- a/frontend/libs/studio-components/src/components/StudioNativeSelect/index.ts +++ b/frontend/libs/studio-components/src/components/StudioNativeSelect/index.ts @@ -1 +1 @@ -export { StudioNativeSelect } from './StudioNativeSelect'; +export * from './StudioNativeSelect'; diff --git a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx index 6ddf4fcd5c1..136b7d6b486 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx @@ -92,8 +92,11 @@ describe('EditFormComponent', () => { Object.keys(labels).map(async (label) => expect(await screen.findByRole(labels[label], { name: textMock(label) })), ); - expect(screen.getByRole('combobox')); - expect(screen.getByLabelText('Autocomplete (WCAG)')); + expect( + screen.getByRole('combobox', { + name: textMock('ux_editor.component_properties.autocomplete'), + }), + ); }); it('should return header specific content when type header', async () => { diff --git a/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx b/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx index bcd4c9887f2..46dff32215d 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/componentConfig.tsx @@ -7,7 +7,7 @@ import { EditOptions } from './editModal/EditOptions'; import { EditPreselectedIndex } from './editModal/EditPreselectedIndex'; import { EditReadOnly } from './editModal/EditReadOnly'; import { EditRequired } from './editModal/EditRequired'; -import { EditAutoComplete } from './editModal/EditAutoComplete'; +import { EditAutocomplete } from './editModal/EditAutocomplete'; import { EditTextResourceBinding } from './editModal/EditTextResourceBinding'; import type { FormComponent } from '../../types/FormComponent'; @@ -121,7 +121,7 @@ export const configComponents: IConfigComponents = { [EditSettings.Options]: EditOptions, [EditSettings.CodeList]: EditCodeList, [EditSettings.PreselectedIndex]: EditPreselectedIndex, - [EditSettings.AutoComplete]: EditAutoComplete, + [EditSettings.AutoComplete]: EditAutocomplete, [EditSettings.Help]: ({ component, handleComponentChange }: IGenericEditComponent) => ( { - const layoutSchemaResult = renderHookWithMockStore()(() => useLayoutSchemaQuery()) - .renderHookResult.result; - await waitFor(() => expect(layoutSchemaResult.current[0].isSuccess).toBe(true)); -}; - -export const render = async ( - handleComponentChangeMock: any = jest.fn(), - component: FormComponent = componentMock, -) => { - await waitForData(); - return renderWithMockStore()( - , - ); -}; - -test('should render first 6 suggestions on search field focused', async () => { - await render(); - const user = userEvent.setup(); - - const inputField = screen.getByRole('textbox'); - expect(inputField).toBeInTheDocument(); - - await user.click(inputField); - - expect(await screen.findByRole('dialog')).toBeInTheDocument(); - expect(screen.getAllByRole('option')).toHaveLength(6); -}); - -test('should filter options while typing in search field', async () => { - await render(); - const user = userEvent.setup(); - - await user.type(screen.getByRole('textbox'), 'of'); - - await waitFor(() => expect(screen.getByRole('textbox')).toHaveValue('of')); - - expect(screen.getByRole('option', { name: 'off' })).toBeInTheDocument(); - expect(screen.queryByRole('option', { name: 'given-name' })).not.toBeInTheDocument(); -}); - -test('should set the chosen options within the search field', async () => { - await render(); - const user = userEvent.setup(); - - const searchField = screen.getByRole('textbox'); - - await user.type(searchField, 'of'); - await waitFor(() => expect(searchField).toHaveValue('of')); - await user.click(screen.getByRole('option', { name: 'off' })); - - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - await waitFor(() => expect(searchField).toHaveValue('off')); -}); - -test('should toggle autocomplete-popup based onFocus and onBlur', async () => { - await render(); - const user = userEvent.setup(); - await user.click(screen.getByRole('textbox')); - - expect(await screen.findByRole('dialog')).toBeInTheDocument(); - - await user.tab(); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); -}); - -test('should call handleComponentChangeMock callback ', async () => { - const handleComponentChangeMock = jest.fn(); - await render(handleComponentChangeMock); - - const user = userEvent.setup(); - - const inputField = screen.getByRole('textbox'); - expect(inputField).toBeInTheDocument(); - - await user.click(inputField); - await screen.findByRole('dialog'); - - await user.click(screen.getByRole('option', { name: 'on' })); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(handleComponentChangeMock).toHaveBeenCalledWith({ - autocomplete: 'on', - dataModelBindings: {}, - id: 'random-id', - itemType: 'COMPONENT', - propertyPath: 'definitions/inputComponent', - type: 'Input', - }); -}); diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx deleted file mode 100644 index 6fc0dd4d643..00000000000 --- a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import type { ChangeEvent } from 'react'; -import React, { useEffect, useMemo, useState } from 'react'; -import type { IGenericEditComponent } from '../componentConfig'; -import { stringToArray, arrayToString } from '../../../utils/stringUtils'; -import { FormField } from '../../FormField'; -import { StudioButton, StudioPopover, StudioTextfield } from '@studio/components'; -import { ArrayUtils } from '@studio/pure-functions'; - -const getLastWord = (value: string) => value.split(' ').pop(); -const stdAutocompleteOpts = [ - 'on', - 'off', - 'name', - 'honorific-prefix', - 'given-name', - 'additional-name', - 'family-name', - 'honorific-suffix', - 'nickname', - 'email', - 'username', - 'new-password', - 'current-password', - 'one-time-code', - 'organization-title', - 'organization', - 'street-address', - 'address-line1', - 'address-line2', - 'address-line3', - 'address-level4', - 'address-level3', - 'address-level2', - 'address-level1', - 'country', - 'country-name', - 'postal-code', - 'cc-name', - 'cc-given-name', - 'cc-additional-name', - 'cc-family-name', - 'cc-number', - 'cc-exp', - 'cc-exp-month', - 'cc-exp-year', - 'cc-csc', - 'cc-type', - 'transaction-currency', - 'transaction-amount', - 'language', - 'bday', - 'bday-day', - 'bday-month', - 'bday-year', - 'sex', - 'tel', - 'tel-country-code', - 'tel-national', - 'tel-area-code', - 'tel-local', - 'tel-extension', - 'url', - 'photo', -]; - -export const EditAutoComplete = ({ component, handleComponentChange }: IGenericEditComponent) => { - const [searchFieldFocused, setSearchFieldFocused] = useState(false); - const initialAutocompleteText = component?.autocomplete || ''; - const [autocompleteText, setAutocompleteText] = useState(initialAutocompleteText); - - useEffect(() => { - setAutocompleteText(initialAutocompleteText); - }, [initialAutocompleteText, component.id]); - - const autoCompleteOptions = useMemo((): string[] => { - const lastWord = getLastWord(autocompleteText); - return stdAutocompleteOpts.filter((alternative) => alternative.includes(lastWord))?.slice(0, 6); - }, [autocompleteText]); - - const buildNewText = (word: string): string => { - const wordParts = stringToArray(autocompleteText, ' '); - const newWordParts = ArrayUtils.replaceLastItem(wordParts, word); - return arrayToString(newWordParts); - }; - - const handleWordClick = (word: string): void => { - const autocomplete = buildNewText(word); - setAutocompleteText(autocomplete); - handleComponentChange({ - ...component, - autocomplete, - }); - setSearchFieldFocused(false); - }; - - const handleChange = (value: string): void => { - if (!searchFieldFocused) setSearchFieldFocused(true); - setAutocompleteText(value); - }; - - return ( -
- ( - setSearchFieldFocused(true)} - onBlur={(): void => { - if (searchFieldFocused) setSearchFieldFocused(false); - }} - onChange={(event: ChangeEvent) => { - const value = event.target.value; - handleChange(value); - fieldProps.onChange(value); - }} - /> - )} - /> - 0} - placement='bottom-start' - > - {
} - - {autoCompleteOptions.map( - (option): JSX.Element => ( - handleWordClick(option)} - > - {option} - - ), - )} - - -
- ); -}; diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.test.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.test.tsx new file mode 100644 index 00000000000..ebc4e7b2d8b --- /dev/null +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import type { EditAutocompleteProps } from './'; +import { EditAutocomplete } from './'; +import type { RenderResult } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3'; +import type { FormComponent } from '../../../../types/FormComponent'; +import { renderWithProviders } from '../../../../testing/mocks'; + +// Test data: +const component: FormComponent = { + id: 'random-id', + autocomplete: '', + type: ComponentTypeV3.Input, + itemType: 'COMPONENT', + propertyPath: 'definitions/inputComponent', + dataModelBindings: {}, +}; +const handleComponentChange = jest.fn(); +const defaultProps: EditAutocompleteProps = { + handleComponentChange, + component, +}; + +describe('EditAutocomplete', () => { + it('Calls handleComponentChange with the updated component when the value is changed ', async () => { + const user = userEvent.setup(); + const optionToChoose = 'on'; + renderEditAutocomplete(); + + const combobox = screen.getByRole('combobox'); + const option = screen.getByRole('option', { name: optionToChoose }); + await user.selectOptions(combobox, option); + + expect(handleComponentChange).toHaveBeenCalledWith({ + autocomplete: optionToChoose, + dataModelBindings: {}, + id: 'random-id', + itemType: 'COMPONENT', + propertyPath: 'definitions/inputComponent', + type: 'Input', + }); + }); + + it('Renders with the given autocomplete value as selected', () => { + const selectedValue = 'on'; + const componentWithAutocomplete: FormComponent = { + ...component, + autocomplete: selectedValue, + }; + renderEditAutocomplete({ component: componentWithAutocomplete }); + expect(screen.getByRole('combobox')).toHaveValue(selectedValue); + }); +}); + +function renderEditAutocomplete(props: Partial = {}): RenderResult { + return renderWithProviders(); +} diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.tsx new file mode 100644 index 00000000000..bfc169597a4 --- /dev/null +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/EditAutocomplete.tsx @@ -0,0 +1,78 @@ +import type { ChangeEvent, ReactElement } from 'react'; +import React, { useCallback } from 'react'; +import type { IGenericEditComponent } from '../../componentConfig'; +import { FormField } from '../../../FormField'; +import type { StudioNativeSelectProps } from '@studio/components'; +import { StudioNativeSelect } from '@studio/components'; +import { useTranslation } from 'react-i18next'; +import { updateAutocomplete } from './updateAutocomplete'; +import { autocompleteOptions } from './autocompleteOptions'; +import type { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3'; +import type { FormComponent } from '../../../../types/FormComponent'; + +export type EditAutocompleteProps = IGenericEditComponent>; + +export function EditAutocomplete({ + component, + handleComponentChange, +}: EditAutocompleteProps): ReactElement { + const { t } = useTranslation(); + + const handleChange = useCallback( + (value: string): void => { + const updatedComponent = updateAutocomplete(component, value); + handleComponentChange(updatedComponent); + }, + [component, handleComponentChange], + ); + + return ( +
+ } + /> +
+ ); +} + +type AutocompleteFieldProps = { + onChange: (value: string) => void; +} & Omit; + +function AutocompleteField({ onChange, ...rest }: AutocompleteFieldProps): ReactElement { + const handleChange = useCallback( + (e: ChangeEvent) => { + onChange(e.target.value); + }, + [onChange], + ); + + return ( + + + + + ); +} + +function EmptyOption(): ReactElement { + const { t } = useTranslation(); + return ; +} + +function AutocompleteOptions(): ReactElement { + return ( + <> + {autocompleteOptions.map((option: string) => ( + + ))} + + ); +} diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/autocompleteOptions.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/autocompleteOptions.ts new file mode 100644 index 00000000000..12024a8732e --- /dev/null +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/autocompleteOptions.ts @@ -0,0 +1,55 @@ +export const autocompleteOptions: string[] = [ + 'on', + 'off', + 'name', + 'honorific-prefix', + 'given-name', + 'additional-name', + 'family-name', + 'honorific-suffix', + 'nickname', + 'email', + 'username', + 'new-password', + 'current-password', + 'one-time-code', + 'organization-title', + 'organization', + 'street-address', + 'address-line1', + 'address-line2', + 'address-line3', + 'address-level4', + 'address-level3', + 'address-level2', + 'address-level1', + 'country', + 'country-name', + 'postal-code', + 'cc-name', + 'cc-given-name', + 'cc-additional-name', + 'cc-family-name', + 'cc-number', + 'cc-exp', + 'cc-exp-month', + 'cc-exp-year', + 'cc-csc', + 'cc-type', + 'transaction-currency', + 'transaction-amount', + 'language', + 'bday', + 'bday-day', + 'bday-month', + 'bday-year', + 'sex', + 'tel', + 'tel-country-code', + 'tel-national', + 'tel-area-code', + 'tel-local', + 'tel-extension', + 'url', + 'photo', +]; diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/index.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/index.ts new file mode 100644 index 00000000000..a12d2b32d5e --- /dev/null +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/index.ts @@ -0,0 +1 @@ +export * from './EditAutocomplete'; diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts new file mode 100644 index 00000000000..fdbd0a7a334 --- /dev/null +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts @@ -0,0 +1,21 @@ +import { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3'; +import type { FormComponent } from '../../../../types/FormComponent'; +import { updateAutocomplete } from './updateAutocomplete'; + +describe('updateAutocomplete', () => { + it('Updates the autocomplete value of the given component', () => { + const component: FormComponent = { + id: 'test', + type: ComponentTypeV3.Input, + autocomplete: 'off', + itemType: 'COMPONENT', + dataModelBindings: {}, + }; + const newAutocomplete = 'on'; + const result = updateAutocomplete(component, newAutocomplete); + expect(result).toEqual({ + ...component, + autocomplete: newAutocomplete, + }); + }); +}); diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts new file mode 100644 index 00000000000..e10f37fc8c2 --- /dev/null +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts @@ -0,0 +1,12 @@ +import type { FormComponent } from '../../../../types/FormComponent'; +import type { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3'; + +export function updateAutocomplete( + component: FormComponent, + autocomplete: string, +): FormComponent { + return { + ...component, + autocomplete, + }; +} From f130a2d88e934ed91e84d44cfefd70f830e5eb6e Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Thu, 12 Dec 2024 13:57:55 +0100 Subject: [PATCH 2/3] Update frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx Co-authored-by: andreastanderen <71079896+standeren@users.noreply.github.com> --- .../src/components/config/EditFormComponent.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx index 48388b59f30..9e3d80a1b48 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx +++ b/frontend/packages/ux-editor-v3/src/components/config/EditFormComponent.test.tsx @@ -99,7 +99,7 @@ describe('EditFormComponent', () => { screen.getByRole('combobox', { name: textMock('ux_editor.component_properties.autocomplete'), }), - ); + ).toBeInTheDocument(); }); it('should return header specific content when type header', async () => { From 7412678c1e33ac0824d0694678428ee74be2d948 Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 12 Dec 2024 15:27:01 +0100 Subject: [PATCH 3/3] Add removal of autocomplete property --- .../EditAutocomplete/updateAutocomplete.test.ts | 17 +++++++++++++++++ .../EditAutocomplete/updateAutocomplete.ts | 17 +++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts index fdbd0a7a334..f49d8ab67c4 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.test.ts @@ -18,4 +18,21 @@ describe('updateAutocomplete', () => { autocomplete: newAutocomplete, }); }); + + it('Removes the autocomplete property from the object when it receives an empty string', () => { + const component: FormComponent = { + id: 'test', + type: ComponentTypeV3.Input, + autocomplete: 'on', + itemType: 'COMPONENT', + dataModelBindings: {}, + }; + const result = updateAutocomplete(component, ''); + expect(result).toEqual({ + id: component.id, + type: component.type, + itemType: component.itemType, + dataModelBindings: component.dataModelBindings, + }); + }); }); diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts index e10f37fc8c2..1a86b0a2c46 100644 --- a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts +++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutocomplete/updateAutocomplete.ts @@ -5,8 +5,17 @@ export function updateAutocomplete( component: FormComponent, autocomplete: string, ): FormComponent { - return { - ...component, - autocomplete, - }; + if (autocomplete === '') return removeAutocomplete(component); + else + return { + ...component, + autocomplete, + }; +} + +function removeAutocomplete( + component: FormComponent, +): FormComponent { + const { autocomplete, ...rest } = component; + return rest; }