@dbcdk/react-components 0.0.10 → 0.0.13

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 (106) hide show
  1. package/dist/components/accordion/Accordion.d.ts +2 -2
  2. package/dist/components/accordion/Accordion.js +34 -41
  3. package/dist/components/accordion/Accordion.module.css +13 -72
  4. package/dist/components/accordion/components/AccordionRow.d.ts +10 -0
  5. package/dist/components/accordion/components/AccordionRow.js +51 -0
  6. package/dist/components/accordion/components/AccordionRow.module.css +82 -0
  7. package/dist/components/breadcrumbs/Breadcrumbs.module.css +0 -1
  8. package/dist/components/button/Button.module.css +7 -7
  9. package/dist/components/card/Card.d.ts +15 -6
  10. package/dist/components/card/Card.js +39 -13
  11. package/dist/components/card/Card.module.css +22 -28
  12. package/dist/components/card/components/CardMeta.d.ts +15 -0
  13. package/dist/components/card/components/CardMeta.js +20 -0
  14. package/dist/components/card/components/CardMeta.module.css +51 -0
  15. package/dist/components/card-container/CardContainer.js +1 -1
  16. package/dist/components/card-container/CardContainer.module.css +3 -1
  17. package/dist/components/chip/Chip.module.css +7 -2
  18. package/dist/components/circle/Circle.d.ts +2 -1
  19. package/dist/components/circle/Circle.js +2 -2
  20. package/dist/components/circle/Circle.module.css +6 -2
  21. package/dist/components/code-block/CodeBlock.js +1 -1
  22. package/dist/components/code-block/CodeBlock.module.css +30 -17
  23. package/dist/components/copy-button/CopyButton.d.ts +1 -0
  24. package/dist/components/copy-button/CopyButton.js +10 -2
  25. package/dist/components/datetime-picker/DateTimePicker.d.ts +33 -8
  26. package/dist/components/datetime-picker/DateTimePicker.js +119 -78
  27. package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
  28. package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
  29. package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
  30. package/dist/components/filter-field/FilterField.js +16 -11
  31. package/dist/components/filter-field/FilterField.module.css +137 -16
  32. package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
  33. package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
  34. package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
  35. package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
  36. package/dist/components/forms/form-select/FormSelect.js +86 -0
  37. package/dist/components/forms/form-select/FormSelect.module.css +236 -0
  38. package/dist/components/forms/input/Input.d.ts +0 -3
  39. package/dist/components/forms/input/Input.js +1 -4
  40. package/dist/components/forms/input/Input.module.css +8 -7
  41. package/dist/components/forms/input-container/InputContainer.module.css +1 -1
  42. package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
  43. package/dist/components/forms/select/Select.js +55 -16
  44. package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
  45. package/dist/components/hyperlink/Hyperlink.js +35 -11
  46. package/dist/components/hyperlink/Hyperlink.module.css +50 -2
  47. package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
  48. package/dist/components/interval-select/IntervalSelect.js +21 -6
  49. package/dist/components/menu/Menu.d.ts +29 -0
  50. package/dist/components/menu/Menu.js +61 -16
  51. package/dist/components/menu/Menu.module.css +73 -5
  52. package/dist/components/overlay/modal/Modal.module.css +4 -3
  53. package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
  54. package/dist/components/overlay/side-panel/SidePanel.js +18 -1
  55. package/dist/components/overlay/side-panel/SidePanel.module.css +1 -3
  56. package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
  57. package/dist/components/page-layout/PageLayout.d.ts +16 -4
  58. package/dist/components/page-layout/PageLayout.js +57 -28
  59. package/dist/components/page-layout/PageLayout.module.css +153 -33
  60. package/dist/components/popover/Popover.d.ts +17 -4
  61. package/dist/components/popover/Popover.js +147 -65
  62. package/dist/components/popover/Popover.module.css +5 -0
  63. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
  64. package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
  65. package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
  66. package/dist/components/split-button/SplitButton.d.ts +1 -1
  67. package/dist/components/split-button/SplitButton.js +3 -1
  68. package/dist/components/split-button/SplitButton.module.css +4 -4
  69. package/dist/components/split-pane/SplitPane.d.ts +10 -24
  70. package/dist/components/split-pane/SplitPane.js +83 -54
  71. package/dist/components/split-pane/SplitPane.module.css +11 -6
  72. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
  73. package/dist/components/state-page/StatePage.module.css +1 -1
  74. package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
  75. package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
  76. package/dist/components/table/Table.d.ts +8 -8
  77. package/dist/components/table/Table.js +37 -79
  78. package/dist/components/table/Table.module.css +62 -46
  79. package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +7 -3
  80. package/dist/components/table/TanstackTable.js +84 -0
  81. package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
  82. package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
  83. package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
  84. package/dist/components/table/components/table-settings/TableSettings.js +55 -4
  85. package/dist/components/table/table.utils.d.ts +17 -0
  86. package/dist/components/table/table.utils.js +61 -0
  87. package/dist/components/table/tanstackTable.utils.d.ts +22 -0
  88. package/dist/components/table/tanstackTable.utils.js +104 -0
  89. package/dist/components/tabs/Tabs.d.ts +35 -12
  90. package/dist/components/tabs/Tabs.js +114 -26
  91. package/dist/components/tabs/Tabs.module.css +158 -71
  92. package/dist/hooks/useTableSettings.d.ts +23 -4
  93. package/dist/hooks/useTableSettings.js +64 -17
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +1 -1
  96. package/dist/src/styles/styles.css +38 -23
  97. package/dist/styles/animation.d.ts +5 -0
  98. package/dist/styles/animation.js +5 -0
  99. package/dist/styles/styles.css +38 -23
  100. package/dist/styles/themes/dbc/base.css +136 -0
  101. package/dist/styles/themes/dbc/dark.css +39 -202
  102. package/dist/styles/themes/dbc/light.css +17 -174
  103. package/dist/utils/localStorage.utils.d.ts +19 -0
  104. package/dist/utils/localStorage.utils.js +78 -0
  105. package/package.json +4 -4
  106. package/dist/components/table/tanstack.js +0 -162
@@ -17,7 +17,4 @@ export type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size
17
17
  tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
18
18
  modified?: boolean;
19
19
  };
20
- /**
21
- * Explicit exported type annotation is required with --isolatedDeclarations.
22
- */
23
20
  export declare const Input: React.ForwardRefExoticComponent<React.PropsWithoutRef<InputProps> & React.RefAttributes<HTMLInputElement>>;
@@ -18,12 +18,9 @@ function mergeRefs(...refs) {
18
18
  }
19
19
  };
20
20
  }
21
- /**
22
- * Explicit exported type annotation is required with --isolatedDeclarations.
23
- */
24
21
  export const Input = forwardRef(function Input({
25
22
  // InputContainer props
26
- label, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required, tooltip, tooltipPlacement = 'right', modified,
23
+ label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = false, required, tooltip, tooltipPlacement = 'right', modified,
27
24
  // Input-only props
28
25
  icon, autoFocus, minWidth, width, inputSize = 'md', variant = 'outlined', onClear, onButtonClick, buttonLabel, buttonIcon,
29
26
  // Native input props
@@ -3,6 +3,7 @@
3
3
  /* inline-flex makes width behave weird in parents */
4
4
  display: inline-flex;
5
5
  align-items: stretch;
6
+ flex-grow: 1;
6
7
  gap: 0;
7
8
  /* width control */
8
9
  inline-size: var(--input-width, auto);
@@ -42,8 +43,8 @@
42
43
  border: var(--border-width-thin) solid var(--color-border-default);
43
44
  border-radius: var(--border-radius-default);
44
45
 
45
- padding-inline: var(--control-padding-x);
46
- padding-block: calc(var(--control-padding-y) + var(--density));
46
+ padding-inline: var(--spacing-sm);
47
+ padding-block: var(--spacing-xs);
47
48
 
48
49
  transition:
49
50
  background-color var(--transition-fast) var(--ease-standard),
@@ -66,7 +67,7 @@
66
67
 
67
68
  /* Optional: if ClearButton is absolutely positioned, reserve space */
68
69
  .withClear .input {
69
- padding-inline-end: calc(var(--control-padding-x) + 28px);
70
+ padding-inline-end: calc(var(--spacing-md) + 28px);
70
71
  }
71
72
 
72
73
  /* Placeholder */
@@ -99,20 +100,20 @@
99
100
 
100
101
  /* Sizes */
101
102
  .xs {
102
- block-size: calc(var(--component-size-xs) + var(--density));
103
+ block-size: var(--component-size-xs);
103
104
  font-size: var(--font-size-sm);
104
105
  padding: 0 var(--spacing-xxs);
105
106
  }
106
107
  .sm {
107
- block-size: calc(var(--component-size-sm) + var(--density));
108
+ block-size: var(--component-size-sm);
108
109
  font-size: var(--font-size-sm);
109
110
  }
110
111
  .md {
111
- block-size: calc(var(--component-size-md) + var(--density));
112
+ block-size: var(--component-size-md);
112
113
  font-size: var(--font-size-sm);
113
114
  }
114
115
  .lg {
115
- block-size: calc(var(--component-size-lg) + var(--density));
116
+ block-size: var(--component-size-lg);
116
117
  font-size: var(--font-size-lg);
117
118
  }
118
119
 
@@ -80,7 +80,7 @@
80
80
  */
81
81
  .inputContainer[data-modified] label:not(.label) {
82
82
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 35%, transparent);
83
- border-radius: var(--border-radius-md);
83
+ border-radius: var(--border-radius-default);
84
84
  padding: 2px 6px;
85
85
  }
86
86
 
@@ -78,6 +78,7 @@
78
78
  .label {
79
79
  cursor: pointer;
80
80
  user-select: none;
81
+ flex: 1;
81
82
  }
82
83
 
83
84
  .primary.checked {
@@ -1,45 +1,61 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check } from 'lucide-react';
4
- import { useEffect, useId, useRef, useState } from 'react';
4
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
5
5
  import { useTooltipTrigger } from '../../../components/overlay/tooltip/useTooltipTrigger';
6
6
  import { Button } from '../../button/Button';
7
7
  import { ClearButton } from '../../clear-button/ClearButton';
8
8
  import { Menu } from '../../menu/Menu';
9
9
  import { Popover } from '../../popover/Popover';
10
10
  import { InputContainer } from '../input-container/InputContainer';
11
- export function Select({
12
- // InputContainer props
13
- label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = true, required, tooltip, tooltipPlacement = 'right', modified = false,
14
- // Select props
15
- id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'outlined', onClear, datakey, dataCy, disabled, }) {
11
+ export function Select({ label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = true, required, tooltip, tooltipPlacement = 'right', modified = false, id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'outlined', onClear, datakey, dataCy, disabled, }) {
16
12
  const generatedId = useId();
17
13
  const controlId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
18
14
  const describedById = `${controlId}-desc`;
15
+ const listboxId = `${controlId}-listbox`;
19
16
  const popoverRef = useRef(null);
20
17
  const optionRefs = useRef([]);
21
- const selectedIndex = options.findIndex(o => o.value === selectedValue);
18
+ const selectedIndex = useMemo(() => options.findIndex(o => o.value === selectedValue), [options, selectedValue]);
22
19
  const [activeIndex, setActiveIndex] = useState(selectedIndex >= 0 ? selectedIndex : 0);
20
+ const [open, setOpen] = useState(false);
21
+ // Only move focus when open
23
22
  useEffect(() => {
24
23
  var _a;
24
+ if (!open)
25
+ return;
25
26
  (_a = optionRefs.current[activeIndex]) === null || _a === void 0 ? void 0 : _a.focus();
26
- }, [activeIndex]);
27
+ }, [activeIndex, open]);
28
+ // keep activeIndex aligned when opening
29
+ const resetActiveToSelected = () => setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
27
30
  const handleKeyDown = (e) => {
28
31
  var _a, _b;
29
32
  switch (e.key) {
30
33
  case 'ArrowDown': {
31
34
  e.preventDefault();
35
+ if (!open) {
36
+ setOpen(true);
37
+ return;
38
+ }
32
39
  setActiveIndex(i => Math.min(i + 1, options.length - 1));
33
40
  break;
34
41
  }
35
42
  case 'ArrowUp': {
36
43
  e.preventDefault();
44
+ if (!open) {
45
+ setOpen(true);
46
+ return;
47
+ }
37
48
  setActiveIndex(i => Math.max(i - 1, 0));
38
49
  break;
39
50
  }
40
51
  case 'Enter':
41
52
  case ' ': {
53
+ // Space on a button should open/select depending on state
42
54
  e.preventDefault();
55
+ if (!open) {
56
+ setOpen(true);
57
+ return;
58
+ }
43
59
  const opt = options[activeIndex];
44
60
  if (opt) {
45
61
  onChange(opt.value);
@@ -48,22 +64,35 @@ id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'ou
48
64
  break;
49
65
  }
50
66
  case 'Escape': {
67
+ if (!open)
68
+ return;
51
69
  e.preventDefault();
52
70
  (_b = popoverRef.current) === null || _b === void 0 ? void 0 : _b.close();
53
71
  break;
54
72
  }
73
+ case 'Home': {
74
+ if (!open)
75
+ return;
76
+ e.preventDefault();
77
+ setActiveIndex(0);
78
+ break;
79
+ }
80
+ case 'End': {
81
+ if (!open)
82
+ return;
83
+ e.preventDefault();
84
+ setActiveIndex(options.length - 1);
85
+ break;
86
+ }
55
87
  }
56
88
  };
57
89
  const selected = options.find(o => o.value === selectedValue);
58
- // Tooltip trigger props (anchor to the Button)
59
90
  const tooltipEnabled = Boolean(tooltip);
60
91
  const { triggerProps, id: tooltipId } = useTooltipTrigger({
61
92
  content: tooltipEnabled ? tooltip : null,
62
93
  placement: tooltipPlacement,
63
94
  offset: 8,
64
95
  });
65
- // If you want BOTH tooltip + error/helpText describedby:
66
- // merge describedby ids (keeping existing describedById)
67
96
  const describedBy = (() => {
68
97
  const ids = [];
69
98
  if (error || helpText)
@@ -72,15 +101,25 @@ id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'ou
72
101
  ids.push(tooltipId);
73
102
  return ids.length ? ids.join(' ') : undefined;
74
103
  })();
75
- return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: [_jsx(Popover, { ref: popoverRef, trigger: (onClick, icon) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
76
- setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
77
- onClick(e);
78
- }, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : placeholder }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
104
+ return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: [_jsx(Popover, { ref: popoverRef, open: open, onOpenChange: next => {
105
+ setOpen(next);
106
+ if (next)
107
+ resetActiveToSelected();
108
+ }, contentId: listboxId,
109
+ // Select manages roving focus; don't auto-focus content wrapper
110
+ autoFocusContent: false,
111
+ // keep focus on trigger until you move it yourself (your ArrowDown opens then focuses option)
112
+ returnFocus: true, trigger: (toggle, icon, isOpen) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
113
+ resetActiveToSelected();
114
+ toggle(e);
115
+ }, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : placeholder }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
79
116
  const isSelected = typeof opt.value === 'object' && typeof selectedValue === 'object' && datakey
80
117
  ? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
81
118
  : opt.value === selectedValue;
82
119
  const isActive = index === activeIndex;
83
- return (_jsx(Menu.Item, { active: isActive, "aria-selected": isSelected, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, onClick: () => {
120
+ return (_jsx(Menu.Item, { active: isActive,
121
+ // IMPORTANT: listbox uses role="option"
122
+ itemRole: "option", "aria-selected": isSelected, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, onClick: () => {
84
123
  var _a;
85
124
  onChange(opt.value);
86
125
  (_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
@@ -1,10 +1,22 @@
1
1
  import * as React from 'react';
2
- interface HyperlinkProps {
3
- component: React.ReactElement;
4
- className?: string;
2
+ type BaseProps = {
3
+ children: React.ReactNode;
5
4
  icon?: React.ReactNode;
6
- disableIcon?: boolean;
7
- onClick?: (e: React.MouseEvent) => void;
8
- }
9
- export declare function Hyperlink({ component, icon }: HyperlinkProps): React.ReactElement;
5
+ className?: string;
6
+ /**
7
+ * If true, Hyperlink will NOT render <a>.
8
+ * Instead, it will clone its only child (e.g. Next <Link>) and apply styling/props to it.
9
+ */
10
+ asChild?: boolean;
11
+ variant?: 'primary' | 'secondary';
12
+ inline?: boolean;
13
+ };
14
+ type AnchorProps = BaseProps & React.AnchorHTMLAttributes<HTMLAnchorElement> & {
15
+ as?: 'a';
16
+ };
17
+ type ButtonProps = BaseProps & React.ButtonHTMLAttributes<HTMLButtonElement> & {
18
+ as: 'button';
19
+ };
20
+ type HyperlinkProps = AnchorProps | ButtonProps;
21
+ export declare function Hyperlink(props: HyperlinkProps): React.ReactElement;
10
22
  export {};
@@ -1,15 +1,39 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from 'react';
3
3
  import styles from './Hyperlink.module.css';
4
- export function Hyperlink({ component, icon }) {
5
- const originalProps = component.props;
6
- return React.cloneElement(component, {
7
- ...originalProps,
8
- className: styles.link,
9
- onClick: (e) => {
10
- var _a;
11
- e.stopPropagation();
12
- (_a = originalProps === null || originalProps === void 0 ? void 0 : originalProps.onClick) === null || _a === void 0 ? void 0 : _a.call(originalProps, e);
13
- },
14
- }, _jsxs(_Fragment, { children: [_jsx("span", { className: styles.content, children: originalProps.children }), icon && _jsx("span", { className: styles.icon, children: icon })] }));
4
+ function cx(...parts) {
5
+ return parts.filter(Boolean).join(' ');
6
+ }
7
+ function renderInner(children, icon) {
8
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.content, children: children }), icon && _jsx("span", { className: styles.icon, children: icon })] }));
9
+ }
10
+ export function Hyperlink(props) {
11
+ var _a;
12
+ const { children, icon, className, asChild, as = 'a', variant = 'primary', inline = true, ...rest } = props;
13
+ const linkClassName = cx(styles.link, className, variant === 'secondary' ? styles.secondary : styles.primary, inline ? '' : styles.block);
14
+ if (asChild) {
15
+ const child = React.Children.only(children);
16
+ if (!React.isValidElement(child)) {
17
+ throw new Error('Hyperlink with asChild expects a single valid React element as its child.');
18
+ }
19
+ const childProps = (_a = child.props) !== null && _a !== void 0 ? _a : {};
20
+ return React.cloneElement(child, {
21
+ ...childProps,
22
+ ...rest,
23
+ className: cx(childProps.className, linkClassName),
24
+ children: renderInner(childProps.children, icon),
25
+ onClick: (e) => {
26
+ e.stopPropagation();
27
+ if (childProps.onClick) {
28
+ childProps.onClick(e);
29
+ }
30
+ },
31
+ });
32
+ }
33
+ if (as === 'button') {
34
+ // (Optional) guardrail: avoid accidentally passing href to a button
35
+ // const { href, ...buttonRest } = rest as any
36
+ return (_jsx("button", { type: "button", className: linkClassName, ...rest, children: renderInner(children, icon) }));
37
+ }
38
+ return (_jsx("a", { onClick: e => e.stopPropagation(), className: linkClassName, ...rest, children: renderInner(children, icon) }));
15
39
  }
@@ -2,13 +2,61 @@
2
2
  display: inline-flex;
3
3
  gap: var(--spacing-xs);
4
4
  position: relative;
5
+ font-weight: normal;
6
+ background: none;
7
+ border: none;
8
+ padding: 0;
5
9
  text-decoration: none;
6
- color: var(--color-brand);
7
10
  font-size: inherit;
11
+ font-family: inherit;
12
+ cursor: pointer;
13
+ color: var(--color-brand);
14
+ line-height: inherit;
15
+ }
16
+
17
+ .link.secondary {
18
+ color: var(--color-fg-default);
8
19
  }
9
20
 
10
21
  .link:hover {
11
- text-decoration: underline;
22
+ color: var(--color-brand);
23
+ }
24
+
25
+ .link.primary {
26
+ position: relative;
27
+ color: var(--color-brand);
28
+ text-decoration: none;
29
+ }
30
+
31
+ .link.block {
32
+ background: var(--color-bg-contextual-subtle);
33
+ display: inline-block;
34
+ padding: var(--spacing-xs);
35
+ &:hover {
36
+ background-color: var(--color-bg-contextual);
37
+ }
38
+ }
39
+
40
+ .link::after {
41
+ content: '';
42
+ position: absolute;
43
+ left: 0;
44
+ bottom: -2px;
45
+ width: 100%;
46
+ height: 1px;
47
+ background-color: currentColor;
48
+ transform: scaleX(0);
49
+ transform-origin: left;
50
+ transition: transform 100ms ease;
51
+ }
52
+
53
+ .link:hover::after {
54
+ transform: scaleX(1);
55
+ }
56
+
57
+ .link:focus-visible {
58
+ outline: 2px solid var(--color-brand);
59
+ outline-offset: 2px;
12
60
  }
13
61
 
14
62
  .icon {
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { ButtonVariant } from '../../components/button/Button';
3
3
  import { InputContainer } from '../../components/forms/input-container/InputContainer';
4
+ import { UtcIsoString } from '../datetime-picker/dateTimeHelpers';
4
5
  type InputContainerProps = React.ComponentProps<typeof InputContainer>;
5
6
  export type IntervalOption = {
6
7
  label: string;
@@ -12,9 +13,14 @@ export type IntervalSelectProps = Omit<InputContainerProps, 'children' | 'htmlFo
12
13
  id?: string;
13
14
  options: IntervalOption[];
14
15
  selectedValue: IntervalSelectValue;
15
- onChange: (date: Date, meta: {
16
+ /**
17
+ * Emits a UTC instant as ISO string (Z), consistent with DateTimePicker when enableTime=true.
18
+ * Also includes the computed local Date for convenience.
19
+ */
20
+ onChange: (isoUtc: UtcIsoString, meta: {
16
21
  minutesAgo: number;
17
22
  option: IntervalOption;
23
+ dateLocal: Date;
18
24
  }) => void;
19
25
  /** Base date for the calculation; defaults to "now". */
20
26
  baseDate?: Date;
@@ -23,8 +29,9 @@ export type IntervalSelectProps = Omit<InputContainerProps, 'children' | 'htmlFo
23
29
  variant?: ButtonVariant;
24
30
  onClear?: () => void;
25
31
  dataCy?: string;
32
+ disabled?: boolean;
26
33
  tooltip?: React.ReactNode;
27
34
  tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
28
35
  };
29
- export declare function IntervalSelect({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, id, options, selectedValue, onChange, baseDate, placeholder, size, variant, onClear, dataCy, }: IntervalSelectProps): React.ReactNode;
36
+ export declare function IntervalSelect({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, id, options, selectedValue, onChange, baseDate, placeholder, size, variant, onClear, dataCy, disabled, }: IntervalSelectProps): React.ReactNode;
30
37
  export {};
@@ -8,20 +8,23 @@ import { InputContainer } from '../../components/forms/input-container/InputCont
8
8
  import { Menu } from '../../components/menu/Menu';
9
9
  import { useTooltipTrigger } from '../../components/overlay/tooltip/useTooltipTrigger';
10
10
  import { Popover } from '../../components/popover/Popover';
11
+ import { isoFromLocalDate } from '../datetime-picker/dateTimeHelpers';
11
12
  export function IntervalSelect({
12
13
  // InputContainer props
13
14
  label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = true, required,
14
15
  // tooltip
15
16
  tooltip, tooltipPlacement = 'right',
16
17
  // IntervalSelect props
17
- id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval', size, variant = 'outlined', onClear, dataCy, }) {
18
+ id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval', size, variant = 'outlined', onClear, dataCy, disabled, }) {
18
19
  const generatedId = useId();
19
20
  const controlId = id !== null && id !== void 0 ? id : `interval-select-${generatedId}`;
20
21
  const describedById = `${controlId}-desc`;
22
+ const listboxId = `${controlId}-listbox`;
21
23
  const popoverRef = useRef(null);
22
24
  const optionRefs = useRef([]);
23
25
  const selectedIndex = useMemo(() => options.findIndex(o => o.minutesAgo === selectedValue), [options, selectedValue]);
24
26
  const [activeIndex, setActiveIndex] = useState(selectedIndex >= 0 ? selectedIndex : 0);
27
+ // Focus active option when opened (simple version: always focus when index changes)
25
28
  useEffect(() => {
26
29
  var _a;
27
30
  (_a = optionRefs.current[activeIndex]) === null || _a === void 0 ? void 0 : _a.focus();
@@ -44,8 +47,9 @@ id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval',
44
47
  const handleCommit = (opt) => {
45
48
  var _a;
46
49
  const base = baseDate !== null && baseDate !== void 0 ? baseDate : new Date();
47
- const dt = new Date(base.getTime() - opt.minutesAgo * 60000);
48
- onChange(dt, { minutesAgo: opt.minutesAgo, option: opt });
50
+ const dateLocal = new Date(base.getTime() - opt.minutesAgo * 60000);
51
+ const isoUtc = isoFromLocalDate(dateLocal);
52
+ onChange(isoUtc, { minutesAgo: opt.minutesAgo, option: opt, dateLocal });
49
53
  (_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
50
54
  };
51
55
  const handleKeyDown = (e) => {
@@ -59,6 +63,14 @@ id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval',
59
63
  e.preventDefault();
60
64
  setActiveIndex(i => Math.max(i - 1, 0));
61
65
  break;
66
+ case 'Home':
67
+ e.preventDefault();
68
+ setActiveIndex(0);
69
+ break;
70
+ case 'End':
71
+ e.preventDefault();
72
+ setActiveIndex(options.length - 1);
73
+ break;
62
74
  case 'Enter':
63
75
  case ' ':
64
76
  e.preventDefault();
@@ -71,12 +83,15 @@ id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval',
71
83
  break;
72
84
  }
73
85
  };
74
- return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, children: [_jsx(Popover, { ref: popoverRef, trigger: (onClick, icon) => (_jsx(Button, { ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'interval-select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
86
+ return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, children: [_jsx(Popover, { ref: popoverRef, contentId: listboxId, trigger: (onClick, icon, isOpen) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'interval-select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
87
+ // Reset active to selected when opening
75
88
  setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
76
89
  onClick(e);
77
- }, size: size, type: "button", "aria-haspopup": "listbox", "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [_jsx(Clock, { size: 14 }), selected ? selected.label : placeholder] }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
90
+ }, size: size, type: "button", "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [_jsx(Clock, { size: 14 }), selected ? selected.label : placeholder] }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { id: listboxId, onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
78
91
  const isSelected = opt.minutesAgo === selectedValue;
79
92
  const isActive = index === activeIndex;
80
- return (_jsx(Menu.Item, { active: isActive, "aria-selected": isSelected, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, onClick: () => handleCommit(opt), onFocus: () => setActiveIndex(index), style: { display: 'flex', alignItems: 'center', width: '100%' }, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: isSelected ? _jsx(Check, {}) : null }), opt.label] }) }, opt.minutesAgo));
93
+ return (_jsx(Menu.Item, { active: isActive,
94
+ // IMPORTANT: listbox uses role="option"
95
+ itemRole: "option", "aria-selected": isSelected, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, onClick: () => handleCommit(opt), onFocus: () => setActiveIndex(index), style: { display: 'flex', alignItems: 'center', width: '100%' }, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: isSelected ? _jsx(Check, {}) : null }), opt.label] }) }, opt.minutesAgo));
81
96
  }) }) }), (error || helpText) && (_jsx("span", { id: describedById, style: { display: 'none' }, children: error !== null && error !== void 0 ? error : helpText }))] }));
82
97
  }
@@ -1,12 +1,41 @@
1
1
  import * as React from 'react';
2
2
  export interface MenuProps extends React.HTMLAttributes<HTMLUListElement> {
3
3
  children: React.ReactNode;
4
+ /**
5
+ * Default role for items rendered by Menu.Item when it clones/wraps content.
6
+ * - default: "menuitem" (for role="menu")
7
+ * - for Select/listbox usage, pass itemRole="option" and role="listbox" on Menu.
8
+ */
9
+ itemRole?: 'menuitem' | 'option';
4
10
  }
11
+ export type MenuSeparatorProps = React.LiHTMLAttributes<HTMLLIElement>;
5
12
  export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
6
13
  children: React.ReactNode;
7
14
  active?: boolean;
8
15
  disabled?: boolean;
16
+ /**
17
+ * Override the role applied to the interactive element for this item only.
18
+ * If not set, Menu's `itemRole` is used.
19
+ */
20
+ itemRole?: 'menuitem' | 'option';
21
+ }
22
+ export interface MenuCheckItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
23
+ label: React.ReactNode;
24
+ checked: boolean;
25
+ disabled?: boolean;
26
+ onCheckedChange?: (checked: boolean) => void;
27
+ }
28
+ export interface MenuRadioItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
29
+ name: string;
30
+ value: string;
31
+ checked: boolean;
32
+ disabled?: boolean;
33
+ label: string;
34
+ onValueChange?: (value: string) => void;
9
35
  }
10
36
  export declare const Menu: React.FC<MenuProps> & {
11
37
  Item: React.FC<MenuItemProps>;
38
+ CheckItem: React.FC<MenuCheckItemProps>;
39
+ RadioItem: React.FC<MenuRadioItemProps>;
40
+ Separator: React.FC<MenuSeparatorProps>;
12
41
  };
@@ -2,28 +2,73 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
4
  import styles from './Menu.module.css';
5
- const MenuBase = React.forwardRef(({ children, className, ...props }, ref) => (_jsx("ul", { ref: ref, role: "menu", className: [styles.container, className].filter(Boolean).join(' '), ...props, children: children })));
5
+ import { Checkbox } from '../forms/checkbox/Checkbox';
6
+ import { RadioButton } from '../forms/radio-buttons/RadioButton';
7
+ const MenuBase = React.forwardRef(({ children, className, itemRole = 'menuitem', ...props }, ref) => (_jsx("ul", { ref: ref, role: "menu", "data-itemrole": itemRole, className: [styles.container, className].filter(Boolean).join(' '), ...props, children: children })));
6
8
  MenuBase.displayName = 'Menu';
7
9
  const isInteractiveEl = (el) => React.isValidElement(el) &&
8
- (typeof el.type === 'string' ? el.type === 'a' || el.type === 'button' : true); // allow custom/Next Link components
9
- const MenuItem = React.forwardRef(({ children, active, disabled, className, ...liProps }, ref) => {
10
- // If child is interactive (a/button/NextLink), clone it and style it.
10
+ (typeof el.type === 'string' ? el.type === 'a' || el.type === 'button' : true);
11
+ function applyMenuItemPropsToElement(child, opts) {
12
+ var _a, _b, _c, _d;
13
+ const { active, disabled, role, tabIndex = -1, className } = opts;
14
+ const childClass = [styles.item, active ? styles.active : ''].filter(Boolean).join(' ');
15
+ const nextImmediate = React.cloneElement(child, {
16
+ className: [child.props.className, styles.interactiveChild, className]
17
+ .filter(Boolean)
18
+ .join(' '),
19
+ });
20
+ // If immediate child is interactive, apply a11y+styles there
21
+ if (typeof child.type === 'string' && (child.type === 'a' || child.type === 'button')) {
22
+ return React.cloneElement(child, {
23
+ role: (_a = child.props.role) !== null && _a !== void 0 ? _a : role,
24
+ tabIndex: (_b = child.props.tabIndex) !== null && _b !== void 0 ? _b : tabIndex,
25
+ 'aria-selected': active || undefined,
26
+ 'aria-disabled': disabled || undefined,
27
+ className: [child.props.className, styles.interactive, childClass, className]
28
+ .filter(Boolean)
29
+ .join(' '),
30
+ ...(child.type === 'button' ? { disabled } : {}),
31
+ });
32
+ }
33
+ // For custom components, assume they forward props
34
+ return React.cloneElement(nextImmediate, {
35
+ role: (_c = nextImmediate.props.role) !== null && _c !== void 0 ? _c : role,
36
+ tabIndex: (_d = nextImmediate.props.tabIndex) !== null && _d !== void 0 ? _d : tabIndex,
37
+ 'aria-selected': active || undefined,
38
+ 'aria-disabled': disabled || undefined,
39
+ className: [nextImmediate.props.className, styles.interactive, childClass]
40
+ .filter(Boolean)
41
+ .join(' '),
42
+ disabled,
43
+ });
44
+ }
45
+ const MenuItem = React.forwardRef(({ children, active, disabled, className, itemRole, ...liProps }, ref) => {
46
+ // If caller sets itemRole prop, use it; otherwise attempt to inherit from parent Menu via data attr.
47
+ // (We can’t reliably read parent props here without context; simplest is: caller passes itemRole on Menu.Item when needed.)
48
+ const resolvedRole = itemRole !== null && itemRole !== void 0 ? itemRole : 'menuitem';
11
49
  if (isInteractiveEl(children)) {
12
50
  const child = children;
13
- const childClass = [styles.item, active ? styles.active : ''].filter(Boolean).join(' ');
14
- return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: React.cloneElement(child, {
15
- role: 'menuitem',
16
- tabIndex: -1,
17
- 'aria-selected': active || undefined,
18
- 'aria-disabled': disabled || undefined,
19
- className: [child.props.className, styles.interactive, childClass]
20
- .filter(Boolean)
21
- .join(' '),
22
- }) }));
51
+ return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: applyMenuItemPropsToElement(child, { active, disabled, role: resolvedRole }) }));
23
52
  }
24
- return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("button", { role: "menuitem", tabIndex: -1, "aria-selected": active || undefined, "aria-disabled": disabled || undefined, className: [styles.interactive, styles.item, active ? styles.active : '']
53
+ // Fallback: wrap non-interactive children in a <button>
54
+ return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("button", { role: resolvedRole, tabIndex: -1, "aria-selected": active || undefined, "aria-disabled": disabled || undefined, className: [styles.interactive, styles.item, active ? styles.active : '']
25
55
  .filter(Boolean)
26
56
  .join(' '), type: "button", disabled: disabled, children: children }) }));
27
57
  });
28
58
  MenuItem.displayName = 'Menu.Item';
29
- export const Menu = Object.assign(MenuBase, { Item: MenuItem });
59
+ const MenuCheckItem = React.forwardRef(({ label, checked, disabled, onCheckedChange, className, ...liProps }, ref) => {
60
+ return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(Checkbox, { noContainer: true, checked: checked, disabled: disabled, label: label, onChange: (next, _e) => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(next) }) }) }));
61
+ });
62
+ MenuCheckItem.displayName = 'Menu.CheckItem';
63
+ const MenuRadioItem = React.forwardRef(({ name, value, checked, disabled, label, onValueChange, className, ...liProps }, ref) => {
64
+ return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(RadioButton, { noContainer: true, name: name, value: value, checked: checked, disabled: disabled, label: label, onChange: (v, _e) => onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(v) }) }) }));
65
+ });
66
+ MenuRadioItem.displayName = 'Menu.RadioItem';
67
+ const MenuSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx("li", { ref: ref, role: "separator", className: [styles.separator, className].filter(Boolean).join(' '), ...props })));
68
+ MenuSeparator.displayName = 'Menu.Separator';
69
+ export const Menu = Object.assign(MenuBase, {
70
+ Item: MenuItem,
71
+ CheckItem: MenuCheckItem,
72
+ RadioItem: MenuRadioItem,
73
+ Separator: MenuSeparator,
74
+ });