@navikt/ds-react 7.4.0 → 7.4.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 (119) 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/combobox/FilteredOptions/useVirtualFocus.js +1 -1
  6. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  7. package/cjs/form/combobox/Input/Input.js +17 -4
  8. package/cjs/form/combobox/Input/Input.js.map +1 -1
  9. package/cjs/form/error-summary/ErrorSummary.js +4 -2
  10. package/cjs/form/error-summary/ErrorSummary.js.map +1 -1
  11. package/cjs/form/form-progress/FormProgress.js +1 -3
  12. package/cjs/form/form-progress/FormProgress.js.map +1 -1
  13. package/cjs/form/search/Search.d.ts +1 -2
  14. package/cjs/form/search/Search.js +20 -20
  15. package/cjs/form/search/Search.js.map +1 -1
  16. package/cjs/form/search/SearchButton.js +5 -1
  17. package/cjs/form/search/SearchButton.js.map +1 -1
  18. package/cjs/form/textarea/Textarea.js +1 -3
  19. package/cjs/form/textarea/Textarea.js.map +1 -1
  20. package/cjs/form/textarea/TextareaCounter.d.ts +2 -1
  21. package/cjs/form/textarea/TextareaCounter.js +14 -9
  22. package/cjs/form/textarea/TextareaCounter.js.map +1 -1
  23. package/cjs/loader/Loader.d.ts +2 -2
  24. package/cjs/loader/Loader.js +5 -3
  25. package/cjs/loader/Loader.js.map +1 -1
  26. package/cjs/modal/ModalHeader.js +3 -1
  27. package/cjs/modal/ModalHeader.js.map +1 -1
  28. package/cjs/pagination/Pagination.js +4 -2
  29. package/cjs/pagination/Pagination.js.map +1 -1
  30. package/cjs/progress-bar/ProgressBar.js +14 -7
  31. package/cjs/progress-bar/ProgressBar.js.map +1 -1
  32. package/cjs/table/ExpandableRow.d.ts +1 -1
  33. package/cjs/table/ExpandableRow.js +11 -7
  34. package/cjs/table/ExpandableRow.js.map +1 -1
  35. package/cjs/tabs/parts/tablist/ScrollButtons.js +1 -1
  36. package/cjs/tabs/parts/tablist/ScrollButtons.js.map +1 -1
  37. package/cjs/util/i18n/i18n.context.d.ts +1 -3
  38. package/cjs/util/i18n/i18n.context.js +5 -5
  39. package/cjs/util/i18n/i18n.context.js.map +1 -1
  40. package/cjs/util/i18n/locales/en.d.ts +30 -0
  41. package/cjs/util/i18n/locales/en.js +30 -0
  42. package/cjs/util/i18n/locales/en.js.map +1 -1
  43. package/cjs/util/i18n/locales/nb.d.ts +31 -0
  44. package/cjs/util/i18n/locales/nb.js +31 -0
  45. package/cjs/util/i18n/locales/nb.js.map +1 -1
  46. package/cjs/util/i18n/locales/nn.d.ts +30 -0
  47. package/cjs/util/i18n/locales/nn.js +30 -0
  48. package/cjs/util/i18n/locales/nn.js.map +1 -1
  49. package/esm/date/utils/parse-date.js +1 -0
  50. package/esm/date/utils/parse-date.js.map +1 -1
  51. package/esm/expansion-card/ExpansionCardHeader.js +4 -2
  52. package/esm/expansion-card/ExpansionCardHeader.js.map +1 -1
  53. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js +1 -1
  54. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  55. package/esm/form/combobox/Input/Input.js +17 -4
  56. package/esm/form/combobox/Input/Input.js.map +1 -1
  57. package/esm/form/error-summary/ErrorSummary.js +4 -2
  58. package/esm/form/error-summary/ErrorSummary.js.map +1 -1
  59. package/esm/form/form-progress/FormProgress.js +1 -3
  60. package/esm/form/form-progress/FormProgress.js.map +1 -1
  61. package/esm/form/search/Search.d.ts +1 -2
  62. package/esm/form/search/Search.js +21 -21
  63. package/esm/form/search/Search.js.map +1 -1
  64. package/esm/form/search/SearchButton.js +5 -1
  65. package/esm/form/search/SearchButton.js.map +1 -1
  66. package/esm/form/textarea/Textarea.js +1 -3
  67. package/esm/form/textarea/Textarea.js.map +1 -1
  68. package/esm/form/textarea/TextareaCounter.d.ts +2 -1
  69. package/esm/form/textarea/TextareaCounter.js +14 -9
  70. package/esm/form/textarea/TextareaCounter.js.map +1 -1
  71. package/esm/loader/Loader.d.ts +2 -2
  72. package/esm/loader/Loader.js +5 -3
  73. package/esm/loader/Loader.js.map +1 -1
  74. package/esm/modal/ModalHeader.js +3 -1
  75. package/esm/modal/ModalHeader.js.map +1 -1
  76. package/esm/pagination/Pagination.js +4 -2
  77. package/esm/pagination/Pagination.js.map +1 -1
  78. package/esm/progress-bar/ProgressBar.js +15 -8
  79. package/esm/progress-bar/ProgressBar.js.map +1 -1
  80. package/esm/table/ExpandableRow.d.ts +1 -1
  81. package/esm/table/ExpandableRow.js +11 -7
  82. package/esm/table/ExpandableRow.js.map +1 -1
  83. package/esm/tabs/parts/tablist/ScrollButtons.js +1 -1
  84. package/esm/tabs/parts/tablist/ScrollButtons.js.map +1 -1
  85. package/esm/util/i18n/i18n.context.d.ts +1 -3
  86. package/esm/util/i18n/i18n.context.js +5 -5
  87. package/esm/util/i18n/i18n.context.js.map +1 -1
  88. package/esm/util/i18n/locales/en.d.ts +30 -0
  89. package/esm/util/i18n/locales/en.js +30 -0
  90. package/esm/util/i18n/locales/en.js.map +1 -1
  91. package/esm/util/i18n/locales/nb.d.ts +31 -0
  92. package/esm/util/i18n/locales/nb.js +31 -0
  93. package/esm/util/i18n/locales/nb.js.map +1 -1
  94. package/esm/util/i18n/locales/nn.d.ts +30 -0
  95. package/esm/util/i18n/locales/nn.js +30 -0
  96. package/esm/util/i18n/locales/nn.js.map +1 -1
  97. package/package.json +3 -3
  98. package/src/date/utils/parse-date.ts +1 -0
  99. package/src/expansion-card/ExpansionCardHeader.tsx +4 -2
  100. package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +1 -1
  101. package/src/form/combobox/Input/Input.tsx +27 -9
  102. package/src/form/combobox/__tests__/combobox.test.tsx +39 -0
  103. package/src/form/error-summary/ErrorSummary.tsx +4 -2
  104. package/src/form/form-progress/FormProgress.tsx +1 -3
  105. package/src/form/search/Search.tsx +20 -36
  106. package/src/form/search/SearchButton.tsx +5 -1
  107. package/src/form/textarea/Textarea.tsx +7 -11
  108. package/src/form/textarea/TextareaCounter.tsx +31 -7
  109. package/src/loader/Loader.tsx +8 -4
  110. package/src/modal/ModalHeader.tsx +3 -1
  111. package/src/pagination/Pagination.tsx +6 -4
  112. package/src/progress-bar/ProgressBar.tsx +19 -14
  113. package/src/table/ExpandableRow.tsx +12 -13
  114. package/src/tabs/parts/tablist/ScrollButtons.tsx +1 -5
  115. package/src/util/i18n/i18n.context.test.tsx +4 -6
  116. package/src/util/i18n/i18n.context.ts +5 -5
  117. package/src/util/i18n/locales/en.ts +32 -0
  118. package/src/util/i18n/locales/nb.ts +33 -0
  119. package/src/util/i18n/locales/nn.ts +32 -0
@@ -3,6 +3,7 @@ import React, { HTMLAttributes, forwardRef, useRef } from "react";
3
3
  import { BodyShort, Heading } from "../../typography";
4
4
  import { composeEventHandlers } from "../../util/composeEventHandlers";
5
5
  import { useMergeRefs } from "../../util/hooks";
6
+ import { useI18n } from "../../util/i18n/i18n.context";
6
7
  import ErrorSummaryItem from "./ErrorSummaryItem";
7
8
 
8
9
  export interface ErrorSummaryProps
@@ -72,11 +73,12 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
72
73
  className,
73
74
  size = "medium",
74
75
  headingTag = "h2",
75
- heading = "Du må rette disse feilene før du kan fortsette:",
76
+ heading,
76
77
  ...rest
77
78
  },
78
79
  ref,
79
80
  ) => {
81
+ const translate = useI18n("ErrorSummary");
80
82
  const wrapperRef = useRef<HTMLDivElement>(null);
81
83
  const headingRef = useRef<HTMLHeadingElement>(null);
82
84
 
@@ -105,7 +107,7 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
105
107
  ref={headingRef}
106
108
  tabIndex={-1}
107
109
  >
108
- {heading}
110
+ {heading ?? translate("heading")}
109
111
  </Heading>
110
112
  <BodyShort as="ul" size={size} className="navds-error-summary__list">
111
113
  {children}
@@ -105,9 +105,7 @@ export const FormProgress = forwardRef<HTMLDivElement, FormProgressProps>(
105
105
  <Collapsible lazy open={open} onOpenChange={onOpenChange}>
106
106
  <HStack justify="space-between" align="center">
107
107
  <BodyShort as="span">
108
- {translate("step", {
109
- replacements: { activeStep, totalSteps },
110
- })}
108
+ {translate("step", { activeStep, totalSteps })}
111
109
  </BodyShort>
112
110
  <Collapsible.Trigger asChild aria-expanded={undefined}>
113
111
  <Button
@@ -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
  >
@@ -180,17 +180,13 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
180
180
  {...(describedBy ? { "aria-describedby": describedBy } : {})}
181
181
  />
182
182
  {hasMaxLength && !readOnly && !inputProps.disabled && (
183
- <>
184
- <span id={maxLengthId} className="navds-sr-only">
185
- {`Tekstområde med plass til ${maxLength} tegn.`}
186
- </span>
187
- <Counter
188
- maxLength={maxLength}
189
- currentLength={props.value?.length ?? uncontrolledValue.length}
190
- size={size}
191
- i18n={i18n}
192
- />
193
- </>
183
+ <Counter
184
+ maxLengthId={maxLengthId}
185
+ maxLength={maxLength}
186
+ currentLength={props.value?.length ?? uncontrolledValue.length}
187
+ size={size}
188
+ i18n={i18n}
189
+ />
194
190
  )}
195
191
  <div
196
192
  className="navds-form-field__error"
@@ -2,18 +2,32 @@ import cl from "clsx";
2
2
  import React, { useEffect, useState } from "react";
3
3
  import { BodyShort } from "../../typography";
4
4
  import debounce from "../../util/debounce";
5
+ import { useI18n } from "../../util/i18n/i18n.context";
5
6
  import type { TextareaProps } from "./Textarea";
6
7
 
7
8
  interface Props {
9
+ maxLengthId: string;
8
10
  maxLength: number;
9
11
  currentLength: number;
10
12
  size: TextareaProps["size"];
11
13
  i18n: TextareaProps["i18n"];
12
14
  }
13
15
 
14
- const TextareaCounter = ({ maxLength, currentLength, size, i18n }: Props) => {
15
- const difference = maxLength - currentLength;
16
+ const TextareaCounter = ({
17
+ maxLengthId,
18
+ maxLength,
19
+ currentLength,
20
+ size,
21
+ i18n,
22
+ }: Props) => {
23
+ const translate = useI18n("Textarea", {
24
+ charsLeft: i18n?.counterLeft ? `{chars} ${i18n.counterLeft}` : undefined,
25
+ charsTooMany: i18n?.counterTooMuch
26
+ ? `{chars} ${i18n.counterTooMuch}`
27
+ : undefined,
28
+ });
16
29
 
30
+ const difference = maxLength - currentLength;
17
31
  const [debouncedDiff, setDebouncedDiff] = useState(difference);
18
32
 
19
33
  useEffect(() => {
@@ -29,12 +43,16 @@ const TextareaCounter = ({ maxLength, currentLength, size, i18n }: Props) => {
29
43
 
30
44
  return (
31
45
  <>
46
+ <span id={maxLengthId} className="navds-sr-only">
47
+ {translate("maxLength", { maxLength })}
48
+ </span>
49
+
32
50
  {difference < 20 && (
33
51
  <span
34
52
  role="status"
35
53
  className="navds-textarea__sr-counter navds-sr-only"
36
54
  >
37
- {getCounterText(debouncedDiff, i18n)}
55
+ {getCounterText(debouncedDiff, translate)}
38
56
  </span>
39
57
  )}
40
58
 
@@ -44,15 +62,21 @@ const TextareaCounter = ({ maxLength, currentLength, size, i18n }: Props) => {
44
62
  })}
45
63
  size={size}
46
64
  >
47
- {getCounterText(difference, i18n)}
65
+ {getCounterText(difference, translate)}
48
66
  </BodyShort>
49
67
  </>
50
68
  );
51
69
  };
52
70
 
53
- const getCounterText = (difference: number, i18n: TextareaProps["i18n"]) =>
71
+ const getCounterText = (
72
+ difference: number,
73
+ translate: (
74
+ key: "charsTooMany" | "charsLeft",
75
+ replacements?: { chars: number },
76
+ ) => string,
77
+ ) =>
54
78
  difference < 0
55
- ? `${Math.abs(difference)} ${i18n?.counterTooMuch ?? "tegn for mye"}`
56
- : `${difference} ${i18n?.counterLeft ?? "tegn igjen"}`;
79
+ ? translate("charsTooMany", { chars: Math.abs(difference) })
80
+ : translate("charsLeft", { chars: difference });
57
81
 
58
82
  export default TextareaCounter;
@@ -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,13 @@ 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
+ seconds: Math.round(simulated?.seconds),
126
+ })
127
+ : translate("progress", {
128
+ current: Math.round(value),
129
+ max: Math.round(valueMax),
130
+ })
124
131
  }
125
132
  // biome-ignore lint/a11y/useAriaPropsForRole: We found that adding valueMin was not needed
126
133
  role="progressbar"
@@ -130,17 +137,15 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
130
137
  >
131
138
  <div
132
139
  className={cl("navds-progress-bar__foreground", {
133
- "navds-progress-bar__foreground--indeterminate": Number.isInteger(
134
- simulated?.seconds,
135
- ),
140
+ "navds-progress-bar__foreground--indeterminate":
141
+ simulated?.seconds !== undefined,
136
142
  })}
137
143
  style={{
138
- "--__ac-progress-bar-simulated": Number.isInteger(
139
- simulated?.seconds,
140
- )
141
- ? `${simulated?.seconds}s`
142
- : undefined,
143
- "--__ac-progress-bar-translate": `-${translate}%`,
144
+ "--__ac-progress-bar-simulated":
145
+ simulated?.seconds !== undefined
146
+ ? `${simulated?.seconds}s`
147
+ : undefined,
148
+ "--__ac-progress-bar-translate": `-${translateX}%`,
144
149
  }}
145
150
  />
146
151
  </div>
@@ -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
  }
@@ -79,17 +79,15 @@ describe("useI18n", () => {
79
79
  };
80
80
  const { result } = renderHook(() => useI18n("FileUpload", i18n));
81
81
  const translate = result.current;
82
- expect(
83
- translate("item.uploading", { replacements: { name: "John", cnt: 3 } }),
84
- ).toBe("Hello, John. You have 3 messages.");
82
+ expect(translate("item.uploading", { name: "John", cnt: 3 })).toBe(
83
+ "Hello, John. You have 3 messages.",
84
+ );
85
85
  });
86
86
 
87
87
  test("should throw an error if replacement key is not found", () => {
88
88
  const i18n = { item: { uploading: "Hello, {name}" } };
89
89
  const { result } = renderHook(() => useI18n("FileUpload", i18n));
90
90
  const translate = result.current;
91
- expect(() =>
92
- translate("item.uploading", { replacements: { other: "John" } }),
93
- ).toThrowError();
91
+ expect(() => translate("item.uploading", { other: "John" })).toThrowError();
94
92
  });
95
93
  });
@@ -27,7 +27,7 @@ export function useI18n<T extends Component>(
27
27
  */
28
28
  const translate = (
29
29
  keypath: NestedKeyOf<Translations[T]>,
30
- options?: { replacements: Record<string, string | number> },
30
+ replacements?: Record<string, string | number>,
31
31
  ) => {
32
32
  const text = get(
33
33
  keypath,
@@ -37,19 +37,19 @@ export function useI18n<T extends Component>(
37
37
  : [i18n[componentName]]),
38
38
  );
39
39
 
40
- if (options?.replacements) {
40
+ if (replacements) {
41
41
  return text.replace(REPLACE_REGEX, (match) => {
42
42
  const replacement = match.substring(1, match.length - 1);
43
43
 
44
- if (options.replacements[replacement] === undefined) {
45
- const replacementData = JSON.stringify(options.replacements);
44
+ if (replacements[replacement] === undefined) {
45
+ const replacementData = JSON.stringify(replacements);
46
46
 
47
47
  throw new Error(
48
48
  `Error translating key '${keypath}'. No replacement syntax ({}) found for key '${replacement}'. The following replacements were passed: '${replacementData}'`,
49
49
  );
50
50
  }
51
51
 
52
- return options.replacements[replacement] as string; // can also be a number, but JS doesn't mind...
52
+ return replacements[replacement] as string; // can also be a number, but JS doesn't mind...
53
53
  });
54
54
  }
55
55
 
@@ -1,6 +1,11 @@
1
1
  import type { Translations } from "../i18n.types";
2
2
 
3
3
  export default {
4
+ global: {
5
+ showMore: "Show more",
6
+ showLess: "Show less",
7
+ },
8
+
4
9
  FileUpload: {
5
10
  dropzone: {
6
11
  button: "Choose file",
@@ -37,4 +42,31 @@ export default {
37
42
  labelSuffix: "delete",
38
43
  },
39
44
  },
45
+ ErrorSummary: {
46
+ heading: "You must correct the following errors before you can continue:",
47
+ },
48
+ Loader: {
49
+ title: "Waiting…",
50
+ },
51
+ Modal: {
52
+ close: "Close",
53
+ },
54
+ Pagination: {
55
+ previous: "Previous",
56
+ next: "Next",
57
+ },
58
+ ProgressBar: {
59
+ progress: "{current} of {max}",
60
+ progressUnknown:
61
+ "Progress is unknown, estimated time is {seconds} seconds.",
62
+ },
63
+ Search: {
64
+ clear: "Clear",
65
+ search: "Search",
66
+ },
67
+ Textarea: {
68
+ maxLength: "Text area with a {maxLength} character limit.",
69
+ charsTooMany: "{chars} characters too many",
70
+ charsLeft: "{chars} characters left",
71
+ },
40
72
  } satisfies Translations;