@dbcdk/react-components 0.0.12 → 0.0.14

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 (76) 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.js +21 -6
  24. package/dist/components/filter-field/FilterField.module.css +5 -5
  25. package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
  26. package/dist/components/forms/form-select/FormSelect.js +86 -0
  27. package/dist/components/forms/form-select/FormSelect.module.css +236 -0
  28. package/dist/components/forms/input/Input.d.ts +0 -3
  29. package/dist/components/forms/input/Input.js +0 -3
  30. package/dist/components/forms/input/Input.module.css +7 -7
  31. package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
  32. package/dist/components/forms/select/Select.js +55 -16
  33. package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
  34. package/dist/components/interval-select/IntervalSelect.js +21 -6
  35. package/dist/components/menu/Menu.d.ts +11 -14
  36. package/dist/components/menu/Menu.js +18 -33
  37. package/dist/components/menu/Menu.module.css +2 -2
  38. package/dist/components/overlay/modal/Modal.module.css +2 -1
  39. package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
  40. package/dist/components/overlay/side-panel/SidePanel.js +1 -1
  41. package/dist/components/overlay/side-panel/SidePanel.module.css +1 -1
  42. package/dist/components/page-layout/PageLayout.d.ts +16 -4
  43. package/dist/components/page-layout/PageLayout.js +57 -28
  44. package/dist/components/page-layout/PageLayout.module.css +153 -33
  45. package/dist/components/popover/Popover.d.ts +17 -4
  46. package/dist/components/popover/Popover.js +147 -65
  47. package/dist/components/popover/Popover.module.css +5 -0
  48. package/dist/components/split-pane/SplitPane.d.ts +10 -24
  49. package/dist/components/split-pane/SplitPane.js +83 -54
  50. package/dist/components/split-pane/SplitPane.module.css +11 -6
  51. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
  52. package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
  53. package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
  54. package/dist/components/table/Table.d.ts +3 -8
  55. package/dist/components/table/Table.js +37 -76
  56. package/dist/components/table/Table.module.css +45 -42
  57. package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +5 -12
  58. package/dist/components/table/TanstackTable.js +84 -0
  59. package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
  60. package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
  61. package/dist/components/table/table.utils.d.ts +17 -0
  62. package/dist/components/table/table.utils.js +61 -0
  63. package/dist/components/table/tanstackTable.utils.d.ts +22 -0
  64. package/dist/components/table/tanstackTable.utils.js +104 -0
  65. package/dist/components/tabs/Tabs.d.ts +35 -12
  66. package/dist/components/tabs/Tabs.js +114 -26
  67. package/dist/components/tabs/Tabs.module.css +158 -71
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/src/styles/styles.css +0 -1
  71. package/dist/styles/styles.css +0 -1
  72. package/dist/styles/themes/dbc/base.css +136 -0
  73. package/dist/styles/themes/dbc/dark.css +39 -202
  74. package/dist/styles/themes/dbc/light.css +17 -174
  75. package/package.json +4 -4
  76. package/dist/components/table/tanstack.js +0 -214
@@ -68,24 +68,29 @@ function isFilterActive(value) {
68
68
  return value.trim().length > 0;
69
69
  return value != null;
70
70
  }
71
+ const VALUELESS_OPERATORS = ['isEmpty', 'isNotEmpty'];
71
72
  export function FilterField({ field, control, operator, value, onChange, operators, options = [], single = true, size = 'md', label, placeholder = 'Type value…', disabled, 'data-cy': dataCy, ...inputProps }) {
72
73
  var _a, _b;
73
- const [selectedOperator, setSelectedOperator] = useState(operator);
74
74
  const ops = useMemo(() => operators !== null && operators !== void 0 ? operators : DEFAULT_TEXT_OPERATORS, [operators]);
75
+ // internal operator state (source of truth for UI)
76
+ const [selectedOperator, setSelectedOperator] = useState(operator);
77
+ // "active" based on value only (as requested)
75
78
  const active = isFilterActive(value);
76
- // Local state ONLY for input control (to avoid URL->props lag)
77
- const [localValue, setLocalValue] = useState((_a = value) !== null && _a !== void 0 ? _a : '');
78
- const debounceRef = useRef(null);
79
- const isTypingRef = useRef(false);
79
+ // Overwrite internal operator if parent sends a new one
80
80
  useEffect(() => {
81
81
  if (ops.includes(operator)) {
82
82
  setSelectedOperator(operator);
83
83
  }
84
84
  }, [operator, ops]);
85
+ // Local state ONLY for input control (to avoid URL->props lag)
86
+ const [localValue, setLocalValue] = useState((_a = value) !== null && _a !== void 0 ? _a : '');
87
+ const debounceRef = useRef(null);
88
+ const isTypingRef = useRef(false);
85
89
  const emit = (next) => {
86
90
  var _a, _b;
87
91
  const nextOperator = (_a = next.operator) !== null && _a !== void 0 ? _a : selectedOperator;
88
92
  const nextValue = (_b = next.value) !== null && _b !== void 0 ? _b : value;
93
+ // Always keep internal operator in sync when user picks one
89
94
  if (next.operator)
90
95
  setSelectedOperator(nextOperator);
91
96
  onChange({
@@ -94,6 +99,16 @@ export function FilterField({ field, control, operator, value, onChange, operato
94
99
  value: nextValue,
95
100
  });
96
101
  };
102
+ const handleOperatorChange = (op) => {
103
+ setSelectedOperator(op);
104
+ if (!active && !VALUELESS_OPERATORS.includes(op))
105
+ return;
106
+ if (VALUELESS_OPERATORS.includes(op)) {
107
+ emit({ operator: op, value: null });
108
+ return;
109
+ }
110
+ emit({ operator: op });
111
+ };
97
112
  const scheduleEmitValue = (nextVal) => {
98
113
  if (debounceRef.current)
99
114
  clearTimeout(debounceRef.current);
@@ -122,7 +137,7 @@ export function FilterField({ field, control, operator, value, onChange, operato
122
137
  clearTimeout(debounceRef.current);
123
138
  };
124
139
  }, []);
125
- return (_jsxs("div", { ...(dataCy ? { 'data-cy': dataCy } : {}), className: `${styles.filterField} ${styles[size]} ${active ? styles.active : ''}`, children: [label ? _jsx("span", { className: `${styles.label} ${styles[size]}`, children: label }) : null, _jsx(OperatorDropdown, { value: selectedOperator, onChange: op => emit({ operator: op }), operators: ops, size: size, disabled: disabled }), _jsx("div", { className: `${control === 'input' ? 'dbc-flex dbc-flex-grow' : styles.valueWrapper}`, children: control === 'input' ? (_jsx(Input, { ...inputProps, value: localValue, onChange: e => {
140
+ return (_jsxs("div", { ...(dataCy ? { 'data-cy': dataCy } : {}), className: `${styles.filterField} ${styles[size]} ${active ? styles.active : ''}`, children: [label ? _jsx("span", { className: `${styles.label} ${styles[size]}`, children: label }) : null, _jsx(OperatorDropdown, { value: selectedOperator, onChange: handleOperatorChange, operators: ops, size: size, disabled: disabled }), _jsx("div", { className: `${control === 'input' ? 'dbc-flex dbc-flex-grow' : styles.valueWrapper}`, children: control === 'input' ? (_jsx(Input, { ...inputProps, value: localValue, onChange: e => {
126
141
  const next = e.currentTarget.value;
127
142
  isTypingRef.current = true;
128
143
  setLocalValue(next);
@@ -28,16 +28,16 @@
28
28
  }
29
29
 
30
30
  .filterField.sm {
31
- block-size: calc(var(--component-size-sm) + var(--density));
31
+ block-size: var(--component-size-sm);
32
32
  }
33
33
  .filterField.md {
34
- block-size: calc(var(--component-size-md) + var(--density));
34
+ block-size: var(--component-size-md);
35
35
  }
36
36
 
37
37
  .filterField .label {
38
38
  display: inline-flex;
39
39
  align-items: center;
40
- padding-block: calc(var(--spacing-2xs) + var(--density));
40
+ padding-block: var(--spacing-2xs);
41
41
  padding-inline: var(--spacing-sm);
42
42
  font-size: var(--font-size-sm);
43
43
  color: var(--color-fg-muted);
@@ -51,7 +51,7 @@
51
51
  align-items: center;
52
52
  justify-content: center;
53
53
  height: 100%;
54
- padding-block: calc(var(--spacing-2xs) + var(--density));
54
+ padding-block: var(--spacing-2xs);
55
55
  padding-inline: var(--spacing-sm);
56
56
  background: var(--opac-bg-default);
57
57
  color: var(--color-fg-default);
@@ -223,7 +223,7 @@
223
223
  border-bottom-left-radius: 0;
224
224
 
225
225
  /* a tiny bit more comfort */
226
- padding-block: calc(var(--spacing-3xs) + var(--density));
226
+ padding-block: var(--spacing-3xs);
227
227
  }
228
228
 
229
229
  .filterField button {
@@ -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();