Skip to content

Commit 7fe5c99

Browse files
authored
Fix/3646 read only list (#3647)
* using lack of datamodelbindings to show readonly mode in List component
1 parent a0bb9b4 commit 7fe5c99

File tree

2 files changed

+148
-55
lines changed

2 files changed

+148
-55
lines changed

src/layout/List/ListComponent.test.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ describe('ListComponent', () => {
211211
await waitFor(() => expect(screen.getAllByRole('radio')).toHaveLength(6));
212212
expect(screen.queryByRole('radio', { checked: true })).not.toBeInTheDocument();
213213

214-
// Select the second row
214+
// Select the second row - find by value since label is empty
215215
const swedishRow = screen.getByRole('radio', { name: /sweden/i });
216216
await user.click(swedishRow);
217217

@@ -224,4 +224,70 @@ describe('ListComponent', () => {
224224
],
225225
});
226226
});
227+
228+
describe('auto-readonly mode (no dataModelBindings)', () => {
229+
it('should not render radio buttons when no dataModelBindings exist', async () => {
230+
await render({ component: { dataModelBindings: undefined } });
231+
232+
// Wait for the data to load
233+
await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument());
234+
235+
// No radio buttons should be present
236+
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
237+
});
238+
239+
it('should not render radio buttons when dataModelBindings is empty object', async () => {
240+
await render({ component: { dataModelBindings: {} } });
241+
242+
// Wait for the data to load
243+
await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument());
244+
245+
// No radio buttons should be present
246+
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
247+
});
248+
249+
it('should not allow row selection when no dataModelBindings exist', async () => {
250+
const user = userEvent.setup({ delay: null });
251+
const { formDataMethods } = await render({ component: { dataModelBindings: undefined } });
252+
253+
// Wait for the data to load
254+
await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument());
255+
256+
// Try to click a row
257+
const norwegianRow = screen.getByRole('row', { name: /norway/i });
258+
await user.click(norwegianRow);
259+
260+
// No data should be saved
261+
expect(formDataMethods.setMultiLeafValues).not.toHaveBeenCalled();
262+
});
263+
264+
it('should not render controls in mobile view when no dataModelBindings exist', async () => {
265+
jest.spyOn(useDeviceWidths, 'useIsMobile').mockReturnValue(true);
266+
267+
await render({
268+
component: {
269+
dataModelBindings: undefined,
270+
tableHeadersMobile: ['Name', 'FlagLink'],
271+
},
272+
});
273+
274+
// Wait for the data to load
275+
await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument());
276+
277+
// No radio buttons should be present
278+
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
279+
});
280+
281+
it('should still display table data when no dataModelBindings exist', async () => {
282+
await render({ component: { dataModelBindings: undefined } });
283+
284+
// All data should still be visible
285+
await waitFor(() => expect(screen.getByText('Norway')).toBeInTheDocument());
286+
expect(screen.getByText('Sweden')).toBeInTheDocument();
287+
expect(screen.getByText('Denmark')).toBeInTheDocument();
288+
expect(screen.getByText('Germany')).toBeInTheDocument();
289+
expect(screen.getByText('Spain')).toBeInTheDocument();
290+
expect(screen.getByText('France')).toBeInTheDocument();
291+
});
292+
});
227293
});

src/layout/List/ListComponent.tsx

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ import type { PropsFromGenericComponent } from 'src/layout';
3434
import type { IDataModelBindingsForList } from 'src/layout/List/config.generated';
3535

3636
type Row = Record<string, string | number | boolean>;
37+
type SelectionMode = 'readonly' | 'single' | 'multiple';
38+
39+
function getSelectionMode(bindings: IDataModelBindingsForList): SelectionMode {
40+
const hasValidBindings = Object.keys(bindings).length > 0 && Object.values(bindings).some((b) => b !== undefined);
41+
42+
if (!hasValidBindings) {
43+
return 'readonly';
44+
}
45+
46+
return bindings.group ? 'multiple' : 'single';
47+
}
3748

3849
export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'List'>) => {
3950
const isMobile = useIsMobile();
@@ -66,14 +77,19 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
6677
const { data } = useDataListQuery(filter, dataListId, secure, mapping, queryParameters);
6778
const bindings = item.dataModelBindings ?? ({} as IDataModelBindingsForList);
6879

80+
// Determine selection mode based on bindings
81+
const selectionMode = getSelectionMode(bindings);
82+
const readOnly = selectionMode === 'readonly';
83+
const isMultipleSelection = selectionMode === 'multiple';
84+
6985
const { formData, setValues } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw');
70-
const { toggle, isChecked, enabled } = useSaveObjectToGroup(bindings);
86+
const { toggle, isChecked } = useSaveObjectToGroup(bindings);
7187

7288
const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter(
7389
(key) => !tableHeadersMobile || tableHeadersMobile.includes(key),
7490
);
7591

76-
const selectedRow = !enabled
92+
const selectedRow = !isMultipleSelection
7793
? (data?.listItems.find((row) => Object.keys(formData).every((key) => row[key] === formData[key])) ?? '')
7894
: '';
7995

@@ -86,7 +102,7 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
86102
}
87103

88104
function isRowSelected(row: Row): boolean {
89-
if (enabled) {
105+
if (isMultipleSelection) {
90106
return isChecked(row);
91107
}
92108
return JSON.stringify(selectedRow) === JSON.stringify(row);
@@ -96,7 +112,11 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
96112
const description = item.textResourceBindings?.description;
97113

98114
const handleRowClick = (row: Row) => {
99-
if (enabled) {
115+
if (readOnly) {
116+
return;
117+
}
118+
119+
if (isMultipleSelection) {
100120
toggle(row);
101121
} else {
102122
handleSelectedRadioRow({ selectedValue: row });
@@ -151,10 +171,10 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
151171
required,
152172
});
153173

154-
if (isMobile) {
174+
if (isMobile && !readOnly) {
155175
return (
156176
<ComponentStructureWrapper baseComponentId={baseComponentId}>
157-
{enabled ? (
177+
{isMultipleSelection ? (
158178
<Fieldset>
159179
<Fieldset.Legend>
160180
{description && (
@@ -170,17 +190,19 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
170190
<RequiredIndicator required={required} />
171191
</Heading>
172192
</Fieldset.Legend>
173-
{data?.listItems.map((row) => (
174-
<Checkbox
175-
key={JSON.stringify(row)}
176-
className={cn(classes.mobile)}
177-
{...getCheckboxProps({ value: JSON.stringify(row) })}
178-
onClick={() => handleRowClick(row)}
179-
value={JSON.stringify(row)}
180-
checked={isChecked(row)}
181-
label={renderListItems(row, tableHeaders)}
182-
/>
183-
))}
193+
<div>
194+
{data?.listItems.map((row, idx) => (
195+
<Checkbox
196+
key={idx}
197+
className={cn(classes.mobile)}
198+
{...getCheckboxProps({ value: JSON.stringify(row) })}
199+
onClick={() => handleRowClick(row)}
200+
value={JSON.stringify(row)}
201+
checked={isChecked(row)}
202+
label={renderListItems(row, tableHeaders)}
203+
/>
204+
))}
205+
</div>
184206
</Fieldset>
185207
) : (
186208
<Fieldset className={classes.mobileGroup}>
@@ -199,9 +221,9 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
199221
</Fieldset.Description>
200222
)}
201223

202-
{data?.listItems.map((row) => (
224+
{data?.listItems.map((row, idx) => (
203225
<Radio
204-
key={JSON.stringify(row)}
226+
key={idx}
205227
{...getRadioProps({ value: JSON.stringify(row) })}
206228
value={JSON.stringify(row)}
207229
className={cn(classes.mobile, { [classes.selectedRow]: isRowSelected(row) })}
@@ -246,11 +268,13 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
246268
)}
247269
<Table.Head>
248270
<Table.Row>
249-
<Table.HeaderCell>
250-
<span className={utilClasses.visuallyHidden}>
251-
<Lang id='list_component.controlsHeader' />
252-
</span>
253-
</Table.HeaderCell>
271+
{!readOnly && (
272+
<Table.HeaderCell>
273+
<span className={utilClasses.visuallyHidden}>
274+
<Lang id='list_component.controlsHeader' />
275+
</span>
276+
</Table.HeaderCell>
277+
)}
254278
{Object.entries(tableHeaders).map(([key, value]) => {
255279
const isSortable = sortableColumns?.includes(key);
256280
let sort: AriaAttributes['aria-sort'] = undefined;
@@ -273,41 +297,44 @@ export const ListComponent = ({ baseComponentId }: PropsFromGenericComponent<'Li
273297
{data?.listItems.map((row) => (
274298
<Table.Row
275299
key={JSON.stringify(row)}
276-
onClick={() => handleRowClick(row)}
300+
onClick={!readOnly ? () => handleRowClick(row) : undefined}
301+
className={cn({ [classes.readOnlyRow]: readOnly })}
277302
>
278-
<Table.Cell
279-
className={cn({
280-
[classes.selectedRowCell]: isRowSelected(row),
281-
})}
282-
>
283-
{enabled ? (
284-
<Checkbox
285-
className={classes.toggleControl}
286-
label={<span className='sr-only'>{getRowLabel(row)}</span>}
287-
onChange={() => {}}
288-
value={JSON.stringify(row)}
289-
checked={isChecked(row)}
290-
name={indexedId}
291-
/>
292-
) : (
293-
<RadioButton
294-
className={classes.toggleControl}
295-
label={getRowLabel(row)}
296-
hideLabel
297-
onChange={() => {
298-
handleSelectedRadioRow({ selectedValue: row });
299-
}}
300-
value={JSON.stringify(row)}
301-
checked={isRowSelected(row)}
302-
name={indexedId}
303-
/>
304-
)}
305-
</Table.Cell>
303+
{!readOnly && (
304+
<Table.Cell
305+
className={cn({
306+
[classes.selectedRowCell]: isRowSelected(row) && !readOnly,
307+
})}
308+
>
309+
{isMultipleSelection ? (
310+
<Checkbox
311+
className={classes.toggleControl}
312+
label={<span className='sr-only'>{getRowLabel(row)}</span>}
313+
onChange={() => toggle(row)}
314+
onClick={(e) => e.stopPropagation()}
315+
value={JSON.stringify(row)}
316+
checked={isChecked(row)}
317+
name={indexedId}
318+
/>
319+
) : (
320+
<RadioButton
321+
className={classes.toggleControl}
322+
label={getRowLabel(row)}
323+
hideLabel
324+
onChange={() => handleSelectedRadioRow({ selectedValue: row })}
325+
onClick={(e) => e.stopPropagation()}
326+
value={JSON.stringify(row)}
327+
checked={isRowSelected(row)}
328+
name={indexedId}
329+
/>
330+
)}
331+
</Table.Cell>
332+
)}
306333
{Object.keys(tableHeaders).map((key) => (
307334
<Table.Cell
308335
key={key}
309336
className={cn({
310-
[classes.selectedRowCell]: isRowSelected(row),
337+
[classes.selectedRowCell]: isRowSelected(row) && !readOnly,
311338
})}
312339
>
313340
{typeof row[key] === 'string' ? <Lang id={row[key]} /> : row[key]}

0 commit comments

Comments
 (0)