@dbcdk/react-components 0.0.12 → 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 (75) 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 +9 -18
  10. package/dist/components/card/Card.js +34 -23
  11. package/dist/components/card/Card.module.css +22 -87
  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/datetime-picker/DateTimePicker.d.ts +33 -8
  19. package/dist/components/datetime-picker/DateTimePicker.js +119 -78
  20. package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
  21. package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
  22. package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
  23. package/dist/components/filter-field/FilterField.module.css +5 -5
  24. package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
  25. package/dist/components/forms/form-select/FormSelect.js +86 -0
  26. package/dist/components/forms/form-select/FormSelect.module.css +236 -0
  27. package/dist/components/forms/input/Input.d.ts +0 -3
  28. package/dist/components/forms/input/Input.js +0 -3
  29. package/dist/components/forms/input/Input.module.css +7 -7
  30. package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
  31. package/dist/components/forms/select/Select.js +55 -16
  32. package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
  33. package/dist/components/interval-select/IntervalSelect.js +21 -6
  34. package/dist/components/menu/Menu.d.ts +11 -14
  35. package/dist/components/menu/Menu.js +18 -33
  36. package/dist/components/menu/Menu.module.css +2 -2
  37. package/dist/components/overlay/modal/Modal.module.css +2 -1
  38. package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
  39. package/dist/components/overlay/side-panel/SidePanel.js +1 -1
  40. package/dist/components/overlay/side-panel/SidePanel.module.css +1 -1
  41. package/dist/components/page-layout/PageLayout.d.ts +16 -4
  42. package/dist/components/page-layout/PageLayout.js +57 -28
  43. package/dist/components/page-layout/PageLayout.module.css +153 -33
  44. package/dist/components/popover/Popover.d.ts +17 -4
  45. package/dist/components/popover/Popover.js +147 -65
  46. package/dist/components/popover/Popover.module.css +5 -0
  47. package/dist/components/split-pane/SplitPane.d.ts +10 -24
  48. package/dist/components/split-pane/SplitPane.js +83 -54
  49. package/dist/components/split-pane/SplitPane.module.css +11 -6
  50. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
  51. package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
  52. package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
  53. package/dist/components/table/Table.d.ts +3 -8
  54. package/dist/components/table/Table.js +37 -76
  55. package/dist/components/table/Table.module.css +45 -42
  56. package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +5 -12
  57. package/dist/components/table/TanstackTable.js +84 -0
  58. package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
  59. package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
  60. package/dist/components/table/table.utils.d.ts +17 -0
  61. package/dist/components/table/table.utils.js +61 -0
  62. package/dist/components/table/tanstackTable.utils.d.ts +22 -0
  63. package/dist/components/table/tanstackTable.utils.js +104 -0
  64. package/dist/components/tabs/Tabs.d.ts +35 -12
  65. package/dist/components/tabs/Tabs.js +114 -26
  66. package/dist/components/tabs/Tabs.module.css +158 -71
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/src/styles/styles.css +0 -1
  70. package/dist/styles/styles.css +0 -1
  71. package/dist/styles/themes/dbc/base.css +136 -0
  72. package/dist/styles/themes/dbc/dark.css +39 -202
  73. package/dist/styles/themes/dbc/light.css +17 -174
  74. package/package.json +4 -4
  75. package/dist/components/table/tanstack.js +0 -214
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { InputContainer } from '../input-container/InputContainer';
3
+ import { MultiselectOption } from '../multi-select/MultiSelect';
4
+ type InputContainerProps = React.ComponentProps<typeof InputContainer>;
5
+ export type FormSelectProps<T> = Omit<InputContainerProps, 'children' | 'htmlFor' | 'tooltip' | 'tooltipPlacement'> & {
6
+ id?: string;
7
+ name?: string;
8
+ options: MultiselectOption<T>[];
9
+ selectedValue: T | null;
10
+ onChange: (value: T) => void;
11
+ placeholder?: string;
12
+ size?: 'sm' | 'md' | 'lg';
13
+ variant?: 'outlined' | 'filled' | 'standalone';
14
+ onClear?: () => void;
15
+ /**
16
+ * Needed if T is an object and you want to use native select.
17
+ * Native <select> requires string values; we serialize using value[datakey].
18
+ */
19
+ datakey?: string;
20
+ dataCy?: string;
21
+ disabled?: boolean;
22
+ tooltip?: React.ReactNode;
23
+ tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
24
+ includePlaceholderOption?: boolean;
25
+ /**
26
+ * Default false. If true, we allow clearing even when required=true:
27
+ * - placeholder becomes selectable
28
+ * - clear button is shown
29
+ *
30
+ * This is "odd" UX in many forms, so it's opt-in.
31
+ */
32
+ allowClearWhenRequired?: boolean;
33
+ };
34
+ export declare function FormSelect<T extends string | number | Record<string, any>>({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, modified, id, name, options, selectedValue, onChange, placeholder, size, variant, onClear, datakey, dataCy, disabled, includePlaceholderOption, allowClearWhenRequired, }: FormSelectProps<T>): React.ReactNode;
35
+ export {};
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronDown, X } from 'lucide-react';
4
+ import * as React from 'react';
5
+ import { useTooltipTrigger } from '../../../components/overlay/tooltip/useTooltipTrigger';
6
+ import styles from './FormSelect.module.css';
7
+ import { InputContainer } from '../input-container/InputContainer';
8
+ function isEqualValue(a, b, datakey) {
9
+ if (a === b)
10
+ return true;
11
+ if (!a || !b)
12
+ return false;
13
+ if (typeof a === 'object' && typeof b === 'object' && datakey) {
14
+ return a[datakey] === b[datakey];
15
+ }
16
+ return false;
17
+ }
18
+ function serializeValue(value, datakey) {
19
+ if (typeof value === 'string' || typeof value === 'number')
20
+ return String(value);
21
+ if (datakey && value && typeof value === 'object') {
22
+ const v = value[datakey];
23
+ return v == null ? '' : String(v);
24
+ }
25
+ throw new Error('FormSelect: option value is an object but no `datakey` was provided. Native select requires string/number values.');
26
+ }
27
+ function findSelectedOption(options, selectedValue, datakey) {
28
+ return options.find(o => isEqualValue(o.value, selectedValue, datakey));
29
+ }
30
+ export function FormSelect({
31
+ // InputContainer props
32
+ label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = true, required, tooltip, tooltipPlacement = 'right', modified = false,
33
+ // FormSelect props
34
+ id, name, options, selectedValue, onChange, placeholder = 'Vælg', size = 'md', variant = 'outlined', onClear, datakey, dataCy, disabled, includePlaceholderOption = true, allowClearWhenRequired = false, }) {
35
+ const generatedId = React.useId();
36
+ const controlId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
37
+ const describedById = `${controlId}-desc`;
38
+ const selected = React.useMemo(() => findSelectedOption(options, selectedValue, datakey), [options, selectedValue, datakey]);
39
+ const tooltipEnabled = Boolean(tooltip);
40
+ const { triggerProps, id: tooltipId } = useTooltipTrigger({
41
+ content: tooltipEnabled ? tooltip : null,
42
+ placement: tooltipPlacement,
43
+ offset: 8,
44
+ });
45
+ const describedBy = (() => {
46
+ const ids = [];
47
+ if (error || helpText)
48
+ ids.push(describedById);
49
+ if (tooltipEnabled)
50
+ ids.push(tooltipId);
51
+ return ids.length ? ids.join(' ') : undefined;
52
+ })();
53
+ const nativeValue = React.useMemo(() => {
54
+ if (selected && selected.value != null)
55
+ return serializeValue(selected.value, datakey);
56
+ return '';
57
+ }, [selected, datakey]);
58
+ const canClear = Boolean(onClear) && !disabled && (allowClearWhenRequired || !required);
59
+ const showClear = canClear && Boolean(selected);
60
+ const placeholderDisabled = Boolean(required) && !allowClearWhenRequired;
61
+ const handleNativeChange = e => {
62
+ const raw = e.target.value;
63
+ if (raw === '') {
64
+ // Clear only if consumer provided onClear
65
+ onClear === null || onClear === void 0 ? void 0 : onClear();
66
+ return;
67
+ }
68
+ const opt = options.find(o => serializeValue(o.value, datakey) === raw);
69
+ if (opt)
70
+ onChange(opt.value);
71
+ };
72
+ return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: [_jsxs("div", { className: [
73
+ styles.container,
74
+ fullWidth ? styles.fullWidth : '',
75
+ canClear ? styles.withButton : '',
76
+ ]
77
+ .filter(Boolean)
78
+ .join(' '), children: [_jsxs("div", { className: styles.field, children: [_jsxs("select", { ...(tooltipEnabled ? triggerProps : {}), id: controlId, name: name, className: [styles.input, styles[size], styles[variant]].filter(Boolean).join(' '), value: nativeValue, onChange: handleNativeChange, disabled: disabled, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'formselect', "data-forminput": true, children: [includePlaceholderOption && (_jsx("option", { value: "", disabled: placeholderDisabled, children: placeholder })), options.map(opt => {
79
+ const v = serializeValue(opt.value, datakey);
80
+ return (_jsx("option", { value: v, children: opt.label }, v));
81
+ })] }), _jsx("span", { className: styles.chevron, "aria-hidden": "true", children: _jsx(ChevronDown, { size: 20 }) })] }), canClear && (_jsx("button", { type: "button", className: styles.trailingButton, onClick: e => {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ onClear === null || onClear === void 0 ? void 0 : onClear();
85
+ }, "aria-label": "Ryd valg", disabled: !showClear, "data-cy": dataCy ? `${dataCy}-clear` : 'formselect-clear', children: _jsx(X, { size: 18, "aria-hidden": "true" }) }))] }), (error || helpText) && (_jsx("span", { id: describedById, style: { display: 'none' }, children: error !== null && error !== void 0 ? error : helpText }))] }));
86
+ }
@@ -0,0 +1,236 @@
1
+ /* FormSelect.module.css */
2
+
3
+ /* =========================================
4
+ Root container (select + optional clear)
5
+ ========================================= */
6
+
7
+ .container {
8
+ display: inline-flex;
9
+ align-items: stretch;
10
+ flex-grow: 1;
11
+ gap: 0;
12
+
13
+ inline-size: var(--input-width, auto);
14
+ min-inline-size: var(--input-min-width, 0);
15
+ max-inline-size: var(--input-max-width, none);
16
+
17
+ position: relative;
18
+ border-radius: var(--border-radius-default);
19
+ }
20
+
21
+ /* Full width variant */
22
+ .fullWidth {
23
+ display: flex;
24
+ inline-size: 100%;
25
+ min-inline-size: 0;
26
+ }
27
+
28
+ /* =========================================
29
+ Focus ring (GROUP LEVEL)
30
+ ========================================= */
31
+
32
+ .container:focus-within {
33
+ box-shadow: 0 0 0 2px var(--color-border-selected);
34
+ z-index: 1;
35
+ }
36
+
37
+ /* IMPORTANT:
38
+ When focused, do NOT also turn the select's border blue,
39
+ otherwise you get the vertical seam line next to the button. */
40
+ .container:focus-within .input {
41
+ border-color: var(--color-border-default);
42
+ }
43
+ .container:focus-within .trailingButton {
44
+ border-color: var(--color-border-default);
45
+ }
46
+
47
+ /* =========================================
48
+ Field wrapper (native select + chevron)
49
+ ========================================= */
50
+
51
+ .field {
52
+ position: relative;
53
+ display: flex;
54
+ align-items: center;
55
+ flex: 1 1 auto;
56
+ min-inline-size: 0;
57
+ color: var(--color-fg-default);
58
+ }
59
+
60
+ /* =========================================
61
+ Native select styling
62
+ ========================================= */
63
+
64
+ .input {
65
+ appearance: none;
66
+ -webkit-appearance: none;
67
+ -moz-appearance: none;
68
+
69
+ flex: 1 1 auto;
70
+ min-inline-size: 0;
71
+ inline-size: 100%;
72
+ max-inline-size: 100%;
73
+
74
+ background: var(--color-bg-surface);
75
+ font-family: var(--font-family);
76
+ font-size: var(--font-size-sm);
77
+ line-height: var(--line-height-normal);
78
+ box-sizing: border-box;
79
+ text-overflow: ellipsis;
80
+
81
+ border: var(--border-width-thin) solid var(--color-border-default);
82
+ border-radius: var(--border-radius-default);
83
+
84
+ padding-inline: var(--spacing-sm);
85
+ padding-block: var(--spacing-xs);
86
+
87
+ /* Reserve space for chevron */
88
+ padding-inline-end: calc(var(--spacing-lg) + 28px);
89
+
90
+ transition:
91
+ background-color var(--transition-fast) var(--ease-standard),
92
+ border-color var(--transition-fast) var(--ease-standard),
93
+ box-shadow var(--transition-fast) var(--ease-standard);
94
+ }
95
+
96
+ /* Disabled select */
97
+ .input:disabled {
98
+ background-color: var(--color-disabled-bg);
99
+ color: var(--color-disabled-fg);
100
+ cursor: not-allowed;
101
+ }
102
+
103
+ /* Hover state */
104
+ .input:hover:not(:disabled) {
105
+ border-color: var(--color-border-strong);
106
+ }
107
+
108
+ /* Remove default focus ring (we use group-level ring) */
109
+ .input:focus-visible {
110
+ outline: none;
111
+ /* DO NOT set border-color here */
112
+ }
113
+
114
+ /* =========================================
115
+ Variants
116
+ ========================================= */
117
+
118
+ .filled {
119
+ background-color: var(--color-bg-surface);
120
+ }
121
+
122
+ .standalone {
123
+ border-radius: var(--border-radius-rounded);
124
+ background-color: var(--color-bg-surface);
125
+ box-shadow: var(--shadow-xs), var(--shadow-md);
126
+ }
127
+
128
+ .outlined {
129
+ background-color: transparent;
130
+ }
131
+
132
+ /* =========================================
133
+ Sizes
134
+ ========================================= */
135
+
136
+ .sm {
137
+ block-size: var(--component-size-sm);
138
+ font-size: var(--font-size-sm);
139
+ }
140
+
141
+ .md {
142
+ block-size: var(--component-size-md);
143
+ font-size: var(--font-size-sm);
144
+ }
145
+
146
+ .lg {
147
+ block-size: var(--component-size-lg);
148
+ font-size: var(--font-size-lg);
149
+ }
150
+
151
+ /* =========================================
152
+ Chevron (decorative only)
153
+ ========================================= */
154
+
155
+ .chevron {
156
+ position: absolute;
157
+ inset-inline-end: var(--spacing-sm);
158
+ top: 50%;
159
+ transform: translateY(-50%);
160
+ display: inline-flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ pointer-events: none;
164
+ color: var(--color-fg-subtle);
165
+ }
166
+
167
+ .chevron svg {
168
+ inline-size: 20px;
169
+ block-size: 20px;
170
+ }
171
+
172
+ /* =========================================
173
+ Clear button integration
174
+ ========================================= */
175
+
176
+ /* Remove right radius from select when button exists */
177
+ .withButton .input {
178
+ border-top-right-radius: 0;
179
+ border-bottom-right-radius: 0;
180
+
181
+ /* Prevent double border seam; the button provides the right side */
182
+ border-right-color: transparent;
183
+ }
184
+
185
+ /* Trailing clear button */
186
+ .trailingButton {
187
+ position: relative;
188
+ z-index: 2;
189
+ flex: 0 0 auto;
190
+
191
+ display: inline-flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+
195
+ padding-inline: var(--spacing-sm);
196
+
197
+ border: var(--border-width-thin) solid var(--color-border-default);
198
+
199
+ /* Seam join: don't draw left border; select covers it */
200
+ border-left-color: transparent;
201
+ margin-left: calc(-1 * var(--border-width-thin));
202
+
203
+ border-top-right-radius: var(--border-radius-default);
204
+ border-bottom-right-radius: var(--border-radius-default);
205
+
206
+ background: var(--color-bg-surface);
207
+ cursor: pointer;
208
+
209
+ transition:
210
+ background-color var(--transition-fast) var(--ease-standard),
211
+ border-color var(--transition-fast) var(--ease-standard),
212
+ box-shadow var(--transition-fast) var(--ease-standard);
213
+ }
214
+
215
+ /* Hover state */
216
+ .trailingButton:hover:not(:disabled) {
217
+ border-color: var(--color-border-strong);
218
+ background-color: var(--color-bg-surface-hover, var(--color-bg-surface));
219
+ }
220
+
221
+ /* Remove individual focus ring (group handles it) */
222
+ .trailingButton:focus-visible {
223
+ outline: none;
224
+ box-shadow: none;
225
+ }
226
+
227
+ /* Disabled clear button:
228
+ keep border/background stable; dim only icon */
229
+ .trailingButton:disabled {
230
+ cursor: default;
231
+ background: var(--color-bg-surface);
232
+ }
233
+
234
+ .trailingButton:disabled svg {
235
+ opacity: 0.4;
236
+ }
@@ -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,9 +18,6 @@ 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
23
  label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = false, required, tooltip, tooltipPlacement = 'right', modified,
@@ -43,8 +43,8 @@
43
43
  border: var(--border-width-thin) solid var(--color-border-default);
44
44
  border-radius: var(--border-radius-default);
45
45
 
46
- padding-inline: var(--control-padding-x);
47
- padding-block: calc(var(--control-padding-y) + var(--density));
46
+ padding-inline: var(--spacing-sm);
47
+ padding-block: var(--spacing-xs);
48
48
 
49
49
  transition:
50
50
  background-color var(--transition-fast) var(--ease-standard),
@@ -67,7 +67,7 @@
67
67
 
68
68
  /* Optional: if ClearButton is absolutely positioned, reserve space */
69
69
  .withClear .input {
70
- padding-inline-end: calc(var(--control-padding-x) + 28px);
70
+ padding-inline-end: calc(var(--spacing-md) + 28px);
71
71
  }
72
72
 
73
73
  /* Placeholder */
@@ -100,20 +100,20 @@
100
100
 
101
101
  /* Sizes */
102
102
  .xs {
103
- block-size: calc(var(--component-size-xs) + var(--density));
103
+ block-size: var(--component-size-xs);
104
104
  font-size: var(--font-size-sm);
105
105
  padding: 0 var(--spacing-xxs);
106
106
  }
107
107
  .sm {
108
- block-size: calc(var(--component-size-sm) + var(--density));
108
+ block-size: var(--component-size-sm);
109
109
  font-size: var(--font-size-sm);
110
110
  }
111
111
  .md {
112
- block-size: calc(var(--component-size-md) + var(--density));
112
+ block-size: var(--component-size-md);
113
113
  font-size: var(--font-size-sm);
114
114
  }
115
115
  .lg {
116
- block-size: calc(var(--component-size-lg) + var(--density));
116
+ block-size: var(--component-size-lg);
117
117
  font-size: var(--font-size-lg);
118
118
  }
119
119
 
@@ -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,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 {};