Skip to content
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,167 @@
import React, { PropsWithChildren } from 'react';

import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { defaultDataTypeMock } from 'src/__mocks__/getLayoutSetsMock';
import { useRegisterNodeNavigationHandler } from 'src/features/form/layout/NavigateToNode';
import { ALTINN_ROW_ID } from 'src/features/formData/types';
import { RepeatingGroupProvider } from 'src/layout/RepeatingGroup/Providers/RepeatingGroupContext';
import { RepeatingGroupTableSummary } from 'src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary';
import { renderWithNode } from 'src/test/renderWithProviders';
import type { ILayoutCollection } from 'src/layout/layout';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';

type NodeId = 'input1' | 'input2' | 'input3' | 'repeating-group';

describe('RepeatingGroupTableSummary', () => {
const layoutWithHidden = (hidden: NodeId[]): ILayoutCollection => ({
FormPage1: {
data: {
layout: [
{
id: 'repeating-group',
type: 'RepeatingGroup',
dataModelBindings: {
group: { dataType: defaultDataTypeMock, field: 'group' },
},
tableHeaders: ['input3'],
children: ['input1', 'input2', 'input3'],
maxCount: 3,
hidden: hidden.includes('repeating-group'),
},
{
id: 'input1',
type: 'Input',
dataModelBindings: {
simpleBinding: { dataType: defaultDataTypeMock, field: 'group.field1' },
},
textResourceBindings: {
title: 'Input 1',
},
hidden: hidden.includes('input1'),
},
{
id: 'input2',
type: 'Input',
dataModelBindings: {
simpleBinding: { dataType: defaultDataTypeMock, field: 'group.field2' },
},
textResourceBindings: {
title: 'Input 2',
},
hidden: hidden.includes('input2'),
},
{
id: 'input3',
type: 'Input',
dataModelBindings: {
simpleBinding: { dataType: defaultDataTypeMock, field: 'group.field3' },
},
textResourceBindings: {
title: 'Input 3',
},
hidden: hidden.includes('input3'),
},
],
},
},
FormPage2: {
data: {
layout: [
{
id: 'summary2',
type: 'Summary2',
target: {
type: 'component',
id: 'repeating-group',
},
overrides: [{ componentType: 'RepeatingGroup', display: 'table' }],
},
],
},
},
});

test('should focus the first input when clicking the edit button', async () => {
const user = userEvent.setup();
const navigate = jest.fn();
await render({ navigate });

// Find the edit button and click it
const editButton = screen.getByRole('button', { name: /endre/i });
await user.click(editButton);

// Expect the third input to be navigated to, as that is in tableHeaders
await waitFor(() => expect(navigate).toHaveBeenCalledWith('input3-0'));
});

test('should focus the next input when the first input is hidden', async () => {
const user = userEvent.setup();
const navigate = jest.fn();
await render({ navigate, layout: layoutWithHidden(['input3']) });

// Find the edit button and click it
const editButton = screen.getByRole('button', { name: /endre/i });
await user.click(editButton);

// Expect the next input (after input3) to be navigated to
await waitFor(() => expect(navigate).toHaveBeenCalledWith('input1-0'));
});

test('should focus the last input when the other two are hidden', async () => {
const user = userEvent.setup();
const navigate = jest.fn();
await render({ navigate, layout: layoutWithHidden(['input3', 'input1']) });

// Find the edit button and click it
const editButton = screen.getByRole('button', { name: /endre/i });
await user.click(editButton);

// Expect the last input to be navigated to
await waitFor(() => expect(navigate).toHaveBeenCalledWith('input2-0'));
});

test('should focus the repeating group itself when all inputs are hidden', async () => {
const user = userEvent.setup();
const navigate = jest.fn();
await render({ navigate, layout: layoutWithHidden(['input1', 'input2', 'input3']) });

// Find the edit button and click it
const editButton = screen.getByRole('button', { name: /endre/i });
await user.click(editButton);

// Expect the repeating group to be navigated to
await waitFor(() => expect(navigate).toHaveBeenCalledWith('repeating-group'));
});

function NavigationHook({ fn, children }: PropsWithChildren<{ fn: jest.Mock }>) {
useRegisterNodeNavigationHandler((node, _options) => fn(node.id));
return children;
}

type IRenderProps = {
navigate?: jest.Mock;
layout?: ILayoutCollection;
};

const render = async ({ navigate = jest.fn(), layout = layoutWithHidden([]) }: IRenderProps = {}) =>
await renderWithNode<true, LayoutNode<'RepeatingGroup'>>({
nodeId: 'repeating-group',
inInstance: true,
renderer: ({ node }) => (
<NavigationHook fn={navigate}>
<RepeatingGroupProvider node={node}>
<RepeatingGroupTableSummary componentNode={node} />
</RepeatingGroupProvider>
</NavigationHook>
),
initialPage: 'FormPage2',
queries: {
fetchLayouts: async () => layout,
fetchFormData: async () => ({
group: [{ field1: 'field1-row0', field2: 'field2-row0', field3: 'field3-row0', [ALTINN_ROW_ID]: 'abc123' }],
}),
},
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import classes from 'src/layout/RepeatingGroup/Summary2/RepeatingGroupSummary.mo
import tableClasses from 'src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.module.css';
import { RepeatingGroupTableTitle, useTableTitle } from 'src/layout/RepeatingGroup/Table/RepeatingGroupTableTitle';
import { useTableComponentIds } from 'src/layout/RepeatingGroup/useTableComponentIds';
import { EditButtonById } from 'src/layout/Summary2/CommonSummaryComponents/EditButton';
import { EditButtonFirstVisible } from 'src/layout/Summary2/CommonSummaryComponents/EditButton';
import { useReportSummaryRender } from 'src/layout/Summary2/isEmpty/EmptyChildrenContext';
import { ComponentSummaryById, SummaryContains } from 'src/layout/Summary2/SummaryComponent2/ComponentSummary';
import { useColumnStylesRepeatingGroups } from 'src/utils/formComponentUtils';
import { DataModelLocationProvider, useDataModelLocationForRow } from 'src/utils/layout/DataModelLocation';
import { useNode } from 'src/utils/layout/NodesContext';
import { useNodeItem } from 'src/utils/layout/useNodeItem';
import { useNodeDirectChildren, useNodeItem } from 'src/utils/layout/useNodeItem';
import type { ITableColumnFormatting } from 'src/layout/common.generated';
import type { RepGroupRow } from 'src/layout/RepeatingGroup/types';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
Expand Down Expand Up @@ -130,6 +130,7 @@ function DataRow({ row, node, pdfModeActive, columnSettings }: DataRowProps) {
const layoutLookups = useLayoutLookups();
const rawIds = useTableComponentIds(node);
const indexedIds = useIndexedComponentIds(rawIds);
const otherChildren = useNodeDirectChildren(node, row?.index)?.map((n) => n.id);
const dataModelBindings = useNodeItem(node, (i) => i.dataModelBindings);

if (!row) {
Expand Down Expand Up @@ -160,7 +161,7 @@ function DataRow({ row, node, pdfModeActive, columnSettings }: DataRowProps) {
align='right'
className={tableClasses.buttonCell}
>
{row?.itemIds && row?.itemIds?.length > 0 && indexedIds.length > 0 && <EditButtonById id={indexedIds[0]} />}
<EditButtonFirstVisible ids={[...indexedIds, ...otherChildren, node.id]} />
</Table.Cell>
)}
</Table.Row>
Expand Down
10 changes: 7 additions & 3 deletions src/layout/Summary2/CommonSummaryComponents/EditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useLanguage } from 'src/features/language/useLanguage';
import { usePdfModeActive } from 'src/features/pdf/PDFWrapper';
import { useIsMobile } from 'src/hooks/useDeviceWidths';
import { useCurrentView } from 'src/hooks/useNavigatePage';
import { useNode } from 'src/utils/layout/NodesContext';
import { Hidden, useNode } from 'src/utils/layout/NodesContext';
import { useNodeItem } from 'src/utils/layout/useNodeItem';
import type { NavigationResult } from 'src/features/form/layout/NavigateToNode';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
Expand All @@ -22,8 +22,12 @@ export type EditButtonProps = {
navigationOverride?: (() => Promise<NavigationResult> | void) | null;
} & React.HTMLAttributes<HTMLButtonElement>;

export function EditButtonById({ id, ...rest }: { id: string } & Omit<EditButtonProps, 'componentNode'>) {
const componentNode = useNode(id);
/**
* Render an edit button for the first visible (non-hidden) node in a list of possible IDs
*/
export function EditButtonFirstVisible({ ids, ...rest }: { ids: string[] } & Omit<EditButtonProps, 'componentNode'>) {
const nodeId = Hidden.useFirstVisibleNode(ids);
const componentNode = useNode(nodeId);
if (!componentNode) {
return null;
}
Expand Down
14 changes: 14 additions & 0 deletions src/utils/layout/NodesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,20 @@ export const Hidden = {

return orderWithHidden.includes(pageKey);
},

/**
* Iterate through a list of node IDs and find the first one that is not hidden
*/
useFirstVisibleNode(nodeIds: string[]): string | undefined {
return WhenReady.useSelector((state) => {
for (const id of nodeIds) {
if (!isHidden(state, 'node', id)) {
return id;
}
}
return undefined;
});
},
};

export type NodeDataSelector = ReturnType<typeof NodesInternal.useNodeDataSelector>;
Expand Down
Loading