diff --git a/src/layout/List/ListComponent.test.tsx b/src/layout/List/ListComponent.test.tsx index 02c1853cfc..f1d1f3f041 100644 --- a/src/layout/List/ListComponent.test.tsx +++ b/src/layout/List/ListComponent.test.tsx @@ -211,7 +211,7 @@ describe('ListComponent', () => { await waitFor(() => expect(screen.getAllByRole('radio')).toHaveLength(6)); expect(screen.queryByRole('radio', { checked: true })).not.toBeInTheDocument(); - // Select the second row + // Select the second row - find by value since label is empty const swedishRow = screen.getByRole('radio', { name: /sweden/i }); await user.click(swedishRow); @@ -224,4 +224,70 @@ describe('ListComponent', () => { ], }); }); + + describe('auto-readonly mode (no dataModelBindings)', () => { + it('should not render radio buttons when no dataModelBindings exist', async () => { + await render({ component: { dataModelBindings: undefined } }); + + // Wait for the data to load + await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument()); + + // No radio buttons should be present + expect(screen.queryByRole('radio')).not.toBeInTheDocument(); + }); + + it('should not render radio buttons when dataModelBindings is empty object', async () => { + await render({ component: { dataModelBindings: {} } }); + + // Wait for the data to load + await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument()); + + // No radio buttons should be present + expect(screen.queryByRole('radio')).not.toBeInTheDocument(); + }); + + it('should not allow row selection when no dataModelBindings exist', async () => { + const user = userEvent.setup({ delay: null }); + const { formDataMethods } = await render({ component: { dataModelBindings: undefined } }); + + // Wait for the data to load + await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument()); + + // Try to click a row + const norwegianRow = screen.getByRole('row', { name: /norway/i }); + await user.click(norwegianRow); + + // No data should be saved + expect(formDataMethods.setMultiLeafValues).not.toHaveBeenCalled(); + }); + + it('should not render controls in mobile view when no dataModelBindings exist', async () => { + jest.spyOn(useDeviceWidths, 'useIsMobile').mockReturnValue(true); + + await render({ + component: { + dataModelBindings: undefined, + tableHeadersMobile: ['Name', 'FlagLink'], + }, + }); + + // Wait for the data to load + await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument()); + + // No radio buttons should be present + expect(screen.queryByRole('radio')).not.toBeInTheDocument(); + }); + + it('should still display table data when no dataModelBindings exist', async () => { + await render({ component: { dataModelBindings: undefined } }); + + // All data should still be visible + await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument()); + expect(screen.getByText('Sweden')).toBeInTheDocument(); + expect(screen.getByText('Denmark')).toBeInTheDocument(); + expect(screen.getByText('Germany')).toBeInTheDocument(); + expect(screen.getByText('Spain')).toBeInTheDocument(); + expect(screen.getByText('France')).toBeInTheDocument(); + }); + }); }); diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index d780400a42..a5e1a163d3 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -34,6 +34,17 @@ import type { PropsFromGenericComponent } from 'src/layout'; import type { IDataModelBindingsForList } from 'src/layout/List/config.generated'; type Row = Record; +type SelectionMode = 'readonly' | 'single' | 'multiple'; + +function getSelectionMode(bindings: IDataModelBindingsForList): SelectionMode { + const hasValidBindings = Object.keys(bindings).length > 0 && Object.values(bindings).some((b) => b !== undefined); + + if (!hasValidBindings) { + return 'readonly'; + } + + return bindings.group ? 'multiple' : 'single'; +} export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'List'>) => { const isMobile = useIsMobile(); @@ -66,14 +77,19 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li const { data } = useDataListQuery(filter, dataListId, secure, mapping, queryParameters); const bindings = item.dataModelBindings ?? ({} as IDataModelBindingsForList); + // Determine selection mode based on bindings + const selectionMode = getSelectionMode(bindings); + const readOnly = selectionMode === 'readonly'; + const isMultipleSelection = selectionMode === 'multiple'; + const { formData, setValues } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); - const { toggle, isChecked, enabled } = useSaveObjectToGroup(bindings); + const { toggle, isChecked } = useSaveObjectToGroup(bindings); const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter( (key) => !tableHeadersMobile || tableHeadersMobile.includes(key), ); - const selectedRow = !enabled + const selectedRow = !isMultipleSelection ? (data?.listItems.find((row) => Object.keys(formData).every((key) => row[key] === formData[key])) ?? '') : ''; @@ -86,7 +102,7 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li } function isRowSelected(row: Row): boolean { - if (enabled) { + if (isMultipleSelection) { return isChecked(row); } return JSON.stringify(selectedRow) === JSON.stringify(row); @@ -96,7 +112,11 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li const description = item.textResourceBindings?.description; const handleRowClick = (row: Row) => { - if (enabled) { + if (readOnly) { + return; + } + + if (isMultipleSelection) { toggle(row); } else { handleSelectedRadioRow({ selectedValue: row }); @@ -151,10 +171,10 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li required, }); - if (isMobile) { + if (isMobile && !readOnly) { return ( - {enabled ? ( + {isMultipleSelection ? (
{description && ( @@ -170,17 +190,19 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li - {data?.listItems.map((row) => ( - handleRowClick(row)} - value={JSON.stringify(row)} - checked={isChecked(row)} - label={renderListItems(row, tableHeaders)} - /> - ))} +
+ {data?.listItems.map((row, idx) => ( + handleRowClick(row)} + value={JSON.stringify(row)} + checked={isChecked(row)} + label={renderListItems(row, tableHeaders)} + /> + ))} +
) : (
@@ -199,9 +221,9 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li )} - {data?.listItems.map((row) => ( + {data?.listItems.map((row, idx) => ( - - - - - + {!readOnly && ( + + + + + + )} {Object.entries(tableHeaders).map(([key, value]) => { const isSortable = sortableColumns?.includes(key); let sort: AriaAttributes['aria-sort'] = undefined; @@ -273,41 +297,44 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li {data?.listItems.map((row) => ( handleRowClick(row)} + onClick={!readOnly ? () => handleRowClick(row) : undefined} + className={cn({ [classes.readOnlyRow]: readOnly })} > - - {enabled ? ( - {getRowLabel(row)}} - onChange={() => {}} - value={JSON.stringify(row)} - checked={isChecked(row)} - name={indexedId} - /> - ) : ( - { - handleSelectedRadioRow({ selectedValue: row }); - }} - value={JSON.stringify(row)} - checked={isRowSelected(row)} - name={indexedId} - /> - )} - + {!readOnly && ( + + {isMultipleSelection ? ( + {getRowLabel(row)}} + onChange={() => toggle(row)} + onClick={(e) => e.stopPropagation()} + value={JSON.stringify(row)} + checked={isChecked(row)} + name={indexedId} + /> + ) : ( + handleSelectedRadioRow({ selectedValue: row })} + onClick={(e) => e.stopPropagation()} + value={JSON.stringify(row)} + checked={isRowSelected(row)} + name={indexedId} + /> + )} + + )} {Object.keys(tableHeaders).map((key) => ( {typeof row[key] === 'string' ? : row[key]}