@navikt/ds-react 7.3.1 → 7.4.1

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 (114) hide show
  1. package/cjs/date/utils/parse-date.js +1 -0
  2. package/cjs/date/utils/parse-date.js.map +1 -1
  3. package/cjs/expansion-card/ExpansionCardHeader.js +4 -2
  4. package/cjs/expansion-card/ExpansionCardHeader.js.map +1 -1
  5. package/cjs/form/checkbox/Checkbox.js +1 -1
  6. package/cjs/form/checkbox/Checkbox.js.map +1 -1
  7. package/cjs/form/combobox/FilteredOptions/AddNewOption.js +4 -4
  8. package/cjs/form/combobox/FilteredOptions/AddNewOption.js.map +1 -1
  9. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js +15 -2
  10. package/cjs/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -1
  11. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +2 -2
  12. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  13. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js +12 -11
  14. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  15. package/cjs/form/combobox/Input/Input.js +23 -10
  16. package/cjs/form/combobox/Input/Input.js.map +1 -1
  17. package/cjs/form/search/Search.d.ts +1 -2
  18. package/cjs/form/search/Search.js +20 -20
  19. package/cjs/form/search/Search.js.map +1 -1
  20. package/cjs/form/search/SearchButton.js +5 -1
  21. package/cjs/form/search/SearchButton.js.map +1 -1
  22. package/cjs/loader/Loader.d.ts +2 -2
  23. package/cjs/loader/Loader.js +5 -3
  24. package/cjs/loader/Loader.js.map +1 -1
  25. package/cjs/modal/ModalHeader.js +3 -1
  26. package/cjs/modal/ModalHeader.js.map +1 -1
  27. package/cjs/pagination/Pagination.js +4 -2
  28. package/cjs/pagination/Pagination.js.map +1 -1
  29. package/cjs/progress-bar/ProgressBar.js +16 -7
  30. package/cjs/progress-bar/ProgressBar.js.map +1 -1
  31. package/cjs/slot/Slot.js +4 -3
  32. package/cjs/slot/Slot.js.map +1 -1
  33. package/cjs/table/ExpandableRow.d.ts +1 -1
  34. package/cjs/table/ExpandableRow.js +11 -7
  35. package/cjs/table/ExpandableRow.js.map +1 -1
  36. package/cjs/tabs/parts/tablist/ScrollButtons.js +1 -1
  37. package/cjs/tabs/parts/tablist/ScrollButtons.js.map +1 -1
  38. package/cjs/util/i18n/locales/en.d.ts +22 -0
  39. package/cjs/util/i18n/locales/en.js +22 -0
  40. package/cjs/util/i18n/locales/en.js.map +1 -1
  41. package/cjs/util/i18n/locales/nb.d.ts +22 -0
  42. package/cjs/util/i18n/locales/nb.js +22 -0
  43. package/cjs/util/i18n/locales/nb.js.map +1 -1
  44. package/cjs/util/i18n/locales/nn.d.ts +22 -0
  45. package/cjs/util/i18n/locales/nn.js +22 -0
  46. package/cjs/util/i18n/locales/nn.js.map +1 -1
  47. package/esm/date/utils/parse-date.js +1 -0
  48. package/esm/date/utils/parse-date.js.map +1 -1
  49. package/esm/expansion-card/ExpansionCardHeader.js +4 -2
  50. package/esm/expansion-card/ExpansionCardHeader.js.map +1 -1
  51. package/esm/form/checkbox/Checkbox.js +1 -1
  52. package/esm/form/checkbox/Checkbox.js.map +1 -1
  53. package/esm/form/combobox/FilteredOptions/AddNewOption.js +4 -4
  54. package/esm/form/combobox/FilteredOptions/AddNewOption.js.map +1 -1
  55. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js +15 -2
  56. package/esm/form/combobox/FilteredOptions/FilteredOptionsItem.js.map +1 -1
  57. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +2 -2
  58. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  59. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js +13 -12
  60. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  61. package/esm/form/combobox/Input/Input.js +23 -10
  62. package/esm/form/combobox/Input/Input.js.map +1 -1
  63. package/esm/form/search/Search.d.ts +1 -2
  64. package/esm/form/search/Search.js +21 -21
  65. package/esm/form/search/Search.js.map +1 -1
  66. package/esm/form/search/SearchButton.js +5 -1
  67. package/esm/form/search/SearchButton.js.map +1 -1
  68. package/esm/loader/Loader.d.ts +2 -2
  69. package/esm/loader/Loader.js +5 -3
  70. package/esm/loader/Loader.js.map +1 -1
  71. package/esm/modal/ModalHeader.js +3 -1
  72. package/esm/modal/ModalHeader.js.map +1 -1
  73. package/esm/pagination/Pagination.js +4 -2
  74. package/esm/pagination/Pagination.js.map +1 -1
  75. package/esm/progress-bar/ProgressBar.js +17 -8
  76. package/esm/progress-bar/ProgressBar.js.map +1 -1
  77. package/esm/slot/Slot.js +4 -3
  78. package/esm/slot/Slot.js.map +1 -1
  79. package/esm/table/ExpandableRow.d.ts +1 -1
  80. package/esm/table/ExpandableRow.js +11 -7
  81. package/esm/table/ExpandableRow.js.map +1 -1
  82. package/esm/tabs/parts/tablist/ScrollButtons.js +1 -1
  83. package/esm/tabs/parts/tablist/ScrollButtons.js.map +1 -1
  84. package/esm/util/i18n/locales/en.d.ts +22 -0
  85. package/esm/util/i18n/locales/en.js +22 -0
  86. package/esm/util/i18n/locales/en.js.map +1 -1
  87. package/esm/util/i18n/locales/nb.d.ts +22 -0
  88. package/esm/util/i18n/locales/nb.js +22 -0
  89. package/esm/util/i18n/locales/nb.js.map +1 -1
  90. package/esm/util/i18n/locales/nn.d.ts +22 -0
  91. package/esm/util/i18n/locales/nn.js +22 -0
  92. package/esm/util/i18n/locales/nn.js.map +1 -1
  93. package/package.json +4 -4
  94. package/src/date/utils/parse-date.ts +1 -0
  95. package/src/expansion-card/ExpansionCardHeader.tsx +4 -2
  96. package/src/form/checkbox/Checkbox.tsx +0 -1
  97. package/src/form/combobox/FilteredOptions/AddNewOption.tsx +4 -4
  98. package/src/form/combobox/FilteredOptions/FilteredOptionsItem.tsx +22 -1
  99. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +3 -3
  100. package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +13 -12
  101. package/src/form/combobox/Input/Input.tsx +26 -15
  102. package/src/form/combobox/__tests__/combobox.test.tsx +39 -0
  103. package/src/form/search/Search.tsx +20 -36
  104. package/src/form/search/SearchButton.tsx +5 -1
  105. package/src/loader/Loader.tsx +8 -4
  106. package/src/modal/ModalHeader.tsx +3 -1
  107. package/src/pagination/Pagination.tsx +6 -4
  108. package/src/progress-bar/ProgressBar.tsx +21 -14
  109. package/src/slot/Slot.tsx +8 -3
  110. package/src/table/ExpandableRow.tsx +12 -13
  111. package/src/tabs/parts/tablist/ScrollButtons.tsx +1 -5
  112. package/src/util/i18n/locales/en.ts +24 -0
  113. package/src/util/i18n/locales/nb.ts +24 -0
  114. package/src/util/i18n/locales/nn.ts +24 -0
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from "react";
1
+ import { useState } from "react";
2
2
 
3
3
  export type VirtualFocusType = {
4
4
  activeElement: HTMLElement | undefined;
@@ -43,6 +43,11 @@ const useVirtualFocus = (
43
43
  : false;
44
44
  };
45
45
 
46
+ const setActiveAndScrollToElement = (element?: HTMLElement) => {
47
+ setActiveElement(element);
48
+ element?.scrollIntoView?.({ block: "center" });
49
+ };
50
+
46
51
  const moveFocusUp = () => {
47
52
  if (!activeElement) {
48
53
  return;
@@ -53,14 +58,14 @@ const useVirtualFocus = (
53
58
  if (_currentIndex === 0) {
54
59
  setActiveElement(undefined);
55
60
  } else {
56
- setActiveElement(elementAbove);
61
+ setActiveAndScrollToElement(elementAbove);
57
62
  }
58
63
  };
59
64
 
60
65
  const moveFocusDown = () => {
61
66
  const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
62
67
  if (!activeElement) {
63
- setActiveElement(elementsAbleToReceiveFocus[0]);
68
+ setActiveAndScrollToElement(elementsAbleToReceiveFocus[0]);
64
69
  return;
65
70
  }
66
71
  const _currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
@@ -68,17 +73,17 @@ const useVirtualFocus = (
68
73
  return;
69
74
  }
70
75
 
71
- setActiveElement(elementsAbleToReceiveFocus[_currentIndex + 1]);
76
+ setActiveAndScrollToElement(elementsAbleToReceiveFocus[_currentIndex + 1]);
72
77
  };
73
78
 
74
79
  const resetFocus = () => setActiveElement(undefined);
75
80
  const moveFocusToTop = () => {
76
81
  const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
77
- setActiveElement(elementsAbleToReceiveFocus[0]);
82
+ setActiveAndScrollToElement(elementsAbleToReceiveFocus[0]);
78
83
  };
79
84
  const moveFocusToBottom = () => {
80
85
  const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
81
- setActiveElement(
86
+ setActiveAndScrollToElement(
82
87
  elementsAbleToReceiveFocus[elementsAbleToReceiveFocus.length - 1],
83
88
  );
84
89
  };
@@ -98,7 +103,7 @@ const useVirtualFocus = (
98
103
  const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
99
104
  const currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
100
105
  const newIndex = Math.max(currentIndex - numberOfElements, 0);
101
- setActiveElement(elementsAbleToReceiveFocus[newIndex]);
106
+ setActiveAndScrollToElement(elementsAbleToReceiveFocus[newIndex]);
102
107
  };
103
108
 
104
109
  const moveFocusDownBy = (numberOfElements: number) => {
@@ -110,13 +115,9 @@ const useVirtualFocus = (
110
115
  currentIndex + numberOfElements,
111
116
  elementsAbleToReceiveFocus.length - 1,
112
117
  );
113
- setActiveElement(elementsAbleToReceiveFocus[newIndex]);
118
+ setActiveAndScrollToElement(elementsAbleToReceiveFocus[newIndex]);
114
119
  };
115
120
 
116
- useEffect(() => {
117
- activeElement?.scrollIntoView?.({ block: "nearest" });
118
- }, [activeElement]);
119
-
120
121
  return {
121
122
  activeElement,
122
123
  getElementById,
@@ -11,6 +11,7 @@ import { useMergeRefs } from "../../../util/hooks";
11
11
  import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
12
12
  import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
13
13
  import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
14
+ import { ComboboxOption } from "../types";
14
15
  import { useInputContext } from "./Input.context";
15
16
 
16
17
  interface InputProps
@@ -93,19 +94,23 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
93
94
  clearInput(event);
94
95
  } else if ((allowNewValues || shouldAutocomplete) && value !== "") {
95
96
  event.preventDefault();
96
- // Autocompleting or adding a new value
97
- const selectedValue =
98
- allowNewValues && isValueNew
99
- ? { label: value, value }
100
- : filteredOptionsUtil.getFirstValueStartingWith(
101
- value,
102
- filteredOptions,
103
- ) || filteredOptions[0];
97
+
98
+ const autoCompletedOption =
99
+ filteredOptionsUtil.getFirstValueStartingWith(
100
+ value,
101
+ filteredOptions,
102
+ );
103
+ let selectedValue: ComboboxOption | undefined;
104
+
105
+ if (shouldAutocomplete && autoCompletedOption) {
106
+ selectedValue = autoCompletedOption;
107
+ } else if (allowNewValues && isValueNew) {
108
+ selectedValue = { label: value, value };
109
+ }
104
110
 
105
111
  if (!selectedValue) {
106
112
  return;
107
113
  }
108
-
109
114
  toggleOption(selectedValue, event);
110
115
  if (!isMultiSelect && !isTextInSelectedOptions(selectedValue.label)) {
111
116
  toggleIsListOpen(false);
@@ -177,10 +182,12 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
177
182
  if (value !== searchTerm) {
178
183
  setValue(searchTerm);
179
184
  }
180
- if (virtualFocus.activeElement === null || !isListOpen) {
185
+ if (!isListOpen) {
181
186
  toggleIsListOpen(true);
187
+ setTimeout(virtualFocus.moveFocusDown, 0); // Wait until list is visible so that scrollIntoView works
188
+ } else {
189
+ virtualFocus.moveFocusDown();
182
190
  }
183
- virtualFocus.moveFocusDown();
184
191
  } else if (e.key === "ArrowUp") {
185
192
  if (value !== "" && value !== searchTerm) {
186
193
  onChange(value);
@@ -199,19 +206,23 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
199
206
  virtualFocus.moveFocusToTop();
200
207
  } else if (e.key === "End") {
201
208
  e.preventDefault();
202
- if (virtualFocus.activeElement === null || !isListOpen) {
209
+ if (!isListOpen) {
203
210
  toggleIsListOpen(true);
211
+ setTimeout(virtualFocus.moveFocusToBottom, 0); // Wait until list is visible so that scrollIntoView works
212
+ } else {
213
+ virtualFocus.moveFocusToBottom();
204
214
  }
205
- virtualFocus.moveFocusToBottom();
206
215
  } else if (e.key === "PageUp") {
207
216
  e.preventDefault();
208
217
  virtualFocus.moveFocusUpBy(6);
209
218
  } else if (e.key === "PageDown") {
210
219
  e.preventDefault();
211
- if (virtualFocus.activeElement === null || !isListOpen) {
220
+ if (!isListOpen) {
212
221
  toggleIsListOpen(true);
222
+ setTimeout(() => virtualFocus.moveFocusDownBy(6), 0); // Wait until list is visible so that scrollIntoView works
223
+ } else {
224
+ virtualFocus.moveFocusDownBy(6);
213
225
  }
214
- virtualFocus.moveFocusDownBy(6);
215
226
  }
216
227
  },
217
228
  [
@@ -290,6 +290,45 @@ describe("Render combobox", () => {
290
290
  false,
291
291
  );
292
292
  });
293
+
294
+ test("and pressing enter to select autocompleted word will select existing word when addNewOptions is true", async () => {
295
+ const onToggleSelected = vi.fn();
296
+ render(
297
+ <App
298
+ onToggleSelected={onToggleSelected}
299
+ options={options.map((opt) => ({
300
+ label: `${opt} (${opt})`,
301
+ value: opt,
302
+ }))}
303
+ shouldAutocomplete
304
+ allowNewValues
305
+ />,
306
+ );
307
+
308
+ const combobox = screen.getByRole("combobox", {
309
+ name: "Hva er dine favorittfrukter?",
310
+ });
311
+
312
+ await act(async () => {
313
+ await userEvent.click(combobox);
314
+
315
+ await userEvent.type(combobox, "p");
316
+ });
317
+
318
+ expect(combobox.getAttribute("value")).toBe(
319
+ "passion fruit (passion fruit)",
320
+ );
321
+
322
+ await act(async () => {
323
+ await userEvent.keyboard("{Enter}");
324
+ });
325
+
326
+ expect(onToggleSelected).toHaveBeenCalledWith(
327
+ "passion fruit",
328
+ true,
329
+ false,
330
+ );
331
+ });
293
332
  });
294
333
 
295
334
  describe("has keyboard navigation", () => {
@@ -2,7 +2,6 @@ import cl from "clsx";
2
2
  import React, {
3
3
  InputHTMLAttributes,
4
4
  forwardRef,
5
- useCallback,
6
5
  useRef,
7
6
  useState,
8
7
  } from "react";
@@ -10,6 +9,7 @@ import { MagnifyingGlassIcon, XMarkIcon } from "@navikt/aksel-icons";
10
9
  import { BodyShort, ErrorMessage, Label } from "../../typography";
11
10
  import { omit } from "../../util";
12
11
  import { useMergeRefs } from "../../util/hooks/useMergeRefs";
12
+ import { useI18n } from "../../util/i18n/i18n.context";
13
13
  import { FormFieldProps, useFormField } from "../useFormField";
14
14
  import SearchButton, { SearchButtonType } from "./SearchButton";
15
15
  import { SearchContext } from "./context";
@@ -68,13 +68,9 @@ export interface SearchProps
68
68
  */
69
69
  variant?: "primary" | "secondary" | "simple";
70
70
  /**
71
- * Exposes the HTML size attribute. Specifies the width of the element, in characters.
71
+ * HTML size attribute. Specifies the width of the input, in characters.
72
72
  */
73
73
  htmlSize?: number | string;
74
- /*
75
- * Exposes role attribute.
76
- */
77
- role?: string;
78
74
  }
79
75
 
80
76
  interface SearchComponent
@@ -123,31 +119,24 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
123
119
  onChange,
124
120
  onSearchClick,
125
121
  htmlSize,
126
- role,
127
122
  ...rest
128
123
  } = props;
129
124
 
130
125
  const searchRef = useRef<HTMLInputElement | null>(null);
131
126
  const mergedRef = useMergeRefs(searchRef, ref);
132
-
127
+ const translate = useI18n("Search");
133
128
  const [internalValue, setInternalValue] = useState(defaultValue ?? "");
134
129
 
135
- const handleChange = useCallback(
136
- (v: string) => {
137
- value === undefined && setInternalValue(v);
138
- onChange?.(v);
139
- },
140
- [onChange, value],
141
- );
130
+ const handleChange = (newValue: string) => {
131
+ value === undefined && setInternalValue(newValue);
132
+ onChange?.(newValue);
133
+ };
142
134
 
143
- const handleClear = useCallback(
144
- (event: SearchClearEvent) => {
145
- onClear?.(event);
146
- handleChange("");
147
- searchRef.current?.focus?.();
148
- },
149
- [handleChange, onClear],
150
- );
135
+ const handleClear = (clearEvent: SearchClearEvent) => {
136
+ onClear?.(clearEvent);
137
+ handleChange("");
138
+ searchRef.current?.focus?.();
139
+ };
151
140
 
152
141
  const handleClick = () => {
153
142
  onSearchClick?.(`${value ?? internalValue}`);
@@ -156,26 +145,22 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
156
145
  return (
157
146
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
158
147
  <div
159
- onKeyDown={(e) => {
160
- if (e.key !== "Escape") {
148
+ onKeyDown={(event) => {
149
+ if (event.key !== "Escape") {
161
150
  return;
162
151
  }
163
- searchRef.current?.value &&
164
- searchRef.current?.value !== "" &&
165
- e.preventDefault();
166
-
167
- handleClear({ trigger: "Escape", event: e });
152
+ searchRef.current?.value && event.preventDefault();
153
+ handleClear({ trigger: "Escape", event });
168
154
  }}
169
155
  className={cl(
170
156
  className,
171
157
  "navds-form-field",
172
158
  `navds-form-field--${size}`,
173
159
  "navds-search",
174
-
175
160
  {
176
161
  "navds-search--error": hasError,
177
- "navds-search--disabled": !!inputProps.disabled,
178
- "navds-search--with-size": !!htmlSize,
162
+ "navds-search--disabled": inputProps.disabled,
163
+ "navds-search--with-size": htmlSize,
179
164
  },
180
165
  )}
181
166
  >
@@ -215,7 +200,6 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
215
200
  value={value ?? internalValue}
216
201
  onChange={(e) => handleChange(e.target.value)}
217
202
  type="search"
218
- role={role ?? "searchbox"}
219
203
  className={cl(
220
204
  className,
221
205
  "navds-search__input",
@@ -229,11 +213,11 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
229
213
  {(value ?? internalValue) && clearButton && (
230
214
  <button
231
215
  type="button"
232
- onClick={(e) => handleClear({ trigger: "Click", event: e })}
216
+ onClick={(event) => handleClear({ trigger: "Click", event })}
233
217
  className="navds-search__button-clear"
234
218
  >
235
219
  <span className="navds-sr-only">
236
- {clearButtonLabel ? clearButtonLabel : "Tøm"}
220
+ {clearButtonLabel || translate("clear")}
237
221
  </span>
238
222
  <XMarkIcon aria-hidden />
239
223
  </button>
@@ -3,6 +3,7 @@ import React, { forwardRef, useContext } from "react";
3
3
  import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
4
4
  import { Button, ButtonProps } from "../../button";
5
5
  import { composeEventHandlers } from "../../util/composeEventHandlers";
6
+ import { useI18n } from "../../util/i18n/i18n.context";
6
7
  import { SearchContext } from "./context";
7
8
 
8
9
  export interface SearchButtonProps
@@ -19,6 +20,7 @@ export type SearchButtonType = React.ForwardRefExoticComponent<
19
20
 
20
21
  const SearchButton: SearchButtonType = forwardRef(
21
22
  ({ className, children, disabled, onClick, ...rest }, ref) => {
23
+ const translate = useI18n("Search");
22
24
  const context = useContext(SearchContext);
23
25
 
24
26
  if (context === null) {
@@ -40,7 +42,9 @@ const SearchButton: SearchButtonType = forwardRef(
40
42
  onClick={composeEventHandlers(onClick, handleClick)}
41
43
  icon={
42
44
  <MagnifyingGlassIcon
43
- {...(children ? { "aria-hidden": true } : { title: "Søk" })}
45
+ {...(children
46
+ ? { "aria-hidden": true }
47
+ : { title: translate("search") })}
44
48
  />
45
49
  }
46
50
  >
@@ -2,6 +2,7 @@ import cl from "clsx";
2
2
  import React, { SVGProps, forwardRef } from "react";
3
3
  import { omit } from "../util";
4
4
  import { useId } from "../util/hooks";
5
+ import { useI18n } from "../util/i18n/i18n.context";
5
6
 
6
7
  export interface LoaderProps extends Omit<SVGProps<SVGSVGElement>, "ref"> {
7
8
  /**
@@ -19,7 +20,7 @@ export interface LoaderProps extends Omit<SVGProps<SVGSVGElement>, "ref"> {
19
20
  | "xsmall";
20
21
  /**
21
22
  * Title prop on svg
22
- * @default "venter..."
23
+ * @default "Venter…"
23
24
  */
24
25
  title?: React.ReactNode;
25
26
  /**
@@ -47,7 +48,7 @@ export type LoaderType = React.ForwardRefExoticComponent<
47
48
  *
48
49
  * @example
49
50
  * ```jsx
50
- * <Loader size="3xlarge" title="Venter..." />
51
+ * <Loader size="3xlarge" title="Venter" />
51
52
  * ```
52
53
  */
53
54
  export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
@@ -55,7 +56,7 @@ export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
55
56
  {
56
57
  className,
57
58
  size = "medium",
58
- title = "venter...",
59
+ title,
59
60
  transparent = false,
60
61
  variant = "neutral",
61
62
  id,
@@ -64,6 +65,7 @@ export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
64
65
  ref,
65
66
  ) => {
66
67
  const internalId = useId();
68
+ const translate = useI18n("Loader");
67
69
 
68
70
  return (
69
71
  <svg
@@ -83,7 +85,9 @@ export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
83
85
  preserveAspectRatio="xMidYMid"
84
86
  {...omit(rest, ["children"])}
85
87
  >
86
- <title id={id ?? `loader-${internalId}`}>{title}</title>
88
+ <title id={id ?? `loader-${internalId}`}>
89
+ {title || translate("title")}
90
+ </title>
87
91
  <circle
88
92
  className="navds-loader__background"
89
93
  xmlns="http://www.w3.org/2000/svg"
@@ -2,6 +2,7 @@ import cl from "clsx";
2
2
  import React, { forwardRef } from "react";
3
3
  import { XMarkIcon } from "@navikt/aksel-icons";
4
4
  import { Button } from "../button";
5
+ import { useI18n } from "../util/i18n/i18n.context";
5
6
  import { useModalContext } from "./Modal.context";
6
7
 
7
8
  export interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -16,6 +17,7 @@ export interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
16
17
  const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
17
18
  ({ children, className, closeButton = true, ...rest }, ref) => {
18
19
  const context = useModalContext();
20
+ const translate = useI18n("Modal");
19
21
 
20
22
  return (
21
23
  <div {...rest} ref={ref} className={cl("navds-modal__header", className)}>
@@ -32,7 +34,7 @@ const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
32
34
  }
33
35
  }}
34
36
  onClick={context.closeHandler}
35
- icon={<XMarkIcon title="Lukk" />}
37
+ icon={<XMarkIcon title={translate("close")} />}
36
38
  />
37
39
  )}
38
40
  {children}
@@ -3,6 +3,7 @@ import React, { forwardRef } from "react";
3
3
  import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
4
4
  import { BodyShort, Heading } from "../typography";
5
5
  import { useId } from "../util";
6
+ import { useI18n } from "../util/i18n/i18n.context";
6
7
  import PaginationItem, {
7
8
  PaginationItemProps,
8
9
  PaginationItemType,
@@ -138,6 +139,7 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
138
139
  ref,
139
140
  ) => {
140
141
  const headingId = useId();
142
+ const translate = useI18n("Pagination");
141
143
 
142
144
  if (page < 1) {
143
145
  console.error("page cannot be less than 1");
@@ -194,11 +196,11 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
194
196
  <ChevronLeftIcon
195
197
  {...(prevNextTexts
196
198
  ? { "aria-hidden": true }
197
- : { title: "Forrige" })}
199
+ : { title: translate("previous") })}
198
200
  />
199
201
  }
200
202
  >
201
- {prevNextTexts && `Forrige`}
203
+ {prevNextTexts && translate("previous")}
202
204
  </Item>
203
205
  </li>
204
206
  {getSteps({ page, count, siblingCount, boundaryCount }).map(
@@ -241,12 +243,12 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
241
243
  <ChevronRightIcon
242
244
  {...(prevNextTexts
243
245
  ? { "aria-hidden": true }
244
- : { title: "Neste" })}
246
+ : { title: translate("next") })}
245
247
  />
246
248
  }
247
249
  iconPosition="right"
248
250
  >
249
- {prevNextTexts && `Neste`}
251
+ {prevNextTexts && translate("next")}
250
252
  </Item>
251
253
  </li>
252
254
  </ul>
@@ -1,5 +1,6 @@
1
1
  import cl from "clsx";
2
- import React, { HTMLAttributes, forwardRef, useRef } from "react";
2
+ import React, { HTMLAttributes, forwardRef, useEffect, useRef } from "react";
3
+ import { useI18n } from "../util/i18n/i18n.context";
3
4
 
4
5
  interface ProgressBarPropsBase
5
6
  extends Omit<HTMLAttributes<HTMLDivElement>, "role"> {
@@ -92,11 +93,12 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
92
93
  },
93
94
  ref,
94
95
  ) => {
95
- const translate = 100 - (Math.round(value) / valueMax) * 100;
96
+ const translateX = 100 - (Math.round(value) / valueMax) * 100;
96
97
  const onTimeoutRef = useRef<() => void>();
97
98
  onTimeoutRef.current = simulated?.onTimeout;
99
+ const translate = useI18n("ProgressBar");
98
100
 
99
- React.useEffect(() => {
101
+ useEffect(() => {
100
102
  if (simulated?.seconds && onTimeoutRef.current) {
101
103
  const timeout = setTimeout(
102
104
  onTimeoutRef.current,
@@ -119,8 +121,15 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
119
121
  aria-valuenow={simulated?.seconds ? 0 : Math.round(value)}
120
122
  aria-valuetext={
121
123
  simulated?.seconds
122
- ? `Fremdrift kan ikke beregnes, antatt tid er: ${simulated?.seconds} sekunder`
123
- : `${Math.round(value)} av ${Math.round(valueMax)}`
124
+ ? translate("progressUnknown", {
125
+ replacements: { seconds: Math.round(simulated?.seconds) },
126
+ })
127
+ : translate("progress", {
128
+ replacements: {
129
+ current: Math.round(value),
130
+ max: Math.round(valueMax),
131
+ },
132
+ })
124
133
  }
125
134
  // biome-ignore lint/a11y/useAriaPropsForRole: We found that adding valueMin was not needed
126
135
  role="progressbar"
@@ -130,17 +139,15 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
130
139
  >
131
140
  <div
132
141
  className={cl("navds-progress-bar__foreground", {
133
- "navds-progress-bar__foreground--indeterminate": Number.isInteger(
134
- simulated?.seconds,
135
- ),
142
+ "navds-progress-bar__foreground--indeterminate":
143
+ simulated?.seconds !== undefined,
136
144
  })}
137
145
  style={{
138
- "--__ac-progress-bar-simulated": Number.isInteger(
139
- simulated?.seconds,
140
- )
141
- ? `${simulated?.seconds}s`
142
- : undefined,
143
- "--__ac-progress-bar-translate": `-${translate}%`,
146
+ "--__ac-progress-bar-simulated":
147
+ simulated?.seconds !== undefined
148
+ ? `${simulated?.seconds}s`
149
+ : undefined,
150
+ "--__ac-progress-bar-translate": `-${translateX}%`,
144
151
  }}
145
152
  />
146
153
  </div>
package/src/slot/Slot.tsx CHANGED
@@ -10,11 +10,16 @@ const Slot = React.forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
10
10
  const { children, ...slotProps } = props;
11
11
 
12
12
  if (React.isValidElement(children)) {
13
+ const childRef = Object.prototype.propertyIsEnumerable.call(
14
+ children.props,
15
+ "ref",
16
+ )
17
+ ? children.props.ref // React 19 (children.ref still works, but gives a warning)
18
+ : (children as any).ref; // React <19
19
+
13
20
  return React.cloneElement<any>(children, {
14
21
  ...mergeProps(slotProps, children.props),
15
- ref: forwardedRef
16
- ? mergeRefs([forwardedRef, (children as any).ref])
17
- : (children as any).ref,
22
+ ref: forwardedRef ? mergeRefs([forwardedRef, childRef]) : childRef,
18
23
  });
19
24
  }
20
25
 
@@ -4,6 +4,7 @@ import { ChevronDownIcon } from "@navikt/aksel-icons";
4
4
  import { composeEventHandlers } from "../util/composeEventHandlers";
5
5
  import { useId } from "../util/hooks";
6
6
  import { useControllableState } from "../util/hooks/useControllableState";
7
+ import { useI18n } from "../util/i18n/i18n.context";
7
8
  import AnimateHeight from "./AnimateHeight";
8
9
  import DataCell from "./DataCell";
9
10
  import Row, { RowProps } from "./Row";
@@ -20,7 +21,7 @@ export interface ExpandableRowProps extends Omit<RowProps, "content"> {
20
21
  togglePlacement?: "left" | "right";
21
22
  /**
22
23
  * Opens component if 'true', closes if 'false'
23
- * Using this props removes automatic control of open-state
24
+ * Using this prop removes automatic control of open-state
24
25
  */
25
26
  open?: boolean;
26
27
  /**
@@ -76,21 +77,19 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
76
77
  value: open,
77
78
  onChange: onOpenChange,
78
79
  });
79
-
80
+ const translate = useI18n("global");
80
81
  const id = useId();
81
82
 
82
- const expansionHandler = (e) => {
83
- _setOpen((x) => !x);
84
- e.stopPropagation();
83
+ const expansionHandler = (event: React.MouseEvent<HTMLElement>) => {
84
+ _setOpen((oldOpen) => !oldOpen);
85
+ event.stopPropagation();
85
86
  };
86
87
 
87
- const onRowClick = (e) =>
88
- !isInteractiveTarget(e.target) && expansionHandler(e);
89
-
90
- const handleRowClick = (
91
- e: React.MouseEvent<HTMLTableRowElement, MouseEvent>,
92
- ) => {
93
- !expansionDisabled && expandOnRowClick && onRowClick(e);
88
+ const handleRowClick = (event: React.MouseEvent<HTMLTableRowElement>) => {
89
+ expandOnRowClick &&
90
+ !expansionDisabled &&
91
+ !isInteractiveTarget(event.target as HTMLElement) &&
92
+ expansionHandler(event);
94
93
  };
95
94
 
96
95
  return (
@@ -123,7 +122,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
123
122
  >
124
123
  <ChevronDownIcon
125
124
  className="navds-table__expandable-icon"
126
- title={_open ? "Vis mindre" : "Vis mer"}
125
+ title={_open ? translate("showLess") : translate("showMore")}
127
126
  />
128
127
  </button>
129
128
  )}
@@ -17,11 +17,7 @@ function ScrollButton({ hidden, onClick, dir }: ScrollButtonProps) {
17
17
  onClick={onClick}
18
18
  aria-hidden
19
19
  >
20
- {dir === "left" ? (
21
- <ChevronLeftIcon title="scroll tilbake" />
22
- ) : (
23
- <ChevronRightIcon title="scroll neste" />
24
- )}
20
+ {dir === "left" ? <ChevronLeftIcon /> : <ChevronRightIcon />}
25
21
  </div>
26
22
  );
27
23
  }