@navikt/ds-react 8.10.1 → 8.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/cjs/data/table/column-header/DataTableColumnHeader.d.ts +1 -1
  2. package/cjs/data/table/column-header/DataTableColumnHeader.js +13 -10
  3. package/cjs/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  4. package/cjs/data/table/column-header/useTableColumnResize.d.ts +5 -3
  5. package/cjs/data/table/column-header/useTableColumnResize.js +128 -53
  6. package/cjs/data/table/column-header/useTableColumnResize.js.map +1 -1
  7. package/cjs/data/table/helpers/collectTableRowEntries.d.ts +16 -0
  8. package/cjs/data/table/helpers/collectTableRowEntries.js +27 -0
  9. package/cjs/data/table/helpers/collectTableRowEntries.js.map +1 -0
  10. package/cjs/data/table/helpers/table-keyboard.js +0 -3
  11. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  12. package/cjs/data/table/hooks/useTableExpansion.d.ts +9 -6
  13. package/cjs/data/table/hooks/useTableExpansion.js +36 -15
  14. package/cjs/data/table/hooks/useTableExpansion.js.map +1 -1
  15. package/cjs/data/table/hooks/useTableItems.d.ts +29 -0
  16. package/cjs/data/table/hooks/useTableItems.js +63 -0
  17. package/cjs/data/table/hooks/useTableItems.js.map +1 -0
  18. package/cjs/data/table/hooks/useTableKeyboardNav.js +3 -3
  19. package/cjs/data/table/hooks/useTableKeyboardNav.js.map +1 -1
  20. package/cjs/data/table/root/DataTableAuto.d.ts +18 -0
  21. package/cjs/data/table/root/DataTableAuto.js +71 -29
  22. package/cjs/data/table/root/DataTableAuto.js.map +1 -1
  23. package/cjs/data/table/root/DataTableRoot.context.d.ts +5 -3
  24. package/cjs/data/table/root/DataTableRoot.context.js.map +1 -1
  25. package/cjs/data/table/root/DataTableRoot.js +7 -4
  26. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  27. package/cjs/data/table/tr/DataTableTr.js +30 -32
  28. package/cjs/data/table/tr/DataTableTr.js.map +1 -1
  29. package/esm/data/table/column-header/DataTableColumnHeader.d.ts +1 -1
  30. package/esm/data/table/column-header/DataTableColumnHeader.js +14 -11
  31. package/esm/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  32. package/esm/data/table/column-header/useTableColumnResize.d.ts +5 -3
  33. package/esm/data/table/column-header/useTableColumnResize.js +129 -54
  34. package/esm/data/table/column-header/useTableColumnResize.js.map +1 -1
  35. package/esm/data/table/helpers/collectTableRowEntries.d.ts +16 -0
  36. package/esm/data/table/helpers/collectTableRowEntries.js +25 -0
  37. package/esm/data/table/helpers/collectTableRowEntries.js.map +1 -0
  38. package/esm/data/table/helpers/table-keyboard.js +0 -3
  39. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  40. package/esm/data/table/hooks/useTableExpansion.d.ts +9 -6
  41. package/esm/data/table/hooks/useTableExpansion.js +36 -16
  42. package/esm/data/table/hooks/useTableExpansion.js.map +1 -1
  43. package/esm/data/table/hooks/useTableItems.d.ts +29 -0
  44. package/esm/data/table/hooks/useTableItems.js +58 -0
  45. package/esm/data/table/hooks/useTableItems.js.map +1 -0
  46. package/esm/data/table/hooks/useTableKeyboardNav.js +3 -3
  47. package/esm/data/table/hooks/useTableKeyboardNav.js.map +1 -1
  48. package/esm/data/table/root/DataTableAuto.d.ts +18 -0
  49. package/esm/data/table/root/DataTableAuto.js +72 -30
  50. package/esm/data/table/root/DataTableAuto.js.map +1 -1
  51. package/esm/data/table/root/DataTableRoot.context.d.ts +5 -3
  52. package/esm/data/table/root/DataTableRoot.context.js.map +1 -1
  53. package/esm/data/table/root/DataTableRoot.js +7 -4
  54. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  55. package/esm/data/table/tr/DataTableTr.js +32 -34
  56. package/esm/data/table/tr/DataTableTr.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/data/table/column-header/DataTableColumnHeader.tsx +21 -13
  59. package/src/data/table/column-header/useTableColumnResize.ts +152 -79
  60. package/src/data/table/helpers/collectTableRowEntries.ts +58 -0
  61. package/src/data/table/helpers/table-keyboard.ts +0 -4
  62. package/src/data/table/hooks/__tests__/useTableExpansion.test.tsx +115 -0
  63. package/src/data/table/hooks/__tests__/useTableItems.test.ts +131 -0
  64. package/src/data/table/hooks/useTableExpansion.tsx +63 -22
  65. package/src/data/table/hooks/useTableItems.ts +123 -0
  66. package/src/data/table/hooks/useTableKeyboardNav.ts +3 -3
  67. package/src/data/table/root/DataTableAuto.test.tsx +118 -0
  68. package/src/data/table/root/DataTableAuto.tsx +159 -49
  69. package/src/data/table/root/DataTableRoot.context.ts +4 -2
  70. package/src/data/table/root/DataTableRoot.tsx +20 -13
  71. package/src/data/table/tr/DataTableTr.tsx +48 -47
@@ -1,5 +1,7 @@
1
1
  /** biome-ignore-all lint/correctness/useHookAtTopLevel: False positive because of the way forwardRef() is added */
2
2
  import React, { forwardRef, useMemo } from "react";
3
+ import { ChevronDownIcon, ChevronRightIcon } from "@navikt/aksel-icons";
4
+ import { Button } from "../../../button";
3
5
  import { Skeleton } from "../../../skeleton";
4
6
  import { useId } from "../../../utils-external";
5
7
  import { cl } from "../../../utils/helpers";
@@ -7,12 +9,18 @@ import { useMergeRefs } from "../../../utils/hooks";
7
9
  import { DataTableBaseCell } from "../base-cell/DataTableBaseCell";
8
10
  import { DataTableColumnHeader } from "../column-header/DataTableColumnHeader";
9
11
  import { DataTableEmptyState } from "../empty-state/DataTableEmptyState";
10
- import type { UseColumnOptionsResult } from "../hooks/useColumnOptions";
11
12
  import { useColumnOptions } from "../hooks/useColumnOptions";
12
13
  import {
13
14
  DataTableExpansionProvider,
15
+ getDataTableExpansionId,
14
16
  useDataTableExpansion,
15
17
  } from "../hooks/useTableExpansion";
18
+ import {
19
+ type ItemDetail,
20
+ TableItemsProvider,
21
+ useTableItems,
22
+ useTableItemsContext,
23
+ } from "../hooks/useTableItems";
16
24
  import { useTableKeyboardNav } from "../hooks/useTableKeyboardNav";
17
25
  import {
18
26
  type SelectionProps,
@@ -157,6 +165,11 @@ interface DataTableProps<T>
157
165
  * When provided, an expand toggle column is added automatically.
158
166
  */
159
167
  getDetailsPanelContent?: (rowData: T) => React.ReactNode;
168
+ /**
169
+ * Determines whether a row can be expanded to show details panel content.
170
+ * @default () => true
171
+ */
172
+ isDetailsPanelExpandable?: (rowData: T) => boolean;
160
173
  /**
161
174
  * Controlled list of expanded row IDs.
162
175
  * Use with `onDetailsPanelChange` for controlled usage, or `defaultDetailsPanelRowIds` for uncontrolled.
@@ -169,6 +182,10 @@ interface DataTableProps<T>
169
182
  defaultDetailsPanelRowIds?: (string | number)[];
170
183
  /**
171
184
  * Called when the list of expanded row IDs changes.
185
+ *
186
+ *
187
+ * TODO:
188
+ * - Docs: This pattern is called "Master / Detail" in general terms
172
189
  */
173
190
  onDetailsPanelChange?: (ids: (string | number)[]) => void;
174
191
  /**
@@ -182,6 +199,15 @@ interface DataTableProps<T>
182
199
  * @default false
183
200
  */
184
201
  showExpandAll?: boolean;
202
+ /**
203
+ * Function to get sub-rows for a given row, used for nested rows.
204
+ * When provided, an expand toggle column is added automatically.
205
+ */
206
+ getSubRows?: (rowData: T) => T[];
207
+ expandedSubRowIds?: (string | number)[];
208
+ defaultExpandedSubRowIds?: (string | number)[];
209
+ isSubRowExpandable?: (rowData: T) => boolean;
210
+ onExpandedSubRowIdsChange?: (ids: (string | number)[]) => void;
185
211
  }
186
212
 
187
213
  function DataTableAutoInner<T>(
@@ -215,11 +241,17 @@ function DataTableAutoInner<T>(
215
241
  loadingLabel = "Laster innhold",
216
242
  disableRowSelectionOnClick = false,
217
243
  getDetailsPanelContent,
244
+ isDetailsPanelExpandable,
218
245
  getDetailsPanelHeight,
219
246
  showExpandAll = false,
220
247
  detailsPanelRowIds,
221
248
  defaultDetailsPanelRowIds,
222
249
  onDetailsPanelChange,
250
+ getSubRows,
251
+ expandedSubRowIds,
252
+ defaultExpandedSubRowIds,
253
+ isSubRowExpandable,
254
+ onExpandedSubRowIdsChange,
223
255
  ...rest
224
256
  }: DataTableProps<T>,
225
257
  forwardedRef: React.ForwardedRef<HTMLTableElement>,
@@ -237,12 +269,25 @@ function DataTableAutoInner<T>(
237
269
 
238
270
  const mergedRef = useMergeRefs(forwardedRef, setTableRef);
239
271
 
272
+ const tableItems = useTableItems({
273
+ items: data,
274
+ getRowId,
275
+ getSubRows,
276
+ expandedSubRowIds,
277
+ defaultExpandedSubRowIds,
278
+ isSubRowExpandable,
279
+ onExpandedSubRowIdsChange,
280
+ });
281
+
240
282
  const allRowKeys = useMemo(() => {
241
- const resolvedGetRowId = (item: T, index: number): string | number =>
242
- getRowId?.(item, index) ?? index;
283
+ const rowKeys: (string | number)[] = [];
284
+
285
+ for (const details of tableItems.itemDetails.values()) {
286
+ rowKeys.push(details.id);
287
+ }
243
288
 
244
- return data.map((item, index) => resolvedGetRowId(item, index));
245
- }, [data, getRowId]);
289
+ return rowKeys;
290
+ }, [tableItems.itemDetails]);
246
291
 
247
292
  const tableSelectionState = useTableSelection({
248
293
  selectionMode: selectionModeProp,
@@ -258,6 +303,12 @@ function DataTableAutoInner<T>(
258
303
  selectionMode: tableSelectionState.selection.selectionMode,
259
304
  });
260
305
 
306
+ const fullWidthColSpan =
307
+ columns.length +
308
+ (layout === "fixed" ? 1 : 0) +
309
+ (tableSelectionState.selection.selectionMode !== "none" ? 1 : 0) +
310
+ (getDetailsPanelContent ? 1 : 0);
311
+
261
312
  const tableId = useId(id);
262
313
 
263
314
  return (
@@ -273,13 +324,15 @@ function DataTableAutoInner<T>(
273
324
  disableRowSelectionOnClick={disableRowSelectionOnClick}
274
325
  isLoading={isLoading}
275
326
  showLoadingOverlay={isLoading && !loadingState && !loadingRows}
327
+ columns={columns}
276
328
  >
277
329
  <DataTableExpansionProvider
278
330
  detailsPanelRowIds={detailsPanelRowIds}
279
331
  defaultDetailsPanelRowIds={defaultDetailsPanelRowIds}
280
332
  onDetailsPanelChange={onDetailsPanelChange}
281
- allRowKeys={allRowKeys}
333
+ itemDetails={tableItems.itemDetails}
282
334
  getDetailsPanelContent={getDetailsPanelContent}
335
+ isDetailsPanelExpandable={isDetailsPanelExpandable}
283
336
  getDetailsPanelHeight={getDetailsPanelHeight}
284
337
  showExpandAll={showExpandAll}
285
338
  >
@@ -323,17 +376,22 @@ function DataTableAutoInner<T>(
323
376
  })}
324
377
  </DataTableTr>
325
378
  </DataTableThead>
326
- <DataTableTbody>
327
- <DataTableAutoTBodyContent
328
- columns={columns}
329
- data={data}
330
- allRowKeys={allRowKeys}
331
- loadingState={loadingState}
332
- loadingRows={loadingRows}
333
- loadingLabel={loadingLabel}
334
- emptyState={emptyState}
335
- />
336
- </DataTableTbody>
379
+ <TableItemsProvider
380
+ itemDetails={tableItems.itemDetails}
381
+ items={tableItems.items}
382
+ onExpandedSubRowIdsChange={tableItems.onExpandedSubRowIdsChange}
383
+ isSubRowExpanded={tableItems.isSubRowExpanded}
384
+ >
385
+ <DataTableTbody>
386
+ <DataTableAutoTBodyContent
387
+ loadingState={loadingState}
388
+ loadingRows={loadingRows}
389
+ loadingLabel={loadingLabel}
390
+ emptyState={emptyState}
391
+ fullWidthColSpan={fullWidthColSpan}
392
+ />
393
+ </DataTableTbody>
394
+ </TableItemsProvider>
337
395
  </table>
338
396
  </div>
339
397
  </div>
@@ -342,30 +400,27 @@ function DataTableAutoInner<T>(
342
400
  );
343
401
  }
344
402
 
345
- interface DataTableAutoTBodyContentProps<T> {
346
- columns: UseColumnOptionsResult<T>["columns"];
347
- data: T[];
348
- allRowKeys: (string | number)[];
403
+ interface DataTableAutoTBodyContentProps {
349
404
  loadingState: React.ReactNode;
350
405
  loadingLabel: string;
351
406
  loadingRows?: number;
352
407
  emptyState: React.ReactNode;
408
+ fullWidthColSpan: number;
353
409
  }
354
410
 
355
- function DataTableAutoTBodyContent<T>({
356
- columns,
357
- data,
358
- allRowKeys,
411
+ function DataTableAutoTBodyContent({
359
412
  loadingState,
360
413
  loadingRows,
361
414
  loadingLabel,
362
415
  emptyState,
363
- }: DataTableAutoTBodyContentProps<T>) {
364
- const { isLoading } = useDataTableContext();
416
+ fullWidthColSpan,
417
+ }: DataTableAutoTBodyContentProps) {
418
+ const { items, itemDetails } = useTableItemsContext();
419
+ const { columns, isLoading } = useDataTableContext();
365
420
 
366
421
  if (isLoading && loadingState != null) {
367
422
  return (
368
- <DataTableLoadingState colSpan={columns.length}>
423
+ <DataTableLoadingState colSpan={fullWidthColSpan}>
369
424
  {loadingState}
370
425
  </DataTableLoadingState>
371
426
  );
@@ -375,7 +430,7 @@ function DataTableAutoTBodyContent<T>({
375
430
  return (
376
431
  <>
377
432
  <tr>
378
- <td colSpan={columns.length} className="aksel-sr-only">
433
+ <td colSpan={fullWidthColSpan} className="aksel-sr-only">
379
434
  {loadingLabel}
380
435
  </td>
381
436
  </tr>
@@ -397,29 +452,45 @@ function DataTableAutoTBodyContent<T>({
397
452
  );
398
453
  }
399
454
 
400
- if (data.length === 0 && emptyState !== undefined) {
455
+ if (items.length === 0 && emptyState !== undefined) {
401
456
  return (
402
- <DataTableEmptyState colSpan={columns.length}>
457
+ <DataTableEmptyState colSpan={fullWidthColSpan}>
403
458
  {emptyState}
404
459
  </DataTableEmptyState>
405
460
  );
406
461
  }
407
462
 
408
- const renderLoadingAnnoucement = isLoading && !loadingState && !loadingRows;
463
+ const renderLoadingAnnouncement = isLoading && !loadingState && !loadingRows;
464
+
465
+ return items.map((rowData) => {
466
+ const details = itemDetails.get(rowData);
467
+
468
+ /* Should in theory be impossible. Look about typing this? */
469
+ if (!details) {
470
+ return null;
471
+ }
472
+
473
+ const hasSubRows = details.children.length > 0;
409
474
 
410
- return data.map((rowData, rowIndex) => {
411
- const rowId = allRowKeys[rowIndex];
412
475
  return (
413
- <React.Fragment key={rowId}>
414
- {renderLoadingAnnoucement && (
476
+ <React.Fragment key={details.id}>
477
+ {renderLoadingAnnouncement && (
415
478
  <tr>
416
- <td colSpan={columns.length} className="aksel-sr-only">
479
+ <td colSpan={fullWidthColSpan} className="aksel-sr-only">
417
480
  {loadingLabel}
418
481
  </td>
419
482
  </tr>
420
483
  )}
421
- <DataTableTr rowId={rowId}>
484
+ <DataTableTr rowId={details.id}>
422
485
  {columns.map(({ isSticky, colDef }, colDefIndex) => {
486
+ const renderNestedToggle = colDefIndex === 0 && hasSubRows;
487
+ const renderNestedIndent =
488
+ colDefIndex === 0 && (details.level > 0 || hasSubRows);
489
+
490
+ const style: React.CSSProperties = {
491
+ "--__axc-data-table-nested-depth": details.level,
492
+ };
493
+
423
494
  return (
424
495
  <DataTableBaseCell
425
496
  /* TODO: Make this configurable */
@@ -427,47 +498,86 @@ function DataTableAutoTBodyContent<T>({
427
498
  key={colDef.id || colDefIndex}
428
499
  as={colDef.isRowHeader ? "th" : "td"}
429
500
  isSticky={isSticky}
501
+ data-nested={renderNestedIndent || undefined}
502
+ style={style}
430
503
  >
504
+ {renderNestedToggle && <NestedRowToggle details={details} />}
431
505
  {colDef.cell(rowData)}
432
506
  </DataTableBaseCell>
433
507
  );
434
508
  })}
435
509
  </DataTableTr>
436
510
  <DataTableExpandedRow
437
- rowId={rowId}
511
+ rowId={details.id}
438
512
  rowData={rowData}
439
- columnCount={columns.length}
513
+ fullWidthColSpan={fullWidthColSpan}
440
514
  />
441
515
  </React.Fragment>
442
516
  );
443
517
  });
444
518
  }
445
519
 
520
+ function NestedRowToggle({ details }: { details: ItemDetail<any> }) {
521
+ const { isSubRowExpanded, onExpandedSubRowIdsChange } =
522
+ useTableItemsContext();
523
+
524
+ const subRows = details.children;
525
+ const hasSubRows = subRows && subRows.length > 0;
526
+ const isRowExpanded = isSubRowExpanded(details.id);
527
+
528
+ return (
529
+ <div className="aksel-data-table__nested-toggle">
530
+ {hasSubRows && (
531
+ <Button
532
+ variant="tertiary"
533
+ data-color="neutral"
534
+ size="small"
535
+ onClick={(e) => {
536
+ e.stopPropagation();
537
+ onExpandedSubRowIdsChange(details.id);
538
+ }}
539
+ aria-expanded={isRowExpanded}
540
+ aria-label={isRowExpanded ? "Skjul under-rader" : "Vis under-rader"}
541
+ icon={
542
+ isRowExpanded ? (
543
+ <ChevronDownIcon aria-hidden />
544
+ ) : (
545
+ <ChevronRightIcon aria-hidden />
546
+ )
547
+ }
548
+ />
549
+ )}
550
+ </div>
551
+ );
552
+ }
553
+
446
554
  function DataTableExpandedRow<T>({
447
555
  rowId,
448
556
  rowData,
449
- columnCount,
557
+ fullWidthColSpan,
450
558
  }: {
451
559
  rowId: string | number;
452
560
  rowData: T;
453
- columnCount: number;
561
+ fullWidthColSpan: number;
454
562
  }) {
455
563
  const { tableId } = useDataTableContext();
456
- const expansionContext = useDataTableExpansion(false);
564
+ const {
565
+ enableDetailsPanel,
566
+ isExpanded,
567
+ getDetailsPanelContent,
568
+ getDetailsPanelHeight,
569
+ } = useDataTableExpansion();
457
570
 
458
- /* TODO: Is this the way we want to opt out? Might just be temp until auto and root is merged so they use same context */
459
- if (!expansionContext) {
571
+ if (!enableDetailsPanel) {
460
572
  return null;
461
573
  }
462
574
 
463
- const { isExpanded, getDetailsPanelContent, getDetailsPanelHeight } =
464
- expansionContext;
465
-
466
575
  if (!isExpanded(rowId)) {
467
576
  return null;
468
577
  }
469
578
 
470
579
  const content = getDetailsPanelContent?.(rowData);
580
+ const expansionId = getDataTableExpansionId(tableId, rowId);
471
581
 
472
582
  if (!content) {
473
583
  return null;
@@ -475,7 +585,7 @@ function DataTableExpandedRow<T>({
475
585
 
476
586
  return (
477
587
  <tr>
478
- <td id={`${tableId}-expansion-${rowId}`} colSpan={columnCount}>
588
+ <td id={expansionId} colSpan={fullWidthColSpan}>
479
589
  <div style={{ height: getDetailsPanelHeight?.(rowData) }}>
480
590
  {content}
481
591
  </div>
@@ -1,7 +1,8 @@
1
1
  import { createStrictContext } from "../../../utils/helpers";
2
+ import type { UseColumnOptionsResult } from "../hooks/useColumnOptions";
2
3
  import type { UseTableSelectionReturn } from "../hooks/useTableSelection";
3
4
 
4
- type DataTableContextProps = {
5
+ type DataTableContextProps<T> = {
5
6
  layout: "fixed" | "auto";
6
7
  withKeyboardNav: boolean;
7
8
  selectionState: UseTableSelectionReturn;
@@ -16,10 +17,11 @@ type DataTableContextProps = {
16
17
  disableRowSelectionOnClick: boolean;
17
18
  isLoading?: boolean;
18
19
  showLoadingOverlay: boolean;
20
+ columns: UseColumnOptionsResult<T>["columns"];
19
21
  };
20
22
 
21
23
  const { Provider: DataTableContextProvider, useContext: useDataTableContext } =
22
- createStrictContext<DataTableContextProps>({
24
+ createStrictContext<DataTableContextProps<any>>({
23
25
  name: "DataTableContext",
24
26
  errorMessage: "useDataTableContext must be used within DataTable",
25
27
  });
@@ -10,6 +10,8 @@ import {
10
10
  DataTableEmptyState,
11
11
  type DataTableEmptyStateProps,
12
12
  } from "../empty-state/DataTableEmptyState";
13
+ import { DataTableExpansionProvider } from "../hooks/useTableExpansion";
14
+ import type { ItemDetail } from "../hooks/useTableItems";
13
15
  import { useTableKeyboardNav } from "../hooks/useTableKeyboardNav";
14
16
  import { type SelectionProps } from "../hooks/useTableSelection";
15
17
  import { noSelectionState } from "../hooks/useTableSelection";
@@ -34,6 +36,8 @@ import {
34
36
  import { DataTableTr, type DataTableTrProps } from "../tr/DataTableTr";
35
37
  import { DataTableContextProvider } from "./DataTableRoot.context";
36
38
 
39
+ const EMPTY_ITEM_DETAILS = new Map<never, ItemDetail<never>>();
40
+
37
41
  interface DataTableProps
38
42
  extends React.HTMLAttributes<HTMLTableElement>, SelectionProps {
39
43
  children: React.ReactNode;
@@ -234,21 +238,24 @@ const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
234
238
  onRowClick={undefined}
235
239
  disableRowSelectionOnClick={false}
236
240
  showLoadingOverlay={false}
241
+ columns={[]}
237
242
  >
238
- <div className="aksel-data-table__border-wrapper">
239
- <div className="aksel-data-table__scroll-wrapper">
240
- <table
241
- {...rest}
242
- ref={mergedRef}
243
- className={cl("aksel-data-table", className)}
244
- data-zebra-stripes={zebraStripes}
245
- data-truncate-content={truncateContent}
246
- data-density={rowDensity}
247
- data-layout={layout}
248
- tabIndex={tabIndex}
249
- />
243
+ <DataTableExpansionProvider itemDetails={EMPTY_ITEM_DETAILS}>
244
+ <div className="aksel-data-table__border-wrapper">
245
+ <div className="aksel-data-table__scroll-wrapper">
246
+ <table
247
+ {...rest}
248
+ ref={mergedRef}
249
+ className={cl("aksel-data-table", className)}
250
+ data-zebra-stripes={zebraStripes}
251
+ data-truncate-content={truncateContent}
252
+ data-density={rowDensity}
253
+ data-layout={layout}
254
+ tabIndex={tabIndex}
255
+ />
256
+ </div>
250
257
  </div>
251
- </div>
258
+ </DataTableExpansionProvider>
252
259
  </DataTableContextProvider>
253
260
  );
254
261
  },
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useCallback } from "react";
1
+ import React, { forwardRef } from "react";
2
2
  import {
3
3
  ChevronDownUpIcon,
4
4
  ChevronUpDownIcon,
@@ -14,7 +14,10 @@ import { useId } from "../../../utils-external";
14
14
  import { cl, composeEventHandlers } from "../../../utils/helpers";
15
15
  import { DataTableBaseCell } from "../base-cell/DataTableBaseCell";
16
16
  import { DataTableColumnHeader } from "../column-header/DataTableColumnHeader";
17
- import { useDataTableExpansion } from "../hooks/useTableExpansion";
17
+ import {
18
+ getDataTableExpansionId,
19
+ useDataTableExpansion,
20
+ } from "../hooks/useTableExpansion";
18
21
  import {
19
22
  useDataTableContext,
20
23
  useDataTableLocation,
@@ -59,45 +62,41 @@ const DataTableTr = forwardRef<HTMLTableRowElement, DataTableTrProps>(
59
62
 
60
63
  const isSticky = location === "thead" && stickyHeader;
61
64
 
62
- const handleClick = useCallback(
63
- (event: React.MouseEvent<HTMLTableRowElement>) => {
64
- if (
65
- location !== "tbody" ||
66
- rowId === undefined ||
67
- isInteractiveTarget(event.target) ||
68
- (event.target as HTMLElement | null)?.closest(
69
- "[data-prevent-row-click]",
70
- )
71
- ) {
72
- return;
73
- }
74
-
75
- const selection = window.getSelection();
76
- if (selection && selection.toString().length > 0) {
77
- return;
78
- }
79
-
80
- if (
81
- !disableRowSelectionOnClick &&
82
- selectionState.selection.selectionMode !== "none"
83
- ) {
84
- selectionState.selection.toggleSelection(rowId);
85
- }
86
- onRowClick?.(rowId, event);
87
- },
88
- [
89
- disableRowSelectionOnClick,
90
- location,
91
- onRowClick,
92
- rowId,
93
- selectionState.selection,
94
- ],
95
- );
65
+ const handleClick =
66
+ location === "tbody" && rowId !== undefined
67
+ ? (event: React.MouseEvent<HTMLTableRowElement>) => {
68
+ if (
69
+ rowId === undefined ||
70
+ isInteractiveTarget(event.target) ||
71
+ (event.target as HTMLElement | null)?.closest(
72
+ "[data-prevent-row-click]",
73
+ )
74
+ ) {
75
+ return;
76
+ }
77
+
78
+ const selection = window.getSelection();
79
+ if (selection && selection.toString().length > 0) {
80
+ return;
81
+ }
82
+
83
+ if (
84
+ !disableRowSelectionOnClick &&
85
+ selectionState.selection.selectionMode !== "none"
86
+ ) {
87
+ selectionState.selection.toggleSelection(rowId);
88
+ }
89
+ onRowClick?.(rowId, event);
90
+ }
91
+ : undefined;
96
92
 
97
93
  return (
98
94
  <tr
99
95
  {...rest}
100
- onClick={composeEventHandlers(onClick, handleClick)}
96
+ // Avoid setting onClick if not needed, since this causes NVDA to announce the row as clickable.
97
+ onClick={
98
+ (onClick || handleClick) && composeEventHandlers(onClick, handleClick)
99
+ }
101
100
  ref={forwardedRef}
102
101
  className={cl("aksel-data-table__tr", className)}
103
102
  data-selected={selected}
@@ -122,22 +121,18 @@ const DataTableTr = forwardRef<HTMLTableRowElement, DataTableTrProps>(
122
121
  function RowExpansionCell({ rowId }: { rowId?: string | number }) {
123
122
  const { tableId, showLoadingSkeletons } = useDataTableContext();
124
123
  const { location } = useDataTableLocation();
125
- const expansionContext = useDataTableExpansion(false);
126
-
127
- if (!expansionContext) {
128
- return null;
129
- }
130
124
 
131
125
  const {
132
126
  isExpanded,
127
+ isDetailsPanelExpandable,
133
128
  toggleExpansion,
134
- enableExpansion,
129
+ enableDetailsPanel,
135
130
  isAllExpanded,
136
131
  toggleAll,
137
132
  showExpandAll,
138
- } = expansionContext;
133
+ } = useDataTableExpansion();
139
134
 
140
- if (!enableExpansion) {
135
+ if (!enableDetailsPanel) {
141
136
  return null;
142
137
  }
143
138
 
@@ -202,6 +197,12 @@ function RowExpansionCell({ rowId }: { rowId?: string | number }) {
202
197
  }
203
198
 
204
199
  const isRowExpanded = isExpanded(rowId);
200
+ const canExpandRow = isDetailsPanelExpandable(rowId);
201
+ const expansionId = getDataTableExpansionId(tableId, rowId);
202
+
203
+ if (!canExpandRow) {
204
+ return <DataTableTd UNSAFE_isSelection preventRowClick />;
205
+ }
205
206
 
206
207
  return (
207
208
  <DataTableTd UNSAFE_isSelection preventRowClick>
@@ -214,7 +215,7 @@ function RowExpansionCell({ rowId }: { rowId?: string | number }) {
214
215
  toggleExpansion(rowId);
215
216
  }}
216
217
  aria-expanded={isRowExpanded}
217
- aria-controls={`${tableId}-expansion-${rowId}`}
218
+ aria-controls={expansionId}
218
219
  aria-label={isRowExpanded ? "Skjul detaljer" : "Vis detaljer"}
219
220
  icon={
220
221
  isRowExpanded ? <MinusIcon aria-hidden /> : <PlusIcon aria-hidden />
@@ -225,7 +226,7 @@ function RowExpansionCell({ rowId }: { rowId?: string | number }) {
225
226
  }
226
227
 
227
228
  /**
228
- * TODO: How do these cells handle multiple thead rows, or col/rowspans?
229
+ * TODO: How do these cells handle multiple thead rows, or col/row-spans?
229
230
  * TODO: a11y for labels
230
231
  */
231
232
  function RowSelectionCell({ rowId }: { rowId?: string | number }) {