-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add StudioTextResourcePicker component (#13954)
- Loading branch information
Showing
7 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
...o-components/src/components/StudioTextResourcePicker/StudioTextResourcePicker.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { StudioTextResourcePicker } from './StudioTextResourcePicker'; | ||
import { textResourcesMock } from './test-data/textResourcesMock'; | ||
|
||
type Story = StoryObj<typeof StudioTextResourcePicker>; | ||
|
||
const meta: Meta<typeof StudioTextResourcePicker> = { | ||
title: 'Components/StudioTextResourcePicker', | ||
component: StudioTextResourcePicker, | ||
}; | ||
export default meta; | ||
|
||
export const Preview: Story = { | ||
args: { | ||
label: 'Velg tekst', | ||
emptyListText: 'Fant ingen tekster', | ||
textResources: textResourcesMock, | ||
onValueChange: (id: string) => console.log(id), | ||
value: 'land.NO', | ||
}, | ||
}; |
100 changes: 100 additions & 0 deletions
100
...udio-components/src/components/StudioTextResourcePicker/StudioTextResourcePicker.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type { ForwardedRef } from 'react'; | ||
import React from 'react'; | ||
import { textResourcesMock } from './test-data/textResourcesMock'; | ||
import type { StudioTextResourcePickerProps } from './StudioTextResourcePicker'; | ||
import { StudioTextResourcePicker } from './StudioTextResourcePicker'; | ||
import type { RenderResult } from '@testing-library/react'; | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import { testRefForwarding } from '../../test-utils/testRefForwarding'; | ||
import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending'; | ||
import { testCustomAttributes } from '../../test-utils/testCustomAttributes'; | ||
import userEvent from '@testing-library/user-event'; | ||
import type { TextResource } from './types/TextResource'; | ||
|
||
// Test data: | ||
const textResources = textResourcesMock; | ||
const onValueChange = jest.fn(); | ||
const emptyListText = 'No text resources'; | ||
const defaultProps: StudioTextResourcePickerProps = { | ||
emptyListText, | ||
onValueChange, | ||
textResources, | ||
}; | ||
|
||
describe('StudioTextResourcePicker', () => { | ||
it('Renders a combobox', () => { | ||
renderTextResourcePicker(); | ||
expect(getCombobox()).toBeInTheDocument(); | ||
}); | ||
|
||
it('Renders with the given label', () => { | ||
const label = 'Test label'; | ||
renderTextResourcePicker({ label }); | ||
expect(getCombobox()).toHaveAccessibleName(label); | ||
}); | ||
|
||
it('Displays the given text resources when the user clicks', async () => { | ||
const user = userEvent.setup(); | ||
const testTextResources: TextResource[] = [ | ||
{ id: '1', value: 'Test 1' }, | ||
{ id: '2', value: 'Test 2' }, | ||
]; | ||
renderTextResourcePicker({ textResources: testTextResources }); | ||
await user.click(getCombobox()); | ||
testTextResources.forEach((textResource) => { | ||
const expectedName = expectedOptionName(textResource); | ||
expect(screen.getByRole('option', { name: expectedName })).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('Calls the onValueChange callback when the user picks a text resource', async () => { | ||
const user = userEvent.setup(); | ||
renderTextResourcePicker(); | ||
await user.click(getCombobox()); | ||
const textResourceToPick = textResources[129]; | ||
await user.click(screen.getByRole('option', { name: expectedOptionName(textResourceToPick) })); | ||
await waitFor(expect(onValueChange).toBeCalled); | ||
expect(onValueChange).toHaveBeenCalledTimes(1); | ||
expect(onValueChange).toHaveBeenCalledWith(textResourceToPick.id); | ||
}); | ||
|
||
it('Displays the empty list text when the user clicks and there are no text resources', async () => { | ||
const user = userEvent.setup(); | ||
renderTextResourcePicker({ textResources: [] }); | ||
await user.click(getCombobox()); | ||
expect(screen.getByText(emptyListText)).toBeInTheDocument(); | ||
}); | ||
|
||
it("Renders with the text of the text resource of which the ID is given by the component's value prop", () => { | ||
const pickedTextResource = textResources[129]; | ||
renderTextResourcePicker({ value: pickedTextResource.id }); | ||
expect(getCombobox()).toHaveValue(pickedTextResource.value); | ||
}); | ||
|
||
it('Forwards the ref', () => { | ||
testRefForwarding<HTMLInputElement>((ref) => renderTextResourcePicker({}, ref), getCombobox); | ||
}); | ||
|
||
it('Applies the class name to the root element', () => { | ||
testRootClassNameAppending((className) => renderTextResourcePicker({ className })); | ||
}); | ||
|
||
it('Accepts additional props', () => { | ||
testCustomAttributes(renderTextResourcePicker, getCombobox); | ||
}); | ||
}); | ||
|
||
function renderTextResourcePicker( | ||
props: Partial<StudioTextResourcePickerProps> = {}, | ||
ref?: ForwardedRef<HTMLInputElement>, | ||
): RenderResult { | ||
return render(<StudioTextResourcePicker {...defaultProps} {...props} ref={ref} />); | ||
} | ||
|
||
function getCombobox(): HTMLInputElement { | ||
return screen.getByRole('combobox') as HTMLInputElement; | ||
} | ||
|
||
function expectedOptionName(textResource: TextResource): string { | ||
return textResource.value + ' ' + textResource.id; | ||
} |
57 changes: 57 additions & 0 deletions
57
...bs/studio-components/src/components/StudioTextResourcePicker/StudioTextResourcePicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import type { ReactElement } from 'react'; | ||
import React, { forwardRef, useCallback } from 'react'; | ||
import type { TextResource } from './types/TextResource'; | ||
import type { StudioComboboxProps } from '../StudioCombobox'; | ||
import { StudioCombobox } from '../StudioCombobox'; | ||
|
||
export type StudioTextResourcePickerProps = Omit<StudioComboboxProps, keyof OverriddenProps> & | ||
OverriddenProps & | ||
AdditionalProps; | ||
|
||
type OverriddenProps = { | ||
onValueChange: (id: string) => void; | ||
value?: string; | ||
}; | ||
|
||
type AdditionalProps = { | ||
emptyListText: string; | ||
textResources: TextResource[]; | ||
}; | ||
|
||
export const StudioTextResourcePicker = forwardRef<HTMLInputElement, StudioTextResourcePickerProps>( | ||
({ textResources, onSelect, onValueChange, emptyListText, value, ...rest }, ref) => { | ||
const handleValueChange = useCallback(([id]: string[]) => onValueChange(id), [onValueChange]); | ||
|
||
return ( | ||
<StudioCombobox | ||
hideLabel | ||
onValueChange={handleValueChange} | ||
value={value ? [value] : []} | ||
{...rest} | ||
ref={ref} | ||
> | ||
<StudioCombobox.Empty>{emptyListText}</StudioCombobox.Empty> | ||
{renderTextResourceOptions(textResources)} | ||
</StudioCombobox> | ||
); | ||
}, | ||
); | ||
|
||
function renderTextResourceOptions(textResources: TextResource[]): ReactElement[] { | ||
// This cannot be a component function since the option components must be direct children of the combobox component. | ||
return textResources.map(renderTextResourceOption); | ||
} | ||
|
||
function renderTextResourceOption(textResource: TextResource): ReactElement { | ||
return ( | ||
<StudioCombobox.Option | ||
description={textResource.id} | ||
key={textResource.id} | ||
value={textResource.id} | ||
> | ||
{textResource.value} | ||
</StudioCombobox.Option> | ||
); | ||
} | ||
|
||
StudioTextResourcePicker.displayName = 'StudioTextResourcePicker'; |
1 change: 1 addition & 0 deletions
1
frontend/libs/studio-components/src/components/StudioTextResourcePicker/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './StudioTextResourcePicker'; |
198 changes: 198 additions & 0 deletions
198
.../studio-components/src/components/StudioTextResourcePicker/test-data/textResourcesMock.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import type { TextResource } from '../types/TextResource'; | ||
|
||
export const textResourcesMock: TextResource[] = [ | ||
{ id: 'land.AF', value: 'Afghanistan' }, | ||
{ id: 'land.AL', value: 'Albania' }, | ||
{ id: 'land.DZ', value: 'Algerie' }, | ||
{ id: 'land.AD', value: 'Andorra' }, | ||
{ id: 'land.AO', value: 'Angola' }, | ||
{ id: 'land.AG', value: 'Antigua og Barbuda' }, | ||
{ id: 'land.AR', value: 'Argentina' }, | ||
{ id: 'land.AM', value: 'Armenia' }, | ||
{ id: 'land.AU', value: 'Australia' }, | ||
{ id: 'land.AT', value: 'Østerrike' }, | ||
{ id: 'land.AZ', value: 'Aserbajdsjan' }, | ||
{ id: 'land.BS', value: 'Bahamas' }, | ||
{ id: 'land.BH', value: 'Bahrain' }, | ||
{ id: 'land.BD', value: 'Bangladesh' }, | ||
{ id: 'land.BB', value: 'Barbados' }, | ||
{ id: 'land.BY', value: 'Hviterussland' }, | ||
{ id: 'land.BE', value: 'Belgia' }, | ||
{ id: 'land.BZ', value: 'Belize' }, | ||
{ id: 'land.BJ', value: 'Benin' }, | ||
{ id: 'land.BT', value: 'Bhutan' }, | ||
{ id: 'land.BO', value: 'Bolivia' }, | ||
{ id: 'land.BA', value: 'Bosnia-Hercegovina' }, | ||
{ id: 'land.BW', value: 'Botswana' }, | ||
{ id: 'land.BR', value: 'Brasil' }, | ||
{ id: 'land.BN', value: 'Brunei' }, | ||
{ id: 'land.BG', value: 'Bulgaria' }, | ||
{ id: 'land.BF', value: 'Burkina Faso' }, | ||
{ id: 'land.BI', value: 'Burundi' }, | ||
{ id: 'land.CV', value: 'Kapp Verde' }, | ||
{ id: 'land.KH', value: 'Kambodsja' }, | ||
{ id: 'land.CM', value: 'Kamerun' }, | ||
{ id: 'land.CA', value: 'Canada' }, | ||
{ id: 'land.CF', value: 'Den sentralafrikanske republikk' }, | ||
{ id: 'land.TD', value: 'Tsjad' }, | ||
{ id: 'land.CL', value: 'Chile' }, | ||
{ id: 'land.CN', value: 'Kina' }, | ||
{ id: 'land.CO', value: 'Colombia' }, | ||
{ id: 'land.KM', value: 'Komorene' }, | ||
{ id: 'land.CG', value: 'Kongo (Brazzaville)' }, | ||
{ id: 'land.CD', value: 'Kongo (Kinshasa)' }, | ||
{ id: 'land.CR', value: 'Costa Rica' }, | ||
{ id: 'land.HR', value: 'Kroatia' }, | ||
{ id: 'land.CU', value: 'Cuba' }, | ||
{ id: 'land.CY', value: 'Kypros' }, | ||
{ id: 'land.CZ', value: 'Tsjekkia' }, | ||
{ id: 'land.DK', value: 'Danmark' }, | ||
{ id: 'land.DJ', value: 'Djibouti' }, | ||
{ id: 'land.DM', value: 'Dominica' }, | ||
{ id: 'land.DO', value: 'Den dominikanske republikk' }, | ||
{ id: 'land.EC', value: 'Ecuador' }, | ||
{ id: 'land.EG', value: 'Egypt' }, | ||
{ id: 'land.SV', value: 'El Salvador' }, | ||
{ id: 'land.GQ', value: 'Ekvatorial-Guinea' }, | ||
{ id: 'land.ER', value: 'Eritrea' }, | ||
{ id: 'land.EE', value: 'Estland' }, | ||
{ id: 'land.SZ', value: 'Eswatini' }, | ||
{ id: 'land.ET', value: 'Etiopia' }, | ||
{ id: 'land.FJ', value: 'Fiji' }, | ||
{ id: 'land.FI', value: 'Finland' }, | ||
{ id: 'land.FR', value: 'Frankrike' }, | ||
{ id: 'land.GA', value: 'Gabon' }, | ||
{ id: 'land.GM', value: 'Gambia' }, | ||
{ id: 'land.GE', value: 'Georgia' }, | ||
{ id: 'land.DE', value: 'Tyskland' }, | ||
{ id: 'land.GH', value: 'Ghana' }, | ||
{ id: 'land.GR', value: 'Hellas' }, | ||
{ id: 'land.GD', value: 'Grenada' }, | ||
{ id: 'land.GT', value: 'Guatemala' }, | ||
{ id: 'land.GN', value: 'Guinea' }, | ||
{ id: 'land.GW', value: 'Guinea-Bissau' }, | ||
{ id: 'land.GY', value: 'Guyana' }, | ||
{ id: 'land.HT', value: 'Haiti' }, | ||
{ id: 'land.HN', value: 'Honduras' }, | ||
{ id: 'land.HU', value: 'Ungarn' }, | ||
{ id: 'land.IS', value: 'Island' }, | ||
{ id: 'land.IN', value: 'India' }, | ||
{ id: 'land.ID', value: 'Indonesia' }, | ||
{ id: 'land.IR', value: 'Iran' }, | ||
{ id: 'land.IQ', value: 'Irak' }, | ||
{ id: 'land.IE', value: 'Irland' }, | ||
{ id: 'land.IL', value: 'Israel' }, | ||
{ id: 'land.IT', value: 'Italia' }, | ||
{ id: 'land.CI', value: 'Elfenbenskysten' }, | ||
{ id: 'land.JM', value: 'Jamaica' }, | ||
{ id: 'land.JP', value: 'Japan' }, | ||
{ id: 'land.JO', value: 'Jordan' }, | ||
{ id: 'land.KZ', value: 'Kasakhstan' }, | ||
{ id: 'land.KE', value: 'Kenya' }, | ||
{ id: 'land.KI', value: 'Kiribati' }, | ||
{ id: 'land.KP', value: 'Nord-Korea' }, | ||
{ id: 'land.KR', value: 'Sør-Korea' }, | ||
{ id: 'land.KW', value: 'Kuwait' }, | ||
{ id: 'land.KG', value: 'Kirgisistan' }, | ||
{ id: 'land.LA', value: 'Laos' }, | ||
{ id: 'land.LV', value: 'Latvia' }, | ||
{ id: 'land.LB', value: 'Libanon' }, | ||
{ id: 'land.LS', value: 'Lesotho' }, | ||
{ id: 'land.LR', value: 'Liberia' }, | ||
{ id: 'land.LY', value: 'Libya' }, | ||
{ id: 'land.LI', value: 'Liechtenstein' }, | ||
{ id: 'land.LT', value: 'Litauen' }, | ||
{ id: 'land.LU', value: 'Luxembourg' }, | ||
{ id: 'land.MG', value: 'Madagaskar' }, | ||
{ id: 'land.MW', value: 'Malawi' }, | ||
{ id: 'land.MY', value: 'Malaysia' }, | ||
{ id: 'land.MV', value: 'Maldivene' }, | ||
{ id: 'land.ML', value: 'Mali' }, | ||
{ id: 'land.MT', value: 'Malta' }, | ||
{ id: 'land.MH', value: 'Marshalløyene' }, | ||
{ id: 'land.MR', value: 'Mauritania' }, | ||
{ id: 'land.MU', value: 'Mauritius' }, | ||
{ id: 'land.MX', value: 'Mexico' }, | ||
{ id: 'land.FM', value: 'Mikronesiaføderasjonen' }, | ||
{ id: 'land.MD', value: 'Moldova' }, | ||
{ id: 'land.MC', value: 'Monaco' }, | ||
{ id: 'land.MN', value: 'Mongolia' }, | ||
{ id: 'land.ME', value: 'Montenegro' }, | ||
{ id: 'land.MA', value: 'Marokko' }, | ||
{ id: 'land.MZ', value: 'Mosambik' }, | ||
{ id: 'land.MM', value: 'Myanmar' }, | ||
{ id: 'land.NA', value: 'Namibia' }, | ||
{ id: 'land.NR', value: 'Nauru' }, | ||
{ id: 'land.NP', value: 'Nepal' }, | ||
{ id: 'land.NL', value: 'Nederland' }, | ||
{ id: 'land.NZ', value: 'New Zealand' }, | ||
{ id: 'land.NI', value: 'Nicaragua' }, | ||
{ id: 'land.NE', value: 'Niger' }, | ||
{ id: 'land.NG', value: 'Nigeria' }, | ||
{ id: 'land.MK', value: 'Nord-Makedonia' }, | ||
{ id: 'land.NO', value: 'Norge' }, | ||
{ id: 'land.OM', value: 'Oman' }, | ||
{ id: 'land.PK', value: 'Pakistan' }, | ||
{ id: 'land.PW', value: 'Palau' }, | ||
{ id: 'land.PA', value: 'Panama' }, | ||
{ id: 'land.PG', value: 'Papua Ny-Guinea' }, | ||
{ id: 'land.PY', value: 'Paraguay' }, | ||
{ id: 'land.PE', value: 'Peru' }, | ||
{ id: 'land.PH', value: 'Filippinene' }, | ||
{ id: 'land.PL', value: 'Polen' }, | ||
{ id: 'land.PT', value: 'Portugal' }, | ||
{ id: 'land.QA', value: 'Qatar' }, | ||
{ id: 'land.RO', value: 'Romania' }, | ||
{ id: 'land.RU', value: 'Russland' }, | ||
{ id: 'land.RW', value: 'Rwanda' }, | ||
{ id: 'land.KN', value: 'Saint Kitts og Nevis' }, | ||
{ id: 'land.LC', value: 'Saint Lucia' }, | ||
{ id: 'land.VC', value: 'Saint Vincent og Grenadinene' }, | ||
{ id: 'land.WS', value: 'Samoa' }, | ||
{ id: 'land.SM', value: 'San Marino' }, | ||
{ id: 'land.ST', value: 'São Tomé og Príncipe' }, | ||
{ id: 'land.SA', value: 'Saudi-Arabia' }, | ||
{ id: 'land.SN', value: 'Senegal' }, | ||
{ id: 'land.RS', value: 'Serbia' }, | ||
{ id: 'land.SC', value: 'Seychellene' }, | ||
{ id: 'land.SL', value: 'Sierra Leone' }, | ||
{ id: 'land.SG', value: 'Singapore' }, | ||
{ id: 'land.SK', value: 'Slovakia' }, | ||
{ id: 'land.SI', value: 'Slovenia' }, | ||
{ id: 'land.SB', value: 'Salomonøyene' }, | ||
{ id: 'land.SO', value: 'Somalia' }, | ||
{ id: 'land.ZA', value: 'Sør-Afrika' }, | ||
{ id: 'land.SS', value: 'Sør-Sudan' }, | ||
{ id: 'land.ES', value: 'Spania' }, | ||
{ id: 'land.LK', value: 'Sri Lanka' }, | ||
{ id: 'land.SD', value: 'Sudan' }, | ||
{ id: 'land.SR', value: 'Surinam' }, | ||
{ id: 'land.SE', value: 'Sverige' }, | ||
{ id: 'land.CH', value: 'Sveits' }, | ||
{ id: 'land.SY', value: 'Syria' }, | ||
{ id: 'land.TW', value: 'Taiwan' }, | ||
{ id: 'land.TJ', value: 'Tadsjikistan' }, | ||
{ id: 'land.TZ', value: 'Tanzania' }, | ||
{ id: 'land.TH', value: 'Thailand' }, | ||
{ id: 'land.TG', value: 'Togo' }, | ||
{ id: 'land.TO', value: 'Tonga' }, | ||
{ id: 'land.TT', value: 'Trinidad og Tobago' }, | ||
{ id: 'land.TN', value: 'Tunisia' }, | ||
{ id: 'land.TR', value: 'Tyrkia' }, | ||
{ id: 'land.TM', value: 'Turkmenistan' }, | ||
{ id: 'land.TV', value: 'Tuvalu' }, | ||
{ id: 'land.UG', value: 'Uganda' }, | ||
{ id: 'land.UA', value: 'Ukraina' }, | ||
{ id: 'land.AE', value: 'De forente arabiske emirater' }, | ||
{ id: 'land.GB', value: 'Storbritannia' }, | ||
{ id: 'land.US', value: 'USA' }, | ||
{ id: 'land.UY', value: 'Uruguay' }, | ||
{ id: 'land.UZ', value: 'Usbekistan' }, | ||
{ id: 'land.VU', value: 'Vanuatu' }, | ||
{ id: 'land.VA', value: 'Vatikanstaten' }, | ||
{ id: 'land.VE', value: 'Venezuela' }, | ||
{ id: 'land.VN', value: 'Vietnam' }, | ||
{ id: 'land.YE', value: 'Jemen' }, | ||
{ id: 'land.ZM', value: 'Zambia' }, | ||
{ id: 'land.ZW', value: 'Zimbabwe' }, | ||
]; |
5 changes: 5 additions & 0 deletions
5
...tend/libs/studio-components/src/components/StudioTextResourcePicker/types/TextResource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export type TextResource = { | ||
id: string; | ||
value: string; | ||
[key: string]: any; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters