Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add StudioTextResourcePicker component #13954

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};
Loading