Skip to content

Commit

Permalink
Merge branch 'main' into fix/react-router-v7_relativeSplatPath
Browse files Browse the repository at this point in the history
  • Loading branch information
adamhaeger committed Dec 12, 2024
2 parents 8d5b1d4 + 95de927 commit 70851c7
Show file tree
Hide file tree
Showing 22 changed files with 273 additions and 49 deletions.
58 changes: 57 additions & 1 deletion src/app-components/Input/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,64 @@
import type { HTMLInputTypeAttribute } from 'react';
import type { HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute } from 'react';

export type InputType = Extract<
HTMLInputTypeAttribute,
'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url'
>;

export const EXTERNAL_INPUT_TYPE = ['text', 'search'] satisfies InputType[];

export const INPUT_AUTO_COMPLETE: HTMLInputAutoCompleteAttribute[] = [
'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',
];
29 changes: 28 additions & 1 deletion src/codegen/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,34 @@ const common = {
maxHeight: 2,
},
}),

ILikertColumnProperties: () =>
new CG.obj(
new CG.prop(
'columns',
new CG.arr(
new CG.obj(
new CG.prop(
'value',
new CG.union(new CG.str().setPattern(/^\d+$/), new CG.num())
.setTitle('Value')
.setDescription('The value of the answer column'),
),
new CG.prop(
'divider',
new CG.enum('before', 'after', 'both')
.setTitle('Divider')
.setDescription(
"Choose if the divider should be shown 'before', 'after' or on 'both' sides of the column.",
)
.optional(),
),
),
)
.optional()
.setTitle('Columns')
.setDescription('Add customization to the columns of the likert component'),
),
),
// Types that component definitions extend:
ComponentBase: () =>
new CG.obj(
Expand Down
57 changes: 30 additions & 27 deletions src/features/formData/FormDataReaders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,37 +160,40 @@ describe('FormDataReaders', () => {
.mockName('window.logErrorOnce');
});

it('simple, should render a resource with a variable lookup', async () => {
const { queries, urlFor } = await render({
ids: ['test'],
textResources: [
{
id: 'test',
value: 'Hello {0}',
variables: [
{
dataSource: 'dataModel.someModel',
key: 'name',
},
],
it.each<string>(['someModel', 'someModel1.0'])(
'simple, should render a resource with a variable lookup - %s',
async (modelName: string) => {
const { queries, urlFor } = await render({
ids: ['test'],
textResources: [
{
id: 'test',
value: 'Hello {0}',
variables: [
{
dataSource: `dataModel.${modelName}`,
key: 'name',
},
],
},
],
dataModels: {
[modelName]: {
name: 'World',
},
},
],
dataModels: {
someModel: {
name: 'World',
},
},
defaultDataModel: 'someModel',
});
defaultDataModel: modelName,
});

await waitFor(() => expect(screen.getByTestId('test')).toHaveTextContent('Hello World'));
await waitFor(() => expect(screen.getByTestId('test')).toHaveTextContent('Hello World'));

expect(queries.fetchFormData).toHaveBeenCalledTimes(1);
expect(queries.fetchFormData).toHaveBeenCalledWith(urlFor('someModel'), {});
expect(queries.fetchFormData).toHaveBeenCalledTimes(1);
expect(queries.fetchFormData).toHaveBeenCalledWith(urlFor(modelName), {});

expect(window.logError).not.toHaveBeenCalled();
expect(window.logErrorOnce).not.toHaveBeenCalled();
});
expect(window.logError).not.toHaveBeenCalled();
expect(window.logErrorOnce).not.toHaveBeenCalled();
},
);

it('advanced, should fetch data from multiple models, handle failures', async () => {
jest.useFakeTimers();
Expand Down
17 changes: 16 additions & 1 deletion src/features/language/useLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ function getTextResourceByKey(
return getTextResourceByKey(value, textResources, dataSources);
}

function splitNTimes(text: string, sep: string, n: number) {
if (n === 0) {
return [text];
} else if (n < 0) {
throw new Error(`Invalid N:${n}`);
}
const parts = text.split(sep);
const out: string[] = [];
for (let i = 0; i < n; i++) {
out.push(parts.shift() ?? '');
}
out.push(parts.join(sep));
return out;
}

function replaceVariables(text: string, variables: IVariable[], dataSources: TextResourceVariablesDataSources) {
const {
node,
Expand All @@ -345,7 +360,7 @@ function replaceVariables(text: string, variables: IVariable[], dataSources: Tex
let value = variables[idx].key;

if (variable.dataSource.startsWith('dataModel')) {
const dataModelName = variable.dataSource.split('.')[1];
const dataModelName = splitNTimes(variable.dataSource, '.', 1)[1];
const cleanPath = getKeyWithoutIndexIndicators(value);

const dataTypeToRead =
Expand Down
2 changes: 2 additions & 0 deletions src/language/texts/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export function en() {
clear_selection: 'Clear selection',
person_lookup_ssn: 'national ID number/D-number',
person_lookup_name: 'name',
organisation_lookup_orgnr: 'organisation number',
organisation_lookup_name: 'organisation name',
},
navigation: {
main: 'App navigation',
Expand Down
2 changes: 2 additions & 0 deletions src/language/texts/nb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export function nb(): FixedLanguageList {
clear_selection: 'Fjern alle valgte',
person_lookup_ssn: 'fødselsnummer',
person_lookup_name: 'navn',
organisation_lookup_orgnr: 'organisasjonsnummer',
organisation_lookup_name: 'organisasjonsnavn',
},
navigation: {
main: 'Appnavigasjon',
Expand Down
2 changes: 2 additions & 0 deletions src/language/texts/nn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export function nn(): FixedLanguageList {
clear_selection: 'Fjern alle valde',
person_lookup_ssn: 'fødselsnummer',
person_lookup_name: 'namn',
organisation_lookup_orgnr: 'organisasjonsnummer',
organisation_lookup_name: 'organisasjonsnamn',
},
navigation: {
main: 'Appnavigasjon',
Expand Down
13 changes: 13 additions & 0 deletions src/layout/Dropdown/DropdownComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,17 @@ describe('DropdownComponent', () => {
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByRole('listitem')).toHaveTextContent('Du må fylle ut land');
});

it('should render autocomplete prop if provided', async () => {
await render({
component: {
optionsId: 'countries',
autocomplete: 'name',
},
options: countries,
});

const inputComponent = screen.getByRole('combobox') as HTMLInputElement;
expect(inputComponent).toHaveAttribute('autocomplete', 'name');
});
});
3 changes: 2 additions & 1 deletion src/layout/Dropdown/DropdownComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type IDropdownProps = PropsFromGenericComponent<'Dropdown'>;
export function DropdownComponent({ node, overrideDisplay }: IDropdownProps) {
const item = useNodeItem(node);
const isValid = useIsValid(node);
const { id, readOnly, textResourceBindings, alertOnChange, grid, required } = item;
const { id, readOnly, textResourceBindings, alertOnChange, grid, required, autocomplete } = item;
const { langAsString, lang } = useLanguage(node);

const { labelText, getRequiredComponent, getOptionalComponent, getHelpTextComponent, getDescriptionComponent } =
Expand Down Expand Up @@ -94,6 +94,7 @@ export function DropdownComponent({ node, overrideDisplay }: IDropdownProps) {
label={overrideDisplay?.renderedInTable ? langAsString(textResourceBindings?.title) : undefined}
aria-label={overrideDisplay?.renderedInTable ? langAsString(textResourceBindings?.title) : undefined}
className={comboboxClasses.container}
autoComplete={autocomplete}
>
<Combobox.Empty>
<Lang id='form_filler.no_options_found' />
Expand Down
12 changes: 12 additions & 0 deletions src/layout/Dropdown/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { INPUT_AUTO_COMPLETE } from 'src/app-components/Input/constants';
import { CG } from 'src/codegen/CG';
import { AlertOnChangePlugin } from 'src/features/alertOnChange/AlertOnChangePlugin';
import { OptionsPlugin } from 'src/features/options/OptionsPlugin';
Expand Down Expand Up @@ -33,6 +34,17 @@ export const Config = new CG.component({
description: 'Boolean value indicating if the component should alert on change',
}),
)
.addProperty(
new CG.prop(
'autocomplete',
new CG.enum(...INPUT_AUTO_COMPLETE)
.optional()
.setTitle('Autocomplete')
.setDescription(
'The HTML autocomplete attribute helps browsers suggest or autofill input values based on the expected type of data.',
),
),
)
.addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple'))
.extends(CG.common('LabeledComponentProps'))
.extendTextResources(CG.common('TRBLabel'));
15 changes: 15 additions & 0 deletions src/layout/Input/InputComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@ describe('InputComponent', () => {
expect(inputComponent).toHaveValue(initialValue);
});

it('should render autocomplete prop if provided', async () => {
const initialValue = 'initial value';
await render({
component: {
autocomplete: 'name',
},
queries: {
fetchFormData: () => Promise.resolve({ some: { field: initialValue } }),
},
});

const inputComponent = screen.getByRole('textbox') as HTMLInputElement;
expect(inputComponent).toHaveAttribute('autocomplete', 'name');
});

const render = async ({ component, ...rest }: Partial<RenderGenericComponentTestProps<'Input'>> = {}) =>
await renderGenericComponentTest({
type: 'Input',
Expand Down
13 changes: 12 additions & 1 deletion src/layout/Input/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EXTERNAL_INPUT_TYPE } from 'src/app-components/Input/constants';
import { EXTERNAL_INPUT_TYPE, INPUT_AUTO_COMPLETE } from 'src/app-components/Input/constants';
import { CG } from 'src/codegen/CG';
import { CompCategory } from 'src/layout/common';

Expand Down Expand Up @@ -62,5 +62,16 @@ export const Config = new CG.component({
),
),
)
.addProperty(
new CG.prop(
'autocomplete',
new CG.enum(...INPUT_AUTO_COMPLETE)
.optional()
.setTitle('Autocomplete')
.setDescription(
'The HTML autocomplete attribute helps browsers suggest or autofill input values based on the expected type of data.',
),
),
)
.extends(CG.common('LabeledComponentProps'))
.extendTextResources(CG.common('TRBLabel'));
1 change: 1 addition & 0 deletions src/layout/Likert/Generator/LikertGeneratorChildren.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const GenerateRow = React.memo(function GenerateRow({ rowIndex, questionsBinding
hidden: parentItem.hidden,
pageBreak: parentItem.pageBreak,
renderAsSummary: parentItem.renderAsSummary,
columns: parentItem.columns,
}),
[parentItem, childId],
);
Expand Down
28 changes: 19 additions & 9 deletions src/layout/Likert/LikertComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const LikertComponent = ({ node }: LikertComponentProps) => {
const firstLikertNodeId = rowNodeIds[0];
const firstLikertNode = useNode(firstLikertNodeId) as LayoutNode<'LikertItem'> | undefined;
const { options: calculatedOptions, isFetching } = useNodeOptions(firstLikertNode);
const columns = useNodeItem(node, (item) => item.columns);

const id = node.id;

Expand Down Expand Up @@ -122,15 +123,24 @@ export const LikertComponent = ({ node }: LikertComponentProps) => {
<Lang id={textResourceBindings?.leftColumnHeader ?? 'likert.left_column_default_header_text'} />
</span>
</Table.HeaderCell>
{calculatedOptions.map((option, index) => (
<Table.HeaderCell
key={option.value}
scope='col'
id={`${id}-likert-columnheader-${index}`}
>
<Lang id={option.label} />
</Table.HeaderCell>
))}
{calculatedOptions.map((option, index) => {
const divider = columns?.find((column) => column.value == option.value)?.divider;

return (
<Table.HeaderCell
key={option.value}
scope='col'
id={`${id}-likert-columnheader-${index}`}
className={cn({
[classes.likertCellDividerStart]: divider === 'before',
[classes.likertCellDividerEnd]: divider === 'after',
[classes.likertCellDividerBoth]: divider === 'both',
})}
>
<Lang id={option.label} />
</Table.HeaderCell>
);
})}
</Table.Row>
</Table.Head>
<Table.Body>
Expand Down
1 change: 1 addition & 0 deletions src/layout/Likert/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ export const Config = new CG.component({
.exportAs('ILikertFilter'),
),
)
.extends(CG.common('ILikertColumnProperties'))
.addPlugin(new OptionsPlugin({ supportsPreselection: false, type: 'single' }));
Loading

0 comments on commit 70851c7

Please sign in to comment.