@navikt/ds-react 8.2.1 → 8.3.0

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 (99) hide show
  1. package/cjs/accordion/AccordionItem.d.ts +8 -6
  2. package/cjs/accordion/AccordionItem.js +1 -7
  3. package/cjs/accordion/AccordionItem.js.map +1 -1
  4. package/cjs/data/stories/DataTableProfiler.d.ts +6 -0
  5. package/cjs/data/stories/DataTableProfiler.js +124 -0
  6. package/cjs/data/stories/DataTableProfiler.js.map +1 -0
  7. package/cjs/data/stories/dummy-data.d.ts +2 -3
  8. package/cjs/data/stories/dummy-data.js +30 -9
  9. package/cjs/data/stories/dummy-data.js.map +1 -1
  10. package/cjs/data/table/index.d.ts +2 -2
  11. package/cjs/data/table/index.js +2 -1
  12. package/cjs/data/table/index.js.map +1 -1
  13. package/cjs/data/table/root/DataTableRoot.d.ts +15 -2
  14. package/cjs/data/table/root/DataTableRoot.js +4 -1
  15. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  16. package/cjs/data/table/tfoot/DataTableTfoot.d.ts +5 -0
  17. package/cjs/data/table/tfoot/DataTableTfoot.js +55 -0
  18. package/cjs/data/table/tfoot/DataTableTfoot.js.map +1 -0
  19. package/cjs/data/table/th/DataTableTh.d.ts +20 -4
  20. package/cjs/data/table/th/DataTableTh.js +24 -6
  21. package/cjs/data/table/th/DataTableTh.js.map +1 -1
  22. package/cjs/data/table/th/DataTableThActions.d.ts +5 -0
  23. package/cjs/data/table/th/DataTableThActions.js +23 -0
  24. package/cjs/data/table/th/DataTableThActions.js.map +1 -0
  25. package/cjs/data/table/th/DataTableThSortHandle.d.ts +6 -0
  26. package/cjs/data/table/th/DataTableThSortHandle.js +82 -0
  27. package/cjs/data/table/th/DataTableThSortHandle.js.map +1 -0
  28. package/cjs/expansion-card/ExpansionCard.d.ts +6 -5
  29. package/cjs/expansion-card/ExpansionCard.js +3 -9
  30. package/cjs/expansion-card/ExpansionCard.js.map +1 -1
  31. package/cjs/tooltip/Tooltip.d.ts +1 -1
  32. package/cjs/tooltip/Tooltip.js +35 -2
  33. package/cjs/tooltip/Tooltip.js.map +1 -1
  34. package/cjs/utils/i18n/locales/en.d.ts +3 -0
  35. package/cjs/utils/i18n/locales/en.js +3 -0
  36. package/cjs/utils/i18n/locales/en.js.map +1 -1
  37. package/cjs/utils/i18n/locales/nb.d.ts +4 -0
  38. package/cjs/utils/i18n/locales/nb.js +4 -0
  39. package/cjs/utils/i18n/locales/nb.js.map +1 -1
  40. package/cjs/utils/i18n/locales/nn.d.ts +3 -0
  41. package/cjs/utils/i18n/locales/nn.js +3 -0
  42. package/cjs/utils/i18n/locales/nn.js.map +1 -1
  43. package/esm/accordion/AccordionItem.d.ts +8 -6
  44. package/esm/accordion/AccordionItem.js +2 -8
  45. package/esm/accordion/AccordionItem.js.map +1 -1
  46. package/esm/data/stories/DataTableProfiler.d.ts +6 -0
  47. package/esm/data/stories/DataTableProfiler.js +89 -0
  48. package/esm/data/stories/DataTableProfiler.js.map +1 -0
  49. package/esm/data/stories/dummy-data.d.ts +2 -3
  50. package/esm/data/stories/dummy-data.js +30 -9
  51. package/esm/data/stories/dummy-data.js.map +1 -1
  52. package/esm/data/table/index.d.ts +2 -2
  53. package/esm/data/table/index.js +1 -1
  54. package/esm/data/table/index.js.map +1 -1
  55. package/esm/data/table/root/DataTableRoot.d.ts +15 -2
  56. package/esm/data/table/root/DataTableRoot.js +3 -1
  57. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  58. package/esm/data/table/tfoot/DataTableTfoot.d.ts +5 -0
  59. package/esm/data/table/tfoot/DataTableTfoot.js +19 -0
  60. package/esm/data/table/tfoot/DataTableTfoot.js.map +1 -0
  61. package/esm/data/table/th/DataTableTh.d.ts +20 -4
  62. package/esm/data/table/th/DataTableTh.js +25 -7
  63. package/esm/data/table/th/DataTableTh.js.map +1 -1
  64. package/esm/data/table/th/DataTableThActions.d.ts +5 -0
  65. package/esm/data/table/th/DataTableThActions.js +18 -0
  66. package/esm/data/table/th/DataTableThActions.js.map +1 -0
  67. package/esm/data/table/th/DataTableThSortHandle.d.ts +6 -0
  68. package/esm/data/table/th/DataTableThSortHandle.js +47 -0
  69. package/esm/data/table/th/DataTableThSortHandle.js.map +1 -0
  70. package/esm/expansion-card/ExpansionCard.d.ts +6 -5
  71. package/esm/expansion-card/ExpansionCard.js +4 -10
  72. package/esm/expansion-card/ExpansionCard.js.map +1 -1
  73. package/esm/tooltip/Tooltip.d.ts +1 -1
  74. package/esm/tooltip/Tooltip.js +35 -2
  75. package/esm/tooltip/Tooltip.js.map +1 -1
  76. package/esm/utils/i18n/locales/en.d.ts +3 -0
  77. package/esm/utils/i18n/locales/en.js +3 -0
  78. package/esm/utils/i18n/locales/en.js.map +1 -1
  79. package/esm/utils/i18n/locales/nb.d.ts +4 -0
  80. package/esm/utils/i18n/locales/nb.js +4 -0
  81. package/esm/utils/i18n/locales/nb.js.map +1 -1
  82. package/esm/utils/i18n/locales/nn.d.ts +3 -0
  83. package/esm/utils/i18n/locales/nn.js +3 -0
  84. package/esm/utils/i18n/locales/nn.js.map +1 -1
  85. package/package.json +3 -3
  86. package/src/accordion/AccordionItem.tsx +11 -18
  87. package/src/data/stories/DataTableProfiler.tsx +215 -0
  88. package/src/data/stories/dummy-data.tsx +39 -17
  89. package/src/data/table/index.tsx +2 -0
  90. package/src/data/table/root/DataTableRoot.tsx +19 -0
  91. package/src/data/table/tfoot/DataTableTfoot.tsx +21 -0
  92. package/src/data/table/th/DataTableTh.tsx +68 -31
  93. package/src/data/table/th/DataTableThActions.tsx +32 -0
  94. package/src/data/table/th/DataTableThSortHandle.tsx +67 -0
  95. package/src/expansion-card/ExpansionCard.tsx +15 -21
  96. package/src/tooltip/Tooltip.tsx +66 -11
  97. package/src/utils/i18n/locales/en.ts +3 -0
  98. package/src/utils/i18n/locales/nb.ts +3 -0
  99. package/src/utils/i18n/locales/nn.ts +3 -0
@@ -1,6 +1,5 @@
1
1
  import { createColumnHelper } from "@tanstack/react-table";
2
2
  import React from "react";
3
- import { HStack } from "../../layout/stack";
4
3
  import { Tag } from "../../tag";
5
4
 
6
5
  // Helper function to get random integer between min and max (inclusive)
@@ -18,7 +17,7 @@ const getRandomItems = <T,>(arr: T[], min = 1, max = 5): T[] => {
18
17
  return shuffled.slice(0, count);
19
18
  };
20
19
 
21
- interface PersonInfo {
20
+ export interface PersonInfo {
22
21
  // id: number;
23
22
  name: string;
24
23
  nationalId: string;
@@ -66,26 +65,49 @@ export const columns = [
66
65
  {
67
66
  header: "Age",
68
67
  accessorKey: "age",
68
+ footer: ({ table }) => {
69
+ const ages: number[] = [];
70
+ table.getFilteredRowModel().rows.forEach((row) => {
71
+ const value = row.getValue("age");
72
+ value && ages.push(value);
73
+ });
74
+ return `Avg: ${(ages.reduce((a, b) => a + b, 0) / ages.length).toFixed(2)}`;
75
+ },
69
76
  },
70
- {
71
- header: "Force sensitive",
72
- accessorKey: "forceSensitive",
73
- },
77
+
78
+ columnHelper.accessor("forceSensitive", {
79
+ cell: (info) => {
80
+ const value = info.getValue();
81
+ return (
82
+ <Tag
83
+ size="small"
84
+ variant="moderate"
85
+ data-color={value ? "accent" : "warning"}
86
+ >{`${value ? "Yes" : "No"}`}</Tag>
87
+ );
88
+ },
89
+ footer: ({ table }) => {
90
+ const totals = new Map();
91
+ totals.set("Yes", 0);
92
+ totals.set("No", 0);
93
+ table.getFilteredRowModel().rows.forEach((row) => {
94
+ const value = row.getValue("forceSensitive");
95
+ totals.set(
96
+ value ? "Yes" : "No",
97
+ (totals.get(value ? "Yes" : "No") ?? 0) + 1,
98
+ );
99
+ });
100
+ return `Yes: ${totals.get("Yes")}, No: ${totals.get("No")}`;
101
+ },
102
+ }),
74
103
  {
75
104
  header: "Home system",
76
105
  accessorKey: "homeSystem",
77
106
  },
78
- columnHelper.accessor("skills", {
79
- cell: (info) => (
80
- <HStack gap="space-8">
81
- {info.getValue().map((skill) => (
82
- <Tag key={skill} size="small">
83
- {skill}
84
- </Tag>
85
- ))}
86
- </HStack>
87
- ),
88
- }),
107
+ {
108
+ header: "Skills",
109
+ accessorKey: "skills",
110
+ },
89
111
  ];
90
112
 
91
113
  export const homeSystemOptions = [
@@ -7,6 +7,7 @@ export {
7
7
  DataTableTr,
8
8
  DataTableTh,
9
9
  DataTableTd,
10
+ DataTableTfoot,
10
11
  } from "./root/DataTableRoot";
11
12
  export type {
12
13
  DataTableProps,
@@ -16,4 +17,5 @@ export type {
16
17
  DataTableTrProps,
17
18
  DataTableThProps,
18
19
  DataTableTdProps,
20
+ DataTableTfootProps,
19
21
  } from "./root/DataTableRoot";
@@ -9,6 +9,10 @@ import {
9
9
  type DataTableTbodyProps,
10
10
  } from "../tbody/DataTableTbody";
11
11
  import { DataTableTd, type DataTableTdProps } from "../td/DataTableTd";
12
+ import {
13
+ DataTableTfoot,
14
+ type DataTableTfootProps,
15
+ } from "../tfoot/DataTableTfoot";
12
16
  import { DataTableTh, type DataTableThProps } from "../th/DataTableTh";
13
17
  import {
14
18
  DataTableThead,
@@ -101,6 +105,18 @@ interface DataTableRootComponent extends React.ForwardRefExoticComponent<
101
105
  * ```
102
106
  */
103
107
  Td: typeof DataTableTd;
108
+ /**
109
+ * @see 🏷️ {@link DataTableTfootProps}
110
+ * @example
111
+ * ```jsx
112
+ * <DataTable>
113
+ * <DataTable.Tfoot>
114
+ * ...
115
+ * </DataTable.Tfoot>
116
+ * </DataTable>
117
+ * ```
118
+ */
119
+ Tfoot: typeof DataTableTfoot;
104
120
  }
105
121
 
106
122
  const DataTable = forwardRef<HTMLTableElement, DataTableProps>(
@@ -123,6 +139,7 @@ DataTable.Tbody = DataTableTbody;
123
139
  DataTable.Th = DataTableTh;
124
140
  DataTable.Tr = DataTableTr;
125
141
  DataTable.Td = DataTableTd;
142
+ DataTable.Tfoot = DataTableTfoot;
126
143
 
127
144
  export {
128
145
  DataTable,
@@ -132,6 +149,7 @@ export {
132
149
  DataTableTh,
133
150
  DataTableThead,
134
151
  DataTableTr,
152
+ DataTableTfoot,
135
153
  };
136
154
  export default DataTable;
137
155
  export type {
@@ -142,4 +160,5 @@ export type {
142
160
  DataTableThProps,
143
161
  DataTableTheadProps,
144
162
  DataTableTrProps,
163
+ DataTableTfootProps,
145
164
  };
@@ -0,0 +1,21 @@
1
+ import React, { forwardRef } from "react";
2
+ import { cl } from "../../../utils/helpers";
3
+
4
+ type DataTableTfootProps = React.HTMLAttributes<HTMLTableSectionElement>;
5
+
6
+ const DataTableTfoot = forwardRef<HTMLTableSectionElement, DataTableTfootProps>(
7
+ ({ className, children, ...rest }, forwardedRef) => {
8
+ return (
9
+ <tfoot
10
+ {...rest}
11
+ ref={forwardedRef}
12
+ className={cl("aksel-data-table__tfoot", className)}
13
+ >
14
+ {children}
15
+ </tfoot>
16
+ );
17
+ },
18
+ );
19
+
20
+ export { DataTableTfoot };
21
+ export type { DataTableTfootProps };
@@ -1,64 +1,101 @@
1
1
  import React, { forwardRef } from "react";
2
- import {
3
- CaretLeftRightIcon,
4
- PushPinFillIcon,
5
- PushPinIcon,
6
- } from "@navikt/aksel-icons";
7
- import { Button } from "../../../button";
2
+ import { FunnelIcon, SortDownIcon, SortUpIcon } from "@navikt/aksel-icons";
3
+ import { HStack, Spacer } from "../../../layout/stack";
4
+ import { ActionMenu } from "../../../overlays/action-menu";
8
5
  import { cl } from "../../../utils/helpers";
6
+ import { DataTableThActions } from "./DataTableThActions";
7
+ import { DataTableThSortHandle } from "./DataTableThSortHandle";
9
8
 
10
9
  type DataTableThProps = React.HTMLAttributes<HTMLTableCellElement> & {
11
10
  resizeHandler?: React.MouseEventHandler<HTMLButtonElement>;
12
- isPinned?: boolean;
13
- pinningHandler?: React.MouseEventHandler<HTMLButtonElement>;
14
11
  size?: number;
12
+ sortDirection?: "asc" | "desc" | "none" | false;
13
+ onSortChange?: (direction: "asc" | "desc" | "none", event: Event) => void;
14
+ render?: {
15
+ filterMenu?: {
16
+ title: string;
17
+ content: React.ReactNode;
18
+ };
19
+ };
15
20
  };
16
21
 
22
+ /**
23
+ * TODO:
24
+ * - Plan for pinning: Move it into "settings" dialog like here: https://cloudscape.design/examples/react/table.html
25
+ */
17
26
  const DataTableTh = forwardRef<HTMLTableCellElement, DataTableThProps>(
18
27
  (
19
28
  {
20
29
  className,
21
30
  children,
22
31
  resizeHandler,
23
- isPinned = false,
24
- pinningHandler,
25
32
  size,
33
+ sortDirection,
34
+ onSortChange,
35
+ style,
36
+ render,
26
37
  ...rest
27
38
  },
28
39
  forwardedRef,
29
40
  ) => {
41
+ const { filterMenu } = render || {};
42
+
30
43
  return (
31
44
  <th
32
45
  {...rest}
33
46
  ref={forwardedRef}
34
47
  className={cl("aksel-data-table__th", className)}
35
- style={{ width: size }}
48
+ style={{ width: size, ...style }}
36
49
  >
37
- {children}
38
- {pinningHandler && (
39
- <Button
40
- onClick={pinningHandler}
41
- size="small"
42
- variant="secondary"
43
- icon={
44
- isPinned ? (
45
- <PushPinFillIcon aria-hidden title="Fest kolonne" />
46
- ) : (
47
- <PushPinIcon aria-hidden title="Løstne kolonne" />
48
- )
49
- }
50
+ <HStack align="center" gap="space-8" wrap={false}>
51
+ <div className="aksel-data-table__th-content">{children}</div>
52
+ <DataTableThSortHandle
53
+ sortDirection={sortDirection}
54
+ onSortChange={onSortChange}
50
55
  />
51
- )}
56
+ <Spacer />
57
+
58
+ <DataTableThActions>
59
+ {/* TODO: onSortChange just rotates between the three states now */}
60
+ {/* TODO: Sorting texts do not handle different data-types now */}
61
+ {sortDirection && (
62
+ <ActionMenu.Group label="Sortering">
63
+ <ActionMenu.Item
64
+ onSelect={(event) => onSortChange?.("desc", event)}
65
+ icon={<SortUpIcon aria-hidden />}
66
+ >
67
+ {sortDirection === "desc" ? "Fjern sortering" : "A-Z"}
68
+ </ActionMenu.Item>
69
+ <ActionMenu.Item
70
+ onSelect={(event) => onSortChange?.("asc", event)}
71
+ icon={<SortDownIcon aria-hidden />}
72
+ >
73
+ {sortDirection === "asc" ? "Fjern sortering" : "Z-A"}
74
+ </ActionMenu.Item>
75
+ </ActionMenu.Group>
76
+ )}
77
+ {filterMenu && (
78
+ <ActionMenu.Group label="Filter">
79
+ <ActionMenu.Sub>
80
+ <ActionMenu.SubTrigger icon={<FunnelIcon aria-hidden />}>
81
+ {filterMenu.title}
82
+ </ActionMenu.SubTrigger>
83
+ <ActionMenu.SubContent>
84
+ {/* TODO: ActionMenu stops tab from working, so user cant tab "into" filter now even when wrapper has focus */}
85
+ {filterMenu.content}
86
+ </ActionMenu.SubContent>
87
+ </ActionMenu.Sub>
88
+ </ActionMenu.Group>
89
+ )}
90
+ </DataTableThActions>
91
+ </HStack>
92
+
52
93
  {resizeHandler && (
53
- <Button
94
+ <button
54
95
  onMouseDown={resizeHandler}
55
96
  onMouseUp={resizeHandler}
56
97
  className={cl("aksel-data-table__th-resize-handle")}
57
- size="small"
58
- variant="secondary"
59
- icon={
60
- <CaretLeftRightIcon aria-hidden title="Endre kolonnestørrelse" />
61
- }
98
+ data-color="neutral"
62
99
  />
63
100
  )}
64
101
  </th>
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { MenuElipsisVerticalIcon } from "@navikt/aksel-icons";
3
+ import { Button } from "../../../button";
4
+ import { ActionMenu } from "../../../overlays/action-menu";
5
+
6
+ function DataTableThActions({ children }: { children?: React.ReactNode }) {
7
+ const [open, setOpen] = React.useState(false);
8
+ /* TODO: Fix this */
9
+ // @ts-expect-error Temp hack to hide when no children present
10
+ if (!children || !children.filter(Boolean).length) {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <ActionMenu open={open} onOpenChange={setOpen}>
16
+ <ActionMenu.Trigger>
17
+ <Button
18
+ data-color="neutral"
19
+ variant="tertiary"
20
+ size="small"
21
+ icon={<MenuElipsisVerticalIcon title="Åpne kolonnemeny" />}
22
+ onClick={() => setOpen(!open)}
23
+ data-expanded={open}
24
+ className="aksel-data-table__th-action-button"
25
+ />
26
+ </ActionMenu.Trigger>
27
+ <ActionMenu.Content>{children}</ActionMenu.Content>
28
+ </ActionMenu>
29
+ );
30
+ }
31
+
32
+ export { DataTableThActions };
@@ -0,0 +1,67 @@
1
+ import React, { useMemo } from "react";
2
+ import {
3
+ ArrowsUpDownIcon,
4
+ SortDownIcon,
5
+ SortUpIcon,
6
+ } from "@navikt/aksel-icons";
7
+ import { Button } from "../../../button";
8
+
9
+ const ICON_CONFIG = {
10
+ desc: {
11
+ icon: SortDownIcon,
12
+ title: "Sorter stigende",
13
+ },
14
+ asc: {
15
+ icon: SortUpIcon,
16
+ title: "Ingen sortering",
17
+ },
18
+ none: {
19
+ icon: ArrowsUpDownIcon,
20
+ title: "Sorter synkende",
21
+ },
22
+ };
23
+
24
+ function DataTableThSortHandle({
25
+ sortDirection = false,
26
+ onSortChange,
27
+ }: {
28
+ sortDirection?: "asc" | "desc" | "none" | false;
29
+ onSortChange?: (direction: "asc" | "desc" | "none", event: Event) => void;
30
+ }) {
31
+ const IconConfig = useMemo(() => {
32
+ if (!sortDirection) {
33
+ return null;
34
+ }
35
+ return ICON_CONFIG[sortDirection];
36
+ }, [sortDirection]);
37
+
38
+ if (!sortDirection || !IconConfig) {
39
+ return null;
40
+ }
41
+
42
+ return (
43
+ <Button
44
+ data-color="neutral"
45
+ variant="tertiary"
46
+ size="small"
47
+ icon={<IconConfig.icon title={IconConfig.title} />}
48
+ onClick={(event) => {
49
+ if (!onSortChange) return;
50
+
51
+ /* TODO: This configuration is not a given */
52
+ let newDirection: "asc" | "desc" | "none";
53
+ if (sortDirection === "none") {
54
+ newDirection = "asc";
55
+ } else if (sortDirection === "asc") {
56
+ newDirection = "desc";
57
+ } else {
58
+ newDirection = "none";
59
+ }
60
+ /* TODO: Handle types better */
61
+ onSortChange(newDirection, event as unknown as Event);
62
+ }}
63
+ />
64
+ );
65
+ }
66
+
67
+ export { DataTableThSortHandle };
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useRef } from "react";
1
+ import React, { forwardRef } from "react";
2
2
  import type { AkselColor } from "../types";
3
3
  import type { OverridableComponent } from "../utils-external";
4
4
  import { cl } from "../utils/helpers";
@@ -19,10 +19,9 @@ import {
19
19
  } from "./ExpansionCardTitle";
20
20
  import { ExpansionCardContext } from "./context";
21
21
 
22
- interface ExpansionCardComponent
23
- extends React.ForwardRefExoticComponent<
24
- ExpansionCardProps & React.RefAttributes<HTMLDivElement>
25
- > {
22
+ interface ExpansionCardComponent extends React.ForwardRefExoticComponent<
23
+ ExpansionCardProps & React.RefAttributes<HTMLDivElement>
24
+ > {
26
25
  /**
27
26
  * @see 🏷️ {@link ExpansionCardHeaderProps}
28
27
  */
@@ -48,20 +47,23 @@ interface ExpansionCardComponent
48
47
  >;
49
48
  }
50
49
 
51
- interface ExpansionCardCommonProps
52
- extends Omit<React.HTMLAttributes<HTMLDivElement>, "onToggle"> {
50
+ interface ExpansionCardCommonProps extends Omit<
51
+ React.HTMLAttributes<HTMLDivElement>,
52
+ "onToggle"
53
+ > {
53
54
  children: React.ReactNode;
54
55
  /**
55
- * Callback for when Card is toggled open/closed
56
+ * Callback for when Card is opened/closed.
56
57
  */
57
58
  onToggle?: (open: boolean) => void;
58
59
  /**
59
- * Controlled open-state
60
- * Using this removes automatic control of open-state
60
+ * Controlled open-state.
61
+ *
62
+ * Using this removes automatic control of open-state.
61
63
  */
62
64
  open?: boolean;
63
65
  /**
64
- * Defaults to open if not controlled
66
+ * The open state when initially rendered. Use when you do not need to control the open state.
65
67
  * @default false
66
68
  */
67
69
  defaultOpen?: boolean;
@@ -102,7 +104,7 @@ export type ExpansionCardProps = ExpansionCardCommonProps &
102
104
  *
103
105
  * @example
104
106
  * ```jsx
105
- * <ExpansionCard aria-label="default-demo">
107
+ * <ExpansionCard aria-label="Utbetaling av sykepenger">
106
108
  * <ExpansionCard.Header>
107
109
  * <ExpansionCard.Title>Utbetaling av sykepenger</ExpansionCard.Title>
108
110
  * </ExpansionCard.Header>
@@ -125,14 +127,9 @@ export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
125
127
  },
126
128
  ref,
127
129
  ) => {
128
- const shouldFade = useRef<boolean>(!(Boolean(open) || defaultOpen));
129
-
130
130
  const [_open, _setOpen] = useControllableState({
131
131
  value: open,
132
- onChange: (newValue) => {
133
- onToggle?.(newValue);
134
- shouldFade.current = true;
135
- },
132
+ onChange: onToggle,
136
133
  defaultValue: defaultOpen,
137
134
  });
138
135
 
@@ -151,9 +148,6 @@ export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
151
148
  "aksel-expansioncard",
152
149
  className,
153
150
  `aksel-expansioncard--${size}`,
154
- {
155
- "aksel-expansioncard--no-animation": !shouldFade.current,
156
- },
157
151
  )}
158
152
  ref={ref}
159
153
  />
@@ -12,6 +12,7 @@ import {
12
12
  useInteractions,
13
13
  } from "@floating-ui/react";
14
14
  import React, { HTMLAttributes, forwardRef, useRef } from "react";
15
+ import { HStack } from "../layout/stack";
15
16
  import { useModalContext } from "../modal/Modal.context";
16
17
  import { Portal } from "../portal";
17
18
  import { Detail } from "../typography";
@@ -19,6 +20,7 @@ import { useId } from "../utils-external";
19
20
  import { Slot } from "../utils/components/slot/Slot";
20
21
  import { cl } from "../utils/helpers";
21
22
  import { useControllableState, useMergeRefs } from "../utils/hooks";
23
+ import { useI18n } from "../utils/i18n/i18n.hooks";
22
24
 
23
25
  export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
24
26
  /**
@@ -79,7 +81,7 @@ export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
79
81
  /**
80
82
  * List of Keyboard-keys for shortcuts.
81
83
  */
82
- keys?: string[];
84
+ keys?: string[] | [string[], string[]];
83
85
  /**
84
86
  * When false, Tooltip labels the element, and child-elements content will be ignored by screen-readers.
85
87
  * When true, content is added as additional information to the child element.
@@ -204,7 +206,7 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
204
206
  ref={refs.setReference}
205
207
  {...getReferenceProps()}
206
208
  {...labelProps}
207
- aria-keyshortcuts={keys ? keys.join("+") : undefined}
209
+ aria-keyshortcuts={ariaShortcuts(keys)}
208
210
  >
209
211
  {children}
210
212
  </Slot>
@@ -232,15 +234,7 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
232
234
  data-state="open"
233
235
  >
234
236
  {content}
235
- {keys && (
236
- <span className="aksel-tooltip__keys" aria-hidden>
237
- {keys.map((key) => (
238
- <Detail as="kbd" key={key} className="aksel-tooltip__key">
239
- {key}
240
- </Detail>
241
- ))}
242
- </span>
243
- )}
237
+ <TooltipShortcuts shortcuts={keys} />
244
238
  {_arrow && (
245
239
  <div
246
240
  ref={(node) => {
@@ -269,4 +263,65 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
269
263
  },
270
264
  );
271
265
 
266
+ function isKeyShortcutNested(
267
+ shortcuts: TooltipProps["keys"],
268
+ ): shortcuts is [string[], string[]] {
269
+ return Array.isArray(shortcuts?.[0]);
270
+ }
271
+
272
+ /**
273
+ * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-keyshortcuts
274
+ * Space-separated shortcuts is valid syntax
275
+ */
276
+ function ariaShortcuts(shortcuts: TooltipProps["keys"]) {
277
+ if (!shortcuts) {
278
+ return undefined;
279
+ }
280
+
281
+ if (isKeyShortcutNested(shortcuts)) {
282
+ return shortcuts.map((key) => key.join("+")).join(" ");
283
+ }
284
+
285
+ return shortcuts.join("+");
286
+ }
287
+
288
+ function TooltipShortcuts({ shortcuts }: { shortcuts: TooltipProps["keys"] }) {
289
+ const translate = useI18n("Tooltip");
290
+
291
+ if (!shortcuts) {
292
+ return null;
293
+ }
294
+
295
+ if (isKeyShortcutNested(shortcuts)) {
296
+ return (
297
+ <span className="aksel-tooltip__keys" aria-hidden>
298
+ {shortcuts.map((key, index) => (
299
+ <>
300
+ <HStack gap="space-4">
301
+ {key.map((k, i) => (
302
+ <Detail as="kbd" key={i} className="aksel-tooltip__key">
303
+ {k}
304
+ </Detail>
305
+ ))}
306
+ </HStack>
307
+ {index < shortcuts.length - 1 && (
308
+ <span> {translate("shortcutSeparator")} </span>
309
+ )}
310
+ </>
311
+ ))}
312
+ </span>
313
+ );
314
+ }
315
+
316
+ return (
317
+ <span className="aksel-tooltip__keys" aria-hidden>
318
+ {shortcuts.map((k, i) => (
319
+ <Detail as="kbd" key={i} className="aksel-tooltip__key">
320
+ {k}
321
+ </Detail>
322
+ ))}
323
+ </span>
324
+ );
325
+ }
326
+
272
327
  export default Tooltip;
@@ -132,4 +132,7 @@ export default {
132
132
  reset: "Reset zoom",
133
133
  },
134
134
  },
135
+ Tooltip: {
136
+ shortcutSeparator: "or",
137
+ },
135
138
  } satisfies Translations;
@@ -139,4 +139,7 @@ export default {
139
139
  reset: "Tilbakestill tidsperspektiv",
140
140
  },
141
141
  },
142
+ Tooltip: {
143
+ shortcutSeparator: "eller",
144
+ },
142
145
  } satisfies TranslationMap;
@@ -132,4 +132,7 @@ export default {
132
132
  reset: "Tilbakestill tidsperspektiv",
133
133
  },
134
134
  },
135
+ Tooltip: {
136
+ shortcutSeparator: "eller",
137
+ },
135
138
  } satisfies Translations;