Skip to content

Commit

Permalink
feat: Add StudioTextResourcePicker component (#13954)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasEng authored Oct 31, 2024
1 parent fad4b91 commit 79d7a4f
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 0 deletions.
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',
},
};
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;
}
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';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './StudioTextResourcePicker';
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' },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type TextResource = {
id: string;
value: string;
[key: string]: any;
};
1 change: 1 addition & 0 deletions frontend/libs/studio-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export * from './StudioTableLocalPagination';
export * from './StudioTableRemotePagination';
export * from './StudioTabs';
export * from './StudioTag';
export * from './StudioTextResourcePicker';
export * from './StudioTextarea';
export * from './StudioTextfield';
export * from './StudioToggleableTextfield';
Expand Down

0 comments on commit 79d7a4f

Please sign in to comment.