@digdir/designsystemet-react 1.11.0 → 1.12.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 (190) hide show
  1. package/dist/cjs/components/Combobox/Combobox.js +1 -0
  2. package/dist/cjs/components/Combobox/Option/useComboboxOption.js +1 -0
  3. package/dist/cjs/components/Combobox/useComboboxKeyboard.js +1 -0
  4. package/dist/cjs/components/avatar/avatar.js +2 -2
  5. package/dist/cjs/components/breadcrumbs/breadcrumbs-link.js +2 -1
  6. package/dist/cjs/components/breadcrumbs/breadcrumbs-list.js +1 -11
  7. package/dist/cjs/components/breadcrumbs/breadcrumbs.js +2 -1
  8. package/dist/cjs/components/button/button.js +8 -3
  9. package/dist/cjs/components/card/card.js +17 -9
  10. package/dist/cjs/components/details/details-summary.js +3 -3
  11. package/dist/cjs/components/details/details.js +2 -2
  12. package/dist/cjs/components/dialog/dialog-trigger-context.js +5 -6
  13. package/dist/cjs/components/dialog/dialog-trigger.js +3 -8
  14. package/dist/cjs/components/dialog/dialog.js +25 -55
  15. package/dist/cjs/components/error-summary/error-summary-heading.js +2 -8
  16. package/dist/cjs/components/error-summary/error-summary.js +4 -9
  17. package/dist/cjs/components/field/field-counter.js +6 -41
  18. package/dist/cjs/components/field/field-description.js +2 -1
  19. package/dist/cjs/components/field/field.js +6 -4
  20. package/dist/cjs/components/label/label.js +2 -1
  21. package/dist/cjs/components/pagination/pagination-button.js +5 -3
  22. package/dist/cjs/components/pagination/pagination.js +7 -3
  23. package/dist/cjs/components/popover/popover-trigger.js +6 -10
  24. package/dist/cjs/components/popover/popover.js +15 -62
  25. package/dist/cjs/components/select/select.js +2 -12
  26. package/dist/cjs/components/skeleton/skeleton.js +1 -0
  27. package/dist/cjs/components/spinner/spinner.js +1 -0
  28. package/dist/cjs/components/suggestion/suggestion-clear.js +4 -2
  29. package/dist/cjs/components/suggestion/suggestion-empty.js +3 -1
  30. package/dist/cjs/components/suggestion/suggestion-input.js +4 -3
  31. package/dist/cjs/components/suggestion/suggestion-list.js +5 -41
  32. package/dist/cjs/components/suggestion/suggestion-option.js +3 -1
  33. package/dist/cjs/components/suggestion/suggestion.js +9 -9
  34. package/dist/cjs/components/tabs/tabs-list.js +4 -7
  35. package/dist/cjs/components/tabs/tabs-panel.js +5 -28
  36. package/dist/cjs/components/tabs/tabs-tab.js +11 -9
  37. package/dist/cjs/components/tabs/tabs.js +16 -6
  38. package/dist/cjs/components/toggle-group/index.js +1 -1
  39. package/dist/cjs/components/toggle-group/toggle-group-item.js +8 -6
  40. package/dist/cjs/components/toggle-group/toggle-group.js +6 -6
  41. package/dist/cjs/components/tooltip/tooltip.js +6 -147
  42. package/dist/cjs/components/validation-message/validation-message.js +2 -1
  43. package/dist/cjs/index.js +39 -38
  44. package/dist/cjs/utilities/hooks/use-pagination/use-pagination.js +13 -25
  45. package/dist/cjs/utilities/index.js +17 -0
  46. package/dist/cjs/utilities/roving-focus/roving-focus-item.js +2 -0
  47. package/dist/cjs/utilities/roving-focus/roving-focus-root.js +4 -0
  48. package/dist/cjs/utilities/roving-focus/use-roving-focus.js +3 -1
  49. package/dist/esm/components/Combobox/Combobox.js +1 -0
  50. package/dist/esm/components/Combobox/Option/useComboboxOption.js +1 -0
  51. package/dist/esm/components/Combobox/useComboboxKeyboard.js +1 -0
  52. package/dist/esm/components/avatar/avatar.js +2 -2
  53. package/dist/esm/components/breadcrumbs/breadcrumbs-link.js +2 -1
  54. package/dist/esm/components/breadcrumbs/breadcrumbs-list.js +2 -12
  55. package/dist/esm/components/breadcrumbs/breadcrumbs.js +2 -1
  56. package/dist/esm/components/button/button.js +9 -4
  57. package/dist/esm/components/card/card.js +18 -10
  58. package/dist/esm/components/details/details-summary.js +3 -3
  59. package/dist/esm/components/details/details.js +2 -2
  60. package/dist/esm/components/dialog/dialog-trigger-context.js +6 -7
  61. package/dist/esm/components/dialog/dialog-trigger.js +3 -8
  62. package/dist/esm/components/dialog/dialog.js +26 -56
  63. package/dist/esm/components/error-summary/error-summary-heading.js +3 -9
  64. package/dist/esm/components/error-summary/error-summary.js +6 -10
  65. package/dist/esm/components/field/field-counter.js +8 -43
  66. package/dist/esm/components/field/field-description.js +2 -1
  67. package/dist/esm/components/field/field.js +7 -5
  68. package/dist/esm/components/label/label.js +2 -1
  69. package/dist/esm/components/pagination/pagination-button.js +5 -3
  70. package/dist/esm/components/pagination/pagination.js +7 -3
  71. package/dist/esm/components/popover/popover-trigger.js +6 -10
  72. package/dist/esm/components/popover/popover.js +15 -62
  73. package/dist/esm/components/select/select.js +2 -12
  74. package/dist/esm/components/skeleton/skeleton.js +1 -0
  75. package/dist/esm/components/spinner/spinner.js +1 -0
  76. package/dist/esm/components/suggestion/suggestion-clear.js +4 -2
  77. package/dist/esm/components/suggestion/suggestion-empty.js +3 -1
  78. package/dist/esm/components/suggestion/suggestion-input.js +4 -3
  79. package/dist/esm/components/suggestion/suggestion-list.js +5 -41
  80. package/dist/esm/components/suggestion/suggestion-option.js +3 -1
  81. package/dist/esm/components/suggestion/suggestion.js +9 -9
  82. package/dist/esm/components/tabs/tabs-list.js +5 -8
  83. package/dist/esm/components/tabs/tabs-panel.js +6 -29
  84. package/dist/esm/components/tabs/tabs-tab.js +12 -10
  85. package/dist/esm/components/tabs/tabs.js +17 -7
  86. package/dist/esm/components/toggle-group/index.js +1 -1
  87. package/dist/esm/components/toggle-group/toggle-group-item.js +10 -8
  88. package/dist/esm/components/toggle-group/toggle-group.js +7 -7
  89. package/dist/esm/components/tooltip/tooltip.js +8 -149
  90. package/dist/esm/components/validation-message/validation-message.js +2 -1
  91. package/dist/esm/index.js +8 -7
  92. package/dist/esm/utilities/hooks/use-pagination/use-pagination.js +13 -25
  93. package/dist/esm/utilities/index.js +11 -0
  94. package/dist/esm/utilities/roving-focus/roving-focus-item.js +2 -0
  95. package/dist/esm/utilities/roving-focus/roving-focus-root.js +4 -0
  96. package/dist/esm/utilities/roving-focus/use-roving-focus.js +3 -1
  97. package/dist/react-types.d.ts +8 -0
  98. package/dist/types/components/avatar/avatar.d.ts +12 -7
  99. package/dist/types/components/avatar/avatar.d.ts.map +1 -1
  100. package/dist/types/components/breadcrumbs/breadcrumbs-link.d.ts.map +1 -1
  101. package/dist/types/components/breadcrumbs/breadcrumbs-list.d.ts.map +1 -1
  102. package/dist/types/components/breadcrumbs/breadcrumbs.d.ts +5 -3
  103. package/dist/types/components/breadcrumbs/breadcrumbs.d.ts.map +1 -1
  104. package/dist/types/components/button/button.d.ts +1 -1
  105. package/dist/types/components/button/button.d.ts.map +1 -1
  106. package/dist/types/components/card/card.d.ts.map +1 -1
  107. package/dist/types/components/details/details-summary.d.ts.map +1 -1
  108. package/dist/types/components/details/details.d.ts +1 -1
  109. package/dist/types/components/details/details.d.ts.map +1 -1
  110. package/dist/types/components/dialog/dialog-trigger-context.d.ts +10 -3
  111. package/dist/types/components/dialog/dialog-trigger-context.d.ts.map +1 -1
  112. package/dist/types/components/dialog/dialog-trigger.d.ts +1 -1
  113. package/dist/types/components/dialog/dialog-trigger.d.ts.map +1 -1
  114. package/dist/types/components/dialog/dialog.d.ts +3 -3
  115. package/dist/types/components/dialog/dialog.d.ts.map +1 -1
  116. package/dist/types/components/dropdown/dropdown.d.ts +1 -2
  117. package/dist/types/components/dropdown/dropdown.d.ts.map +1 -1
  118. package/dist/types/components/error-summary/error-summary-heading.d.ts.map +1 -1
  119. package/dist/types/components/error-summary/error-summary.d.ts +6 -6
  120. package/dist/types/components/error-summary/error-summary.d.ts.map +1 -1
  121. package/dist/types/components/field/field-counter.d.ts +2 -8
  122. package/dist/types/components/field/field-counter.d.ts.map +1 -1
  123. package/dist/types/components/field/field-description.d.ts.map +1 -1
  124. package/dist/types/components/field/field.d.ts +6 -2
  125. package/dist/types/components/field/field.d.ts.map +1 -1
  126. package/dist/types/components/index.d.ts +1 -0
  127. package/dist/types/components/index.d.ts.map +1 -1
  128. package/dist/types/components/input/input.d.ts +13 -1
  129. package/dist/types/components/input/input.d.ts.map +1 -1
  130. package/dist/types/components/label/label.d.ts.map +1 -1
  131. package/dist/types/components/pagination/pagination-button.d.ts +13 -4
  132. package/dist/types/components/pagination/pagination-button.d.ts.map +1 -1
  133. package/dist/types/components/pagination/pagination.d.ts +27 -5
  134. package/dist/types/components/pagination/pagination.d.ts.map +1 -1
  135. package/dist/types/components/popover/popover-trigger.d.ts.map +1 -1
  136. package/dist/types/components/popover/popover.d.ts +2 -14
  137. package/dist/types/components/popover/popover.d.ts.map +1 -1
  138. package/dist/types/components/search/search-button.d.ts +1 -1
  139. package/dist/types/components/select/select.d.ts +2 -0
  140. package/dist/types/components/select/select.d.ts.map +1 -1
  141. package/dist/types/components/suggestion/suggestion-clear.d.ts +7 -5
  142. package/dist/types/components/suggestion/suggestion-clear.d.ts.map +1 -1
  143. package/dist/types/components/suggestion/suggestion-empty.d.ts +1 -0
  144. package/dist/types/components/suggestion/suggestion-empty.d.ts.map +1 -1
  145. package/dist/types/components/suggestion/suggestion-input.d.ts +2 -1
  146. package/dist/types/components/suggestion/suggestion-input.d.ts.map +1 -1
  147. package/dist/types/components/suggestion/suggestion-list.d.ts +1 -1
  148. package/dist/types/components/suggestion/suggestion-list.d.ts.map +1 -1
  149. package/dist/types/components/suggestion/suggestion-option.d.ts +1 -0
  150. package/dist/types/components/suggestion/suggestion-option.d.ts.map +1 -1
  151. package/dist/types/components/suggestion/suggestion.d.ts +5 -6
  152. package/dist/types/components/suggestion/suggestion.d.ts.map +1 -1
  153. package/dist/types/components/tabs/tabs-list.d.ts +4 -2
  154. package/dist/types/components/tabs/tabs-list.d.ts.map +1 -1
  155. package/dist/types/components/tabs/tabs-panel.d.ts +4 -2
  156. package/dist/types/components/tabs/tabs-panel.d.ts.map +1 -1
  157. package/dist/types/components/tabs/tabs-tab.d.ts +4 -2
  158. package/dist/types/components/tabs/tabs-tab.d.ts.map +1 -1
  159. package/dist/types/components/tabs/tabs.d.ts +6 -6
  160. package/dist/types/components/tabs/tabs.d.ts.map +1 -1
  161. package/dist/types/components/textfield/textfield.d.ts.map +1 -1
  162. package/dist/types/components/toggle-group/index.d.ts +1 -1
  163. package/dist/types/components/toggle-group/toggle-group-item.d.ts +12 -3
  164. package/dist/types/components/toggle-group/toggle-group-item.d.ts.map +1 -1
  165. package/dist/types/components/toggle-group/toggle-group.d.ts +12 -4
  166. package/dist/types/components/toggle-group/toggle-group.d.ts.map +1 -1
  167. package/dist/types/components/tooltip/tooltip.d.ts +10 -3
  168. package/dist/types/components/tooltip/tooltip.d.ts.map +1 -1
  169. package/dist/types/components/validation-message/validation-message.d.ts.map +1 -1
  170. package/dist/types/types.d.ts +2 -0
  171. package/dist/types/types.d.ts.map +1 -1
  172. package/dist/types/utilities/hooks/use-pagination/use-pagination.d.ts +1 -1
  173. package/dist/types/utilities/hooks/use-pagination/use-pagination.d.ts.map +1 -1
  174. package/dist/types/utilities/index.d.ts +6 -0
  175. package/dist/types/utilities/index.d.ts.map +1 -1
  176. package/dist/types/utilities/roving-focus/roving-focus-item.d.ts +1 -0
  177. package/dist/types/utilities/roving-focus/roving-focus-item.d.ts.map +1 -1
  178. package/dist/types/utilities/roving-focus/roving-focus-root.d.ts +1 -0
  179. package/dist/types/utilities/roving-focus/roving-focus-root.d.ts.map +1 -1
  180. package/dist/types/utilities/roving-focus/use-roving-focus.d.ts +14 -12
  181. package/dist/types/utilities/roving-focus/use-roving-focus.d.ts.map +1 -1
  182. package/package.json +16 -19
  183. package/dist/cjs/components/field/field-observer.js +0 -112
  184. package/dist/cjs/components/toggle-group/use-toggle-groupitem.js +0 -34
  185. package/dist/esm/components/field/field-observer.js +0 -107
  186. package/dist/esm/components/toggle-group/use-toggle-groupitem.js +0 -32
  187. package/dist/types/components/field/field-observer.d.ts +0 -5
  188. package/dist/types/components/field/field-observer.d.ts.map +0 -1
  189. package/dist/types/components/toggle-group/use-toggle-groupitem.d.ts +0 -12
  190. package/dist/types/components/toggle-group/use-toggle-groupitem.d.ts.map +0 -1
@@ -2,9 +2,13 @@
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
4
  import cl from 'clsx/lite';
5
- import { forwardRef, useRef, useEffect } from 'react';
5
+ import { forwardRef, useRef, useId, useEffect } from 'react';
6
6
  import { useMergeRefs } from '../../utilities/hooks/use-merge-refs/use-merge-refs.js';
7
+ import '@digdir/designsystemet-web';
7
8
 
9
+ const ATTR_CLICKDELEGATE = 'data-clickdelegatefor';
10
+ const SELECTOR_LINK = `:is(h1,h2,h3,h4,h5,h6) a`;
11
+ const SELECTOR_SKIP = 'a,button,label,details,dialog,[role="button"],[popover],[contenteditable]';
8
12
  /**
9
13
  * Card component to present content in a structured way.
10
14
  *
@@ -18,22 +22,26 @@ import { useMergeRefs } from '../../utilities/hooks/use-merge-refs/use-merge-ref
18
22
  const Card = forwardRef(function Card({ asChild = false, variant = 'default', className, ...rest }, ref) {
19
23
  const Component = asChild ? Slot : 'div';
20
24
  const cardRef = useRef(null);
25
+ const linkGeneratedId = useId();
21
26
  const mergedRefs = useMergeRefs([cardRef, ref]);
22
27
  // Forward click on card to heading links for better accessibility
23
28
  // https://adrianroselli.com/2020/02/block-links-cards-clickable-regions-etc.html
24
29
  useEffect(() => {
25
30
  const card = cardRef.current;
26
- const handleClick = ({ ctrlKey, metaKey, target }) => {
27
- const link = card?.querySelector(':is(h1,h2,h3,h4,h5,h6) a');
28
- if (!link || link?.contains(target))
29
- return; // Let links handle their own clicks
30
- if (ctrlKey || metaKey)
31
- window.open(link.href, '', 'noreferrer');
31
+ const link = card?.querySelector(SELECTOR_LINK);
32
+ const skip = !link || link.parentElement?.closest(SELECTOR_SKIP); // Using parentElement as link variable will always match a selector
33
+ const id = link?.id;
34
+ if (card?.hasAttribute(ATTR_CLICKDELEGATE) || skip)
35
+ return; // Already delegated or skipped
36
+ link.id = id || linkGeneratedId;
37
+ card?.setAttribute(ATTR_CLICKDELEGATE, link.id);
38
+ return () => {
39
+ if (id && link)
40
+ link.id = id;
32
41
  else
33
- link.click(); // Using link.click instead of window.location.href as this will trigger the browser's handling of rel=, target=, etc.
42
+ link?.removeAttribute('id');
43
+ card?.removeAttribute(ATTR_CLICKDELEGATE);
34
44
  };
35
- card?.addEventListener('click', handleClick);
36
- return () => card?.removeEventListener('click', handleClick);
37
45
  }, []);
38
46
  return (jsx(Component, { className: cl(`ds-card`, className), "data-variant": variant, ref: mergedRefs, ...rest }));
39
47
  });
@@ -8,9 +8,9 @@ import { forwardRef } from 'react';
8
8
  * @example
9
9
  * <Details.Summary>Heading</Details.Summary>
10
10
  */
11
- const DetailsSummary = forwardRef(function DetailsSummary({ className, ...rest }, ref) {
12
- /* Set `className` as `class` so react is happy */
13
- return jsx("u-summary", { ref: ref, class: className, ...rest });
11
+ const DetailsSummary = forwardRef(function DetailsSummary(rest, ref) {
12
+ return (jsx("summary", { suppressHydrationWarning // Since <details> polyfill adds attributes
13
+ : true, ref: ref, ...rest }));
14
14
  });
15
15
 
16
16
  export { DetailsSummary };
@@ -3,7 +3,7 @@ import { jsx } from 'react/jsx-runtime';
3
3
  import cl from 'clsx/lite';
4
4
  import { forwardRef, useRef, useEffect } from 'react';
5
5
  import { useMergeRefs } from '../../utilities/hooks/use-merge-refs/use-merge-refs.js';
6
- import '@u-elements/u-details';
6
+ import '@digdir/designsystemet-web';
7
7
 
8
8
  /**
9
9
  * Details component, contains `Details.Summary` and `Details.Content` components.
@@ -35,7 +35,7 @@ const Details = forwardRef(function Details({ className, open, defaultOpen = fal
35
35
  details?.addEventListener('toggle', handleToggle, true);
36
36
  return () => details?.removeEventListener('toggle', handleToggle, true);
37
37
  }, []);
38
- return (jsx("u-details", { class: cl('ds-details', className), open: (open ?? initialOpen.current) || undefined, "data-variant": variant, ref: mergedRefs, ...rest }));
38
+ return (jsx("details", { className: cl('ds-details', className), open: (open ?? initialOpen.current) || undefined, "data-variant": variant, ref: mergedRefs, ...rest }));
39
39
  });
40
40
 
41
41
  export { Details };
@@ -1,10 +1,8 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
- import { createContext, useRef } from 'react';
3
+ import { createContext, useState } from 'react';
4
4
 
5
- const Context = createContext({
6
- current: null,
7
- });
5
+ const Context = createContext({});
8
6
  /**
9
7
  * DialogTriggerContext component, used to provide a context for a dialog trigger.
10
8
  *
@@ -16,9 +14,10 @@ const Context = createContext({
16
14
  * </Dialog>
17
15
  * </Dialog.TriggerContext>
18
16
  */
19
- const DialogTriggerContext = ({ children, }) => {
20
- const contextRef = useRef(null);
21
- return jsx(Context.Provider, { value: contextRef, children: children });
17
+ const DialogTriggerContext = (rest) => {
18
+ const [state, setState] = useState({});
19
+ const setContext = (next) => setState({ ...state, ...next });
20
+ return jsx(Context.Provider, { value: { ...state, setContext }, ...rest });
22
21
  };
23
22
  DialogTriggerContext.displayName = 'DialogTriggerContext';
24
23
 
@@ -17,15 +17,10 @@ import { Context } from './dialog-trigger-context.js';
17
17
  * </Dialog.TriggerContext>
18
18
  */
19
19
  const DialogTrigger = forwardRef(function DialogTrigger({ asChild, ...rest }, ref) {
20
- const contextRef = useContext(Context);
20
+ const { id, modal } = useContext(Context);
21
21
  const Component = asChild ? Slot : Button;
22
- const openDialog = () => {
23
- /* check if element has `data-modal`, it it has, use `showModal` */
24
- contextRef.current?.getAttribute('data-modal') === 'true'
25
- ? contextRef.current?.showModal()
26
- : contextRef.current?.show();
27
- };
28
- return (jsx(Component, { "aria-haspopup": 'dialog', onClick: openDialog, ref: ref, ...rest }));
22
+ return (jsx(Component, { suppressHydrationWarning // Might get augmented through designsystemet-web with aria-haspopup
23
+ : true, command: modal ? 'show-modal' : 'show', commandfor: id, ref: ref, ...rest }));
29
24
  });
30
25
 
31
26
  export { DialogTrigger };
@@ -2,8 +2,9 @@
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
4
  import cl from 'clsx/lite';
5
- import { forwardRef, useContext, useRef, useEffect } from 'react';
5
+ import { forwardRef, useContext, useRef, useId, useEffect } from 'react';
6
6
  import { useMergeRefs } from '../../utilities/hooks/use-merge-refs/use-merge-refs.js';
7
+ import '@digdir/designsystemet-web';
7
8
  import { Button } from '../button/button.js';
8
9
  import { Context } from './dialog-trigger-context.js';
9
10
 
@@ -25,69 +26,38 @@ import { Context } from './dialog-trigger-context.js';
25
26
  *
26
27
  * ...
27
28
  *
28
- * <Button onClick={() => dialogRef.current?.showModal()}>Open Dialog</Button>
29
- * <Dialog ref={dialogRef}>
29
+ * <Button command="show-modal" commandfor="my-dialog">Open Dialog</Button>
30
+ * <Dialog id="my-dialog">
30
31
  * Content
31
32
  * </Dialog>
32
33
  */
33
- const Dialog = forwardRef(function Dialog({ asChild, children, className, placement = 'center', closeButton = 'Lukk dialogvindu', closedby = 'closerequest', modal = true, onClose, open, ...rest }, ref) {
34
- const contextRef = useContext(Context);
34
+ const Dialog = forwardRef(function Dialog({ asChild, children, className, closeButton = 'Lukk dialogvindu', id, modal = true, onAnimationEnd, onClick, onClose, open, placement = 'center', ...rest }, ref) {
35
+ const { setContext } = useContext(Context);
35
36
  const dialogRef = useRef(null); // This local ref is used to make sure the dialog works without a DialogTriggerContext
36
37
  const Component = asChild ? Slot : 'dialog';
37
- const mergedRefs = useMergeRefs([contextRef, ref, dialogRef]);
38
+ const mergedRefs = useMergeRefs([ref, dialogRef]);
38
39
  const showProp = modal ? 'showModal' : 'show';
39
- useEffect(() => dialogRef.current?.[open ? showProp : 'close'](), [open]); // Toggle open based on prop
40
- useEffect(() => {
41
- const dialog = dialogRef.current;
42
- const handleClosedby = (event) => {
43
- if (event.defaultPrevented)
44
- return; // Skip if default action is prevented
45
- const { clientY: y, clientX: x, target } = event;
46
- /* Check if clicked element or its closest parent has data-command='close' */
47
- if (target instanceof Element && event.type === 'click') {
48
- const closeElement = target.closest('[data-command="close"]');
49
- if (closeElement)
50
- return dialog?.close();
40
+ const autoId = useId();
41
+ const usedId = id ?? autoId;
42
+ // Toggle open based on prop
43
+ useEffect(() => dialogRef.current?.[open ? showProp : 'close'](), [open]);
44
+ // Store context for DialogTrigger to consume, so it can open the dialog when the trigger is clicked
45
+ useEffect(() => setContext?.({ id: usedId, modal }), [usedId, modal]);
46
+ return (jsxs(Component, { className: cl('ds-dialog', className), "data-placement": placement, id: usedId, onClose: (event) => onClose?.(event.nativeEvent), onClick: (event) => {
47
+ onClick?.(event);
48
+ const { currentTarget: dialog, target: el, defaultPrevented } = event;
49
+ const isClose = el?.closest?.('[data-command="close"]');
50
+ if (!defaultPrevented && isClose) {
51
+ dialog.close();
52
+ console.warn('Designsystemet: data-command="close" is deprecated. Use command="close" and commandfor="DIALOG-ID" instead.');
51
53
  }
52
- // if the browser supports closedBy, we let the browser handle it
53
- // see https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/closedBy
54
- if (dialog && 'closedBy' in dialog)
55
- return;
56
- if (event instanceof KeyboardEvent)
57
- return (closedby === 'none' &&
58
- event.key === 'Escape' &&
59
- event.preventDefault()); // Skip ESC-key if closedby="none"
60
- if (window.getSelection()?.toString())
61
- return; // Fix bug where if you select text spanning two divs it thinks you clicked outside
62
- if (dialog && target === dialog && closedby === 'any') {
63
- const { top, left, right, bottom } = dialog.getBoundingClientRect();
64
- const isInDialog = top <= y && y <= bottom && left <= x && x <= right;
65
- if (!isInDialog)
66
- dialog?.close(); // Both <dialog> and ::backdrop is considered same event.target
67
- }
68
- };
69
- const handleAutoFocus = () => {
70
- const autofocus = dialog?.querySelector('[autofocus]');
54
+ }, onAnimationEnd: (event) => {
55
+ const { currentTarget: dialog } = event;
56
+ const autofocus = dialog.querySelector('[autofocus]');
71
57
  if (document.activeElement !== autofocus)
72
- autofocus?.focus();
73
- };
74
- dialog?.addEventListener('animationend', handleAutoFocus);
75
- dialog?.addEventListener('click', handleClosedby);
76
- dialog?.addEventListener('keydown', handleClosedby);
77
- return () => {
78
- dialog?.removeEventListener('animationend', handleAutoFocus);
79
- dialog?.removeEventListener('click', handleClosedby);
80
- dialog?.removeEventListener('keydown', handleClosedby);
81
- };
82
- }, [closedby]);
83
- /* handle closing */
84
- useEffect(() => {
85
- const handleClose = (event) => onClose?.(event);
86
- const currentRef = dialogRef.current;
87
- currentRef?.addEventListener('close', handleClose);
88
- return () => currentRef?.removeEventListener('close', handleClose);
89
- }, [onClose]);
90
- return (jsxs(Component, { className: cl('ds-dialog', className), ref: mergedRefs, "data-placement": placement, "data-modal": modal, closedby: closedby, ...rest, children: [closeButton !== false && (jsx(Button, { "aria-label": closeButton, "data-color": 'neutral', icon: true, variant: 'tertiary', "data-command": 'close' })), children] }));
58
+ autofocus?.focus(); // Handle autofocus on open
59
+ onAnimationEnd?.(event);
60
+ }, ref: mergedRefs, ...rest, children: [closeButton !== false && (jsx(Button, { "aria-label": closeButton, "data-color": 'neutral', icon: true, variant: 'tertiary', command: 'close', commandfor: id ?? autoId })), children] }));
91
61
  });
92
62
 
93
63
  export { Dialog };
@@ -1,8 +1,7 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
- import { forwardRef, useContext, useEffect } from 'react';
3
+ import { forwardRef } from 'react';
4
4
  import { Heading } from '../heading/heading.js';
5
- import { ErrorSummaryContext } from './error-summary.js';
6
5
 
7
6
  /**
8
7
  * ErrorSummary heading component, used to display a heading for the error summary.
@@ -12,13 +11,8 @@ import { ErrorSummaryContext } from './error-summary.js';
12
11
  * <ErrorSummaryHeading>Heading</ErrorSummaryHeading>
13
12
  * </ErrorSummary>
14
13
  */
15
- const ErrorSummaryHeading = forwardRef(function ErrorSummaryHeading({ className, id, ...rest }, ref) {
16
- const { headingId, setHeadingId } = useContext(ErrorSummaryContext);
17
- useEffect(() => {
18
- if (id && headingId !== id)
19
- setHeadingId(id);
20
- }, [headingId, id, setHeadingId]);
21
- return jsx(Heading, { id: headingId, ref: ref, ...rest });
14
+ const ErrorSummaryHeading = forwardRef(function ErrorSummaryHeading(rest, ref) {
15
+ return jsx(Heading, { ref: ref, suppressHydrationWarning: true, ...rest }); // Suppress hydration warning since we will get an ID from <ds-error-summary>
22
16
  });
23
17
 
24
18
  export { ErrorSummaryHeading };
@@ -2,12 +2,8 @@
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
4
  import cl from 'clsx/lite';
5
- import { createContext, forwardRef, useId, useState } from 'react';
5
+ import { forwardRef } from 'react';
6
6
 
7
- const ErrorSummaryContext = createContext({
8
- headingId: 'heading',
9
- setHeadingId: () => { },
10
- });
11
7
  /**
12
8
  * ErrorSummary component, used to display a list of errors.
13
9
  *
@@ -25,10 +21,10 @@ const ErrorSummaryContext = createContext({
25
21
  * </ErrorSummary>
26
22
  */
27
23
  const ErrorSummary = forwardRef(function ErrorSummary({ asChild, className, ...rest }, ref) {
28
- const randomId = useId();
29
- const [headingId, setHeadingId] = useState(randomId);
30
- const Component = asChild ? Slot : 'div';
31
- return (jsx(ErrorSummaryContext.Provider, { value: { headingId, setHeadingId }, children: jsx(Component, { tabIndex: -1, "aria-labelledby": headingId, className: cl('ds-error-summary', className), ref: ref, ...rest }) }));
24
+ const Component = asChild ? Slot : 'ds-error-summary';
25
+ return (jsx(Component, { ...(asChild
26
+ ? { className: cl('ds-error-summary', className) }
27
+ : { class: cl('ds-error-summary', className) }), ref: ref, suppressHydrationWarning: true, ...rest }));
32
28
  });
33
29
 
34
- export { ErrorSummary, ErrorSummaryContext };
30
+ export { ErrorSummary };
@@ -1,14 +1,9 @@
1
1
  'use client';
2
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
3
- import { forwardRef, useState, useRef, useEffect } from 'react';
4
- import { useDebounceCallback } from '../../utilities/hooks/deprecated/use-debounce-callback/use-debounce-callback.js';
5
- import '../../utilities/roving-focus/roving-focus-item.js';
6
- import '../../utilities/roving-focus/roving-focus-root.js';
7
- import { Paragraph } from '../paragraph/paragraph.js';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { forwardRef } from 'react';
4
+ import { warn } from '../../utilities/index.js';
8
5
  import { ValidationMessage } from '../validation-message/validation-message.js';
9
- import { isInputLike } from './field-observer.js';
10
6
 
11
- const label = (text, count) => text.replace('%d', Math.abs(count).toString());
12
7
  /**
13
8
  * FieldCounter component, used to display a counter for a form field.
14
9
  *
@@ -18,41 +13,11 @@ const label = (text, count) => text.replace('%d', Math.abs(count).toString());
18
13
  * <Field.Counter limit={100} under='%d tegn igjen' over='%d tegn for mye' />
19
14
  * </Field>
20
15
  */
21
- const FieldCounter = forwardRef(function FieldCounter({ limit, under = '%d tegn igjen', over = '%d tegn for mye', hint = 'Maks %d tegn tillatt.', ...rest }, ref) {
22
- const [count, setCount] = useState(0);
23
- const [liveRegionText, setLiveRegionText] = useState('');
24
- const fieldInputRef = useRef(null);
25
- const counterRef = useRef(null);
26
- const hasExceededLimit = count > limit;
27
- const remainder = limit - count;
28
- const debouncedSetLiveRegionText = useDebounceCallback((text) => setLiveRegionText(text), 1200);
29
- // Listen to native input events (user typing) to update the counter in real time
30
- useEffect(() => {
31
- const field = counterRef.current?.closest('.ds-field');
32
- const input = Array.from(field?.getElementsByTagName('*') || []).find(isInputLike);
33
- const onInput = ({ target }) => {
34
- if (isInputLike(target))
35
- setCount(target.value.length);
36
- };
37
- if (input)
38
- onInput({ target: input }); // Initial setup
39
- fieldInputRef.current = input;
40
- field?.addEventListener('input', onInput);
41
- return () => field?.removeEventListener('input', onInput);
42
- }, []);
43
- /* React does not dispatch a native input event when the value prop changes externally.
44
- Since the parent re-renders this component when value changes, we can sync on render. */
45
- useEffect(() => {
46
- if (fieldInputRef.current) {
47
- const valueLength = fieldInputRef.current.value.length;
48
- setCount((prev) => (prev === valueLength ? prev : valueLength));
49
- }
50
- });
51
- // Update live region text when count or limit changes
52
- useEffect(() => {
53
- debouncedSetLiveRegionText(label(hasExceededLimit ? over : under, remainder));
54
- }, [count, limit, over, under, hasExceededLimit, remainder]);
55
- return (jsxs(Fragment, { children: [jsx("div", { className: 'ds-sr-only', "aria-live": 'polite', ref: counterRef, children: liveRegionText }), hasExceededLimit ? (jsx(ValidationMessage, { ref: ref, ...rest, "aria-hidden": 'true', "data-field": null, children: label(over, remainder) })) : (jsx(Paragraph, { ref: ref, ...rest, "aria-hidden": 'true', children: label(under, remainder) })), jsx("div", { className: 'ds-sr-only', "aria-hidden": 'true', "data-field": 'description', children: label(hint, limit) })] }));
16
+ const FieldCounter = forwardRef(function FieldCounter({ limit, under, over, hint, ...rest }, _ref) {
17
+ if (hint)
18
+ warn('hint attribute is deprecated on Field.Counter');
19
+ return (jsx(ValidationMessage, { suppressHydrationWarning // Since <ds-field> adds attributes
20
+ : true, "data-field": 'counter', "data-limit": limit, "data-under": under, "data-over": over, ...rest }));
56
21
  });
57
22
 
58
23
  export { FieldCounter };
@@ -9,7 +9,8 @@ import { forwardRef } from 'react';
9
9
  * <FieldDescription>Additional information</FieldDescription>
10
10
  */
11
11
  const FieldDescription = forwardRef(function FieldDescription(rest, ref) {
12
- return jsx("div", { "data-field": 'description', ref: ref, ...rest });
12
+ return (jsx("div", { suppressHydrationWarning // Since <ds-field> adds attributes
13
+ : true, "data-field": 'description', ref: ref, ...rest }));
13
14
  });
14
15
 
15
16
  export { FieldDescription };
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
+ import '@digdir/designsystemet-web';
3
4
  import { Slot } from '@radix-ui/react-slot';
4
5
  import cl from 'clsx/lite';
5
- import { forwardRef, useRef, useEffect } from 'react';
6
+ import { forwardRef, useRef } from 'react';
6
7
  import { useMergeRefs } from '../../utilities/hooks/use-merge-refs/use-merge-refs.js';
7
- import { fieldObserver } from './field-observer.js';
8
8
 
9
9
  /**
10
10
  * Field component, used to wrap a form field.
@@ -18,11 +18,13 @@ import { fieldObserver } from './field-observer.js';
18
18
  * </Field>
19
19
  */
20
20
  const Field = forwardRef(function Field({ className, position, asChild, ...rest }, ref) {
21
- const Component = asChild ? Slot : 'div';
21
+ const Component = asChild ? Slot : 'ds-field';
22
22
  const fieldRef = useRef(null);
23
23
  const mergedRefs = useMergeRefs([fieldRef, ref]);
24
- useEffect(() => fieldObserver(fieldRef.current), []);
25
- return (jsx(Component, { className: cl('ds-field', className), "data-position": position, ref: mergedRefs, ...rest }));
24
+ return (jsx(Component, { ...(asChild
25
+ ? { className: cl('ds-field', className) }
26
+ : { class: cl('ds-field', className) }), suppressHydrationWarning // Since <ds-field> adds attributes
27
+ : true, "data-position": position, ref: mergedRefs, ...rest }));
26
28
  });
27
29
 
28
30
  export { Field };
@@ -12,7 +12,8 @@ import { forwardRef } from 'react';
12
12
  */
13
13
  const Label = forwardRef(function Label({ className, weight = 'medium', asChild, ...rest }, ref) {
14
14
  const Component = asChild ? Slot : 'label';
15
- return (jsx(Component, { ref: ref, className: cl('ds-label', className), "data-weight": weight, ...rest }));
15
+ return (jsx(Component, { ref: ref, className: cl('ds-label', className), suppressHydrationWarning // Since <ds-field> will add for attribute dynamically
16
+ : true, "data-weight": weight, ...rest }));
16
17
  });
17
18
 
18
19
  export { Label };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
+ import { Slot } from '@radix-ui/react-slot';
3
4
  import { forwardRef } from 'react';
4
- import { Button } from '../button/button.js';
5
5
 
6
6
  /**
7
7
  * PaginationButton component, use within a Pagination.Item.
@@ -11,8 +11,10 @@ import { Button } from '../button/button.js';
11
11
  * <PaginationButton aria-label='Forrige side'>Forrige</PaginationButton>
12
12
  * </PaginationItem>
13
13
  */
14
- const PaginationButton = forwardRef(function PaginationButton(rest, ref) {
15
- return jsx(Button, { ref: ref, ...rest });
14
+ const PaginationButton = forwardRef(function PaginationButton({ asChild, ...rest }, ref) {
15
+ const Component = asChild ? Slot : 'button';
16
+ return (jsx(Component, { className: 'ds-button', "data-variant": 'tertiary', suppressHydrationWarning // Since <ds-pagination> adds attributes
17
+ : true, ref: ref, ...rest }));
16
18
  });
17
19
 
18
20
  export { PaginationButton };
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
+ import '@digdir/designsystemet-web';
3
4
  import { Slot } from '@radix-ui/react-slot';
4
5
  import cl from 'clsx/lite';
5
6
  import { forwardRef } from 'react';
@@ -22,9 +23,12 @@ import { forwardRef } from 'react';
22
23
  * </Pagination.List>
23
24
  * </Pagination>
24
25
  */
25
- const Pagination = forwardRef(function Pagination({ 'aria-label': ariaLabel = 'Sidenavigering', asChild, className, ...rest }, ref) {
26
- const Component = asChild ? Slot : 'nav';
27
- return (jsx(Component, { "aria-label": ariaLabel, className: cl('ds-pagination', className), ref: ref, ...rest }));
26
+ const Pagination = forwardRef(function Pagination({ asChild, className, ...rest }, ref) {
27
+ const Component = asChild ? Slot : 'ds-pagination';
28
+ return (jsx(Component, { ...(asChild
29
+ ? { className: cl('ds-pagination', className) }
30
+ : { class: cl('ds-pagination', className) }), suppressHydrationWarning // Since ds-pagination will change aria-label and role after hydration
31
+ : true, ref: ref, ...rest }));
28
32
  });
29
33
 
30
34
  export { Pagination };
@@ -26,18 +26,14 @@ import { Context } from './popover-trigger-context.js';
26
26
  * </Popover>
27
27
  * </PopoverTriggerContext>
28
28
  */
29
- const PopoverTrigger = forwardRef(function PopoverTrigger({ id, inline, asChild, ...rest }, ref) {
29
+ const PopoverTrigger = forwardRef(function PopoverTrigger({ id, inline, asChild, popovertarget, popoverTarget, ...rest }, ref) {
30
30
  const { popoverId } = useContext(Context);
31
31
  const Component = asChild ? Slot : inline ? 'button' : Button;
32
- const popoverProps = Object.assign({
33
- [version.startsWith('19') ? 'popoverTarget' : 'popovertarget']: popoverId,
34
- ...(inline
35
- ? {
36
- 'data-popover': 'inline',
37
- }
38
- : {}),
39
- }, rest);
40
- return jsx(Component, { ref: ref, ...popoverProps });
32
+ const popoverVal = popoverTarget ?? popovertarget ?? popoverId;
33
+ const popoverKey = version.startsWith('19')
34
+ ? 'popoverTarget'
35
+ : 'popovertarget';
36
+ return (jsx(Component, { ref: ref, "data-popover": inline ? 'inline' : undefined, [popoverKey]: popoverVal, ...rest }));
41
37
  });
42
38
 
43
39
  export { PopoverTrigger };
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
- import { autoUpdate, computePosition, offset, flip, shift } from '@floating-ui/dom';
4
3
  import { Slot } from '@radix-ui/react-slot';
5
4
  import cl from 'clsx/lite';
6
5
  import { forwardRef, useRef, useContext, useState, useEffect } from 'react';
6
+ import '@digdir/designsystemet-web';
7
7
  import { useMergeRefs } from '../../utilities/hooks/use-merge-refs/use-merge-refs.js';
8
8
  import { Context } from './popover-trigger-context.js';
9
9
 
@@ -34,13 +34,13 @@ const Popover = forwardRef(function Popover({ id, className, onClose, onOpen, op
34
34
  // NOTE: This code is purely to add React controlled component ability to Popover API
35
35
  useEffect(() => {
36
36
  const popover = popoverRef.current;
37
+ const trigger = `[popovertarget="${popover?.id}"],[commandfor="${popover?.id}"]`;
37
38
  const handleClick = (event) => {
38
39
  const el = event.target;
39
- const isTrigger = el?.closest?.(`[popovertarget="${popover?.id}"]`);
40
+ const isTrigger = el?.closest?.(trigger);
40
41
  const isOutside = !isTrigger && !popover?.contains(el);
41
- if (isTrigger) {
42
+ if (isTrigger)
42
43
  event.preventDefault(); // Prevent native Popover API
43
- }
44
44
  if (controlledOpen && (isTrigger || isOutside)) {
45
45
  setInternalOpen(false);
46
46
  onClose?.();
@@ -53,11 +53,20 @@ const Popover = forwardRef(function Popover({ id, className, onClose, onOpen, op
53
53
  const handleKeydown = (event) => {
54
54
  if (event.key !== 'Escape' || !controlledOpen)
55
55
  return;
56
+ const isOpen = popoverRef.current?.matches(':popover-open') ||
57
+ popoverRef.current?.classList.contains(':popover-open'); // Polyfill support
58
+ if (!isOpen)
59
+ return;
56
60
  event.preventDefault(); // Prevent closing fullscreen in Safari
61
+ document.querySelector(trigger)?.focus?.(); // Move focus back to trigger since `popoover="manual"` doesn't do this
57
62
  setInternalOpen(false);
58
63
  onClose?.();
59
64
  };
60
65
  popover?.togglePopover?.(controlledOpen);
66
+ if (controlledOpen) {
67
+ const options = { detail: document.querySelector(trigger) };
68
+ popover?.dispatchEvent(new CustomEvent('ds-toggle-source', options)); // Since togglePopover({ source }) is not supported in all browsers yet
69
+ }
61
70
  document.addEventListener('click', handleClick, true); // Use capture to execute before React event API
62
71
  document.addEventListener('keydown', handleKeydown);
63
72
  return () => {
@@ -65,69 +74,13 @@ const Popover = forwardRef(function Popover({ id, className, onClose, onOpen, op
65
74
  document.removeEventListener('keydown', handleKeydown);
66
75
  };
67
76
  }, [controlledOpen]);
68
- // Position with floating-ui
69
- useEffect(() => {
70
- const popover = popoverRef.current;
71
- const trigger = document.querySelector(`[popovertarget="${popover?.id}"]`);
72
- if (popover && trigger && controlledOpen)
73
- return autoUpdate(trigger, popover, () => {
74
- computePosition(trigger, popover, {
75
- placement,
76
- strategy: 'fixed',
77
- middleware: [
78
- offset((data) => {
79
- // get pseudo element arrow size
80
- const styles = getComputedStyle(data.elements.floating, '::before');
81
- return parseFloat(styles.height);
82
- }),
83
- ...(autoPlacement
84
- ? [flip({ fallbackAxisSideDirection: 'start' }), shift()]
85
- : []),
86
- arrowPseudoElement,
87
- ],
88
- }).then(({ x, y }) => {
89
- popover.style.translate = `${Math.round(x)}px ${Math.round(y)}px`;
90
- });
91
- });
92
- }, [controlledOpen, placement, id, autoPlacement]);
93
77
  // Update context with id
94
78
  useEffect(() => {
95
79
  if (id)
96
80
  setPopoverId?.(id);
97
81
  }, [id]);
98
- return (jsx(Component, { className: cl('ds-popover', className), id: id || popoverId, popover: 'manual', "data-variant": variant, ref: mergedRefs, ...rest }));
82
+ return (jsx(Component, { className: cl('ds-popover', className), id: id || popoverId, popover: 'manual', "data-placement": placement, "data-variant": variant, ref: mergedRefs, suppressHydrationWarning // Since _ds-floating adds attributes
83
+ : true, ...rest }));
99
84
  });
100
- const arrowPseudoElement = {
101
- name: 'ArrowPseudoElement',
102
- fn(data) {
103
- const { elements, rects, placement } = data;
104
- let arrowX = `${Math.round(rects.reference.width / 2 + rects.reference.x - data.x)}px`;
105
- let arrowY = `${Math.round(rects.reference.height / 2 + rects.reference.y - data.y)}px`;
106
- if (rects.reference.width > rects.floating.width) {
107
- arrowX = `${Math.round(rects.floating.width / 2)}px`;
108
- }
109
- if (rects.reference.height > rects.floating.height) {
110
- arrowY = `${Math.round(rects.floating.height / 2)}px`;
111
- }
112
- switch (placement.split('-')[0]) {
113
- case 'top':
114
- arrowY = '100%';
115
- break;
116
- case 'right':
117
- arrowX = '0';
118
- break;
119
- case 'bottom':
120
- arrowY = '0';
121
- break;
122
- case 'left':
123
- arrowX = '100%';
124
- break;
125
- }
126
- elements.floating.setAttribute('data-placement', placement.split('-')[0]); // We only need top/left/right/bottom
127
- elements.floating.style.setProperty('--ds-popover-arrow-x', arrowX);
128
- elements.floating.style.setProperty('--ds-popover-arrow-y', arrowY);
129
- return data;
130
- },
131
- };
132
85
 
133
86
  export { Popover };
@@ -12,18 +12,8 @@ import { forwardRef } from 'react';
12
12
  * <Select.Option value='2'>Option 2</Select.Option>
13
13
  * </Select>
14
14
  */
15
- const Select = forwardRef(function Select({ className, onKeyDown, onMouseDown, width, ...rest }, ref) {
16
- return (jsx("select", { className: cl('ds-input', className), "data-width": width, ref: ref, onKeyDown: (event) => {
17
- if (event.key === 'Tab')
18
- return;
19
- if (rest.readOnly)
20
- event.preventDefault(); // Make readonly work for select
21
- onKeyDown?.(event);
22
- }, onMouseDown: (event) => {
23
- if (rest.readOnly)
24
- event.preventDefault(); // Make readonly work for select
25
- onMouseDown?.(event);
26
- }, ...rest }));
15
+ const Select = forwardRef(function Select({ className, onKeyDown, onMouseDown, width, readOnly, ...rest }, ref) {
16
+ return (jsx("select", { className: cl('ds-input', className), "aria-readonly": rest['aria-readonly'] ?? readOnly, "data-width": width, ref: ref, ...rest }));
27
17
  });
28
18
 
29
19
  export { Select };