@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.
- package/dist/components/accordion/Accordion.d.ts +2 -2
- package/dist/components/accordion/Accordion.js +34 -41
- package/dist/components/accordion/Accordion.module.css +13 -72
- package/dist/components/accordion/components/AccordionRow.d.ts +10 -0
- package/dist/components/accordion/components/AccordionRow.js +51 -0
- package/dist/components/accordion/components/AccordionRow.module.css +82 -0
- package/dist/components/breadcrumbs/Breadcrumbs.module.css +0 -1
- package/dist/components/button/Button.module.css +7 -7
- package/dist/components/card/Card.d.ts +15 -6
- package/dist/components/card/Card.js +39 -13
- package/dist/components/card/Card.module.css +22 -28
- package/dist/components/card/components/CardMeta.d.ts +15 -0
- package/dist/components/card/components/CardMeta.js +20 -0
- package/dist/components/card/components/CardMeta.module.css +51 -0
- package/dist/components/card-container/CardContainer.js +1 -1
- package/dist/components/card-container/CardContainer.module.css +3 -1
- package/dist/components/chip/Chip.module.css +7 -2
- package/dist/components/circle/Circle.d.ts +2 -1
- package/dist/components/circle/Circle.js +2 -2
- package/dist/components/circle/Circle.module.css +6 -2
- package/dist/components/code-block/CodeBlock.js +1 -1
- package/dist/components/code-block/CodeBlock.module.css +30 -17
- package/dist/components/copy-button/CopyButton.d.ts +1 -0
- package/dist/components/copy-button/CopyButton.js +10 -2
- package/dist/components/datetime-picker/DateTimePicker.d.ts +33 -8
- package/dist/components/datetime-picker/DateTimePicker.js +119 -78
- package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
- package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
- package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
- package/dist/components/filter-field/FilterField.js +16 -11
- package/dist/components/filter-field/FilterField.module.css +137 -16
- package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
- package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
- package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
- package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
- package/dist/components/forms/form-select/FormSelect.js +86 -0
- package/dist/components/forms/form-select/FormSelect.module.css +236 -0
- package/dist/components/forms/input/Input.d.ts +0 -3
- package/dist/components/forms/input/Input.js +1 -4
- package/dist/components/forms/input/Input.module.css +8 -7
- package/dist/components/forms/input-container/InputContainer.module.css +1 -1
- package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
- package/dist/components/forms/select/Select.js +55 -16
- package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
- package/dist/components/hyperlink/Hyperlink.js +35 -11
- package/dist/components/hyperlink/Hyperlink.module.css +50 -2
- package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
- package/dist/components/interval-select/IntervalSelect.js +21 -6
- package/dist/components/menu/Menu.d.ts +29 -0
- package/dist/components/menu/Menu.js +61 -16
- package/dist/components/menu/Menu.module.css +73 -5
- package/dist/components/overlay/modal/Modal.module.css +4 -3
- package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
- package/dist/components/overlay/side-panel/SidePanel.js +18 -1
- package/dist/components/overlay/side-panel/SidePanel.module.css +1 -3
- package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
- package/dist/components/page-layout/PageLayout.d.ts +16 -4
- package/dist/components/page-layout/PageLayout.js +57 -28
- package/dist/components/page-layout/PageLayout.module.css +153 -33
- package/dist/components/popover/Popover.d.ts +17 -4
- package/dist/components/popover/Popover.js +147 -65
- package/dist/components/popover/Popover.module.css +5 -0
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
- package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
- package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
- package/dist/components/split-button/SplitButton.d.ts +1 -1
- package/dist/components/split-button/SplitButton.js +3 -1
- package/dist/components/split-button/SplitButton.module.css +4 -4
- package/dist/components/split-pane/SplitPane.d.ts +10 -24
- package/dist/components/split-pane/SplitPane.js +83 -54
- package/dist/components/split-pane/SplitPane.module.css +11 -6
- package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
- package/dist/components/state-page/StatePage.module.css +1 -1
- package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
- package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
- package/dist/components/table/Table.d.ts +8 -8
- package/dist/components/table/Table.js +37 -79
- package/dist/components/table/Table.module.css +62 -46
- package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +7 -3
- package/dist/components/table/TanstackTable.js +84 -0
- package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
- package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
- package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
- package/dist/components/table/components/table-settings/TableSettings.js +55 -4
- package/dist/components/table/table.utils.d.ts +17 -0
- package/dist/components/table/table.utils.js +61 -0
- package/dist/components/table/tanstackTable.utils.d.ts +22 -0
- package/dist/components/table/tanstackTable.utils.js +104 -0
- package/dist/components/tabs/Tabs.d.ts +35 -12
- package/dist/components/tabs/Tabs.js +114 -26
- package/dist/components/tabs/Tabs.module.css +158 -71
- package/dist/hooks/useTableSettings.d.ts +23 -4
- package/dist/hooks/useTableSettings.js +64 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/src/styles/styles.css +38 -23
- package/dist/styles/animation.d.ts +5 -0
- package/dist/styles/animation.js +5 -0
- package/dist/styles/styles.css +38 -23
- package/dist/styles/themes/dbc/base.css +136 -0
- package/dist/styles/themes/dbc/dark.css +39 -202
- package/dist/styles/themes/dbc/light.css +17 -174
- package/dist/utils/localStorage.utils.d.ts +19 -0
- package/dist/utils/localStorage.utils.js +78 -0
- package/package.json +4 -4
- package/dist/components/table/tanstack.js +0 -162
|
@@ -61,10 +61,18 @@ function OperatorDropdown({ value, onChange, operators, size = 'sm', disabled, }
|
|
|
61
61
|
return (_jsx(Menu.Item, { active: selected, children: _jsxs("button", { type: "button", onClick: () => handleSelect(op), disabled: disabled, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: selected ? _jsx(Check, { size: 16 }) : null }), LABELS[op]] }) }, op));
|
|
62
62
|
}) }) }));
|
|
63
63
|
}
|
|
64
|
+
function isFilterActive(value) {
|
|
65
|
+
if (Array.isArray(value))
|
|
66
|
+
return value.length > 0;
|
|
67
|
+
if (typeof value === 'string')
|
|
68
|
+
return value.trim().length > 0;
|
|
69
|
+
return value != null;
|
|
70
|
+
}
|
|
64
71
|
export function FilterField({ field, control, operator, value, onChange, operators, options = [], single = true, size = 'md', label, placeholder = 'Type value…', disabled, 'data-cy': dataCy, ...inputProps }) {
|
|
65
72
|
var _a, _b;
|
|
66
73
|
const [selectedOperator, setSelectedOperator] = useState(operator);
|
|
67
74
|
const ops = useMemo(() => operators !== null && operators !== void 0 ? operators : DEFAULT_TEXT_OPERATORS, [operators]);
|
|
75
|
+
const active = isFilterActive(value);
|
|
68
76
|
// Local state ONLY for input control (to avoid URL->props lag)
|
|
69
77
|
const [localValue, setLocalValue] = useState((_a = value) !== null && _a !== void 0 ? _a : '');
|
|
70
78
|
const debounceRef = useRef(null);
|
|
@@ -92,7 +100,7 @@ export function FilterField({ field, control, operator, value, onChange, operato
|
|
|
92
100
|
debounceRef.current = setTimeout(() => {
|
|
93
101
|
isTypingRef.current = false;
|
|
94
102
|
emit({ value: nextVal });
|
|
95
|
-
}, 250);
|
|
103
|
+
}, 250);
|
|
96
104
|
};
|
|
97
105
|
// Sync internal value when parent value changes (e.g. URL updates)
|
|
98
106
|
useEffect(() => {
|
|
@@ -100,7 +108,6 @@ export function FilterField({ field, control, operator, value, onChange, operato
|
|
|
100
108
|
if (control !== 'input')
|
|
101
109
|
return;
|
|
102
110
|
const incoming = (_a = value) !== null && _a !== void 0 ? _a : '';
|
|
103
|
-
// don't fight the user mid-typing; once parent catches up, we allow sync again
|
|
104
111
|
if (!isTypingRef.current && incoming !== localValue) {
|
|
105
112
|
setLocalValue(incoming);
|
|
106
113
|
}
|
|
@@ -115,25 +122,23 @@ export function FilterField({ field, control, operator, value, onChange, operato
|
|
|
115
122
|
clearTimeout(debounceRef.current);
|
|
116
123
|
};
|
|
117
124
|
}, []);
|
|
118
|
-
return (_jsxs("div", { ...(dataCy ? { 'data-cy': dataCy } : {}), className:
|
|
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 => {
|
|
119
126
|
const next = e.currentTarget.value;
|
|
120
127
|
isTypingRef.current = true;
|
|
121
|
-
setLocalValue(next);
|
|
122
|
-
scheduleEmitValue(next);
|
|
123
|
-
}, inputSize: size, placeholder: placeholder, width: "160px", minWidth: "120px", disabled: disabled, onClear: () => {
|
|
128
|
+
setLocalValue(next);
|
|
129
|
+
scheduleEmitValue(next);
|
|
130
|
+
}, fullWidth: true, inputSize: size, placeholder: placeholder, width: "160px", minWidth: "120px", disabled: disabled, onClear: () => {
|
|
124
131
|
isTypingRef.current = false;
|
|
125
132
|
if (debounceRef.current)
|
|
126
133
|
clearTimeout(debounceRef.current);
|
|
127
134
|
setLocalValue('');
|
|
128
|
-
emit({ value: '' });
|
|
135
|
+
emit({ value: '' });
|
|
129
136
|
} })) : single ? (_jsx(Select, { options: options, selectedValue: (_b = value) !== null && _b !== void 0 ? _b : null, onChange: v => emit({ value: v }), placeholder: placeholder, size: size, variant: "inline", onClear: () => emit({ value: '' }) })) : (_jsx(MultiSelect, { options: options, size: size, variant: "inline", selectedValues: (Array.isArray(value) ? value : []), onChange: v => {
|
|
130
137
|
const current = new Set((Array.isArray(value) ? value : []));
|
|
131
|
-
if (current.has(v))
|
|
138
|
+
if (current.has(v))
|
|
132
139
|
current.delete(v);
|
|
133
|
-
|
|
134
|
-
else {
|
|
140
|
+
else
|
|
135
141
|
current.add(v);
|
|
136
|
-
}
|
|
137
142
|
emit({ value: Array.from(current) });
|
|
138
143
|
}, onClear: () => emit({ value: [] }), children: placeholder })) })] }));
|
|
139
144
|
}
|
|
@@ -10,28 +10,34 @@
|
|
|
10
10
|
border: var(--border-width-thin) solid var(--color-border-default);
|
|
11
11
|
border-radius: var(--border-radius-default);
|
|
12
12
|
|
|
13
|
+
position: relative;
|
|
14
|
+
|
|
13
15
|
transition:
|
|
14
16
|
border-color var(--transition-fast) var(--ease-standard),
|
|
15
17
|
box-shadow var(--transition-fast) var(--ease-standard),
|
|
16
18
|
background-color var(--transition-fast) var(--ease-standard);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
/*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/* More comfortable active state:
|
|
22
|
+
- less "blue outline" noise
|
|
23
|
+
- slightly warmer surface hint
|
|
24
|
+
*/
|
|
25
|
+
.filterField.active {
|
|
26
|
+
border-color: color-mix(in srgb, var(--color-border-default) 75%, var(--color-border-selected));
|
|
27
|
+
background: color-mix(in srgb, var(--color-bg-surface) 96%, var(--color-bg-selected));
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
.filterField.sm {
|
|
25
|
-
block-size:
|
|
31
|
+
block-size: var(--component-size-sm);
|
|
26
32
|
}
|
|
27
33
|
.filterField.md {
|
|
28
|
-
block-size:
|
|
34
|
+
block-size: var(--component-size-md);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
.filterField .label {
|
|
32
38
|
display: inline-flex;
|
|
33
39
|
align-items: center;
|
|
34
|
-
padding-block:
|
|
40
|
+
padding-block: var(--spacing-2xs);
|
|
35
41
|
padding-inline: var(--spacing-sm);
|
|
36
42
|
font-size: var(--font-size-sm);
|
|
37
43
|
color: var(--color-fg-muted);
|
|
@@ -39,12 +45,13 @@
|
|
|
39
45
|
user-select: none;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
/* Operator trigger */
|
|
42
49
|
.filterField .operatorTrigger {
|
|
43
50
|
display: inline-flex;
|
|
44
51
|
align-items: center;
|
|
45
52
|
justify-content: center;
|
|
46
53
|
height: 100%;
|
|
47
|
-
padding-block:
|
|
54
|
+
padding-block: var(--spacing-2xs);
|
|
48
55
|
padding-inline: var(--spacing-sm);
|
|
49
56
|
background: var(--opac-bg-default);
|
|
50
57
|
color: var(--color-fg-default);
|
|
@@ -68,30 +75,139 @@
|
|
|
68
75
|
color: var(--color-disabled-fg);
|
|
69
76
|
background: var(--color-disabled-bg);
|
|
70
77
|
}
|
|
71
|
-
|
|
72
78
|
.filterField .operatorText {
|
|
73
79
|
white-space: nowrap;
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
/* When active, operator is less dominant (calmer + more "token"-like) */
|
|
83
|
+
.filterField.active .operatorTrigger {
|
|
84
|
+
background: transparent;
|
|
85
|
+
color: var(--color-fg-muted);
|
|
86
|
+
}
|
|
87
|
+
.filterField.active .operatorTrigger:hover {
|
|
88
|
+
background: var(--opac-bg-dark);
|
|
89
|
+
color: var(--color-fg-default);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Wrapper for the select / multiselect control */
|
|
76
93
|
.filterField .valueWrapper {
|
|
77
94
|
display: inline-flex;
|
|
78
95
|
align-items: center;
|
|
79
96
|
padding: 0;
|
|
80
97
|
height: 100%;
|
|
98
|
+
|
|
99
|
+
flex: 1;
|
|
100
|
+
min-width: 0;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
|
-
|
|
103
|
+
/* Ensure the control inside can stretch/shrink */
|
|
104
|
+
.filterField .valueWrapper > * {
|
|
84
105
|
height: 100%;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
106
|
+
width: 100%;
|
|
107
|
+
min-width: 0;
|
|
88
108
|
}
|
|
89
|
-
|
|
90
|
-
.valueWrapper button {
|
|
109
|
+
.filterField .valueWrapper > * > * {
|
|
91
110
|
height: 100% !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Active: emphasize VALUE area (but keep it soft) */
|
|
114
|
+
.filterField.active .valueWrapper {
|
|
115
|
+
background: color-mix(in srgb, var(--color-bg-surface) 88%, var(--color-bg-selected));
|
|
116
|
+
border-left: var(--border-width-thin) solid
|
|
117
|
+
color-mix(in srgb, var(--color-border-default) 80%, var(--color-border-selected));
|
|
118
|
+
border-top-right-radius: calc(var(--border-radius-default) - 1px);
|
|
119
|
+
border-bottom-right-radius: calc(var(--border-radius-default) - 1px);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* =========================
|
|
123
|
+
TRIGGER BUTTON TARGETING
|
|
124
|
+
========================= */
|
|
125
|
+
|
|
126
|
+
/* Select trigger button */
|
|
127
|
+
.filterField .valueWrapper :global(button[data-forminput]) {
|
|
128
|
+
width: 100%;
|
|
129
|
+
height: 100%;
|
|
130
|
+
border: 0 !important;
|
|
131
|
+
|
|
132
|
+
/* slightly more breathing room than before */
|
|
133
|
+
padding-inline: calc(var(--spacing-sm) + var(--spacing-2xs)) !important;
|
|
134
|
+
|
|
135
|
+
text-align: left;
|
|
136
|
+
justify-content: flex-start;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* MultiSelect trigger button (Popover container is a div; trigger is its direct child button) */
|
|
140
|
+
.filterField .valueWrapper > div > button {
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
92
143
|
border: 0 !important;
|
|
93
|
-
padding-inline: var(--spacing-sm) !important;
|
|
144
|
+
padding-inline: calc(var(--spacing-sm) + var(--spacing-2xs)) !important;
|
|
145
|
+
|
|
146
|
+
text-align: left;
|
|
147
|
+
justify-content: flex-start;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Slight spacing between clear (×) and chevron for Select + MultiSelect
|
|
151
|
+
(feels less cramped / more intentional) */
|
|
152
|
+
.filterField .valueWrapper :global(button[data-forminput]) :global(.dbc-flex),
|
|
153
|
+
.filterField .valueWrapper > div > button {
|
|
154
|
+
column-gap: var(--spacing-xs);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Make the internal Select layout behave */
|
|
158
|
+
.filterField .valueWrapper :global(.dbc-flex) {
|
|
159
|
+
width: 100% !important;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.filterField .valueWrapper :global(.dbc-flex > span:first-child) {
|
|
164
|
+
flex: 1;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
text-overflow: ellipsis;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Keep ClearButton + chevron from stretching */
|
|
172
|
+
.filterField .valueWrapper :global(.dbc-flex) > *:not(span:first-child) {
|
|
173
|
+
flex: 0 0 auto;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* For MultiSelect: label + chip should truncate nicely */
|
|
177
|
+
.filterField .valueWrapper > div > button > span:first-child {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: var(--spacing-xxs);
|
|
181
|
+
|
|
182
|
+
flex: 1;
|
|
183
|
+
min-width: 0;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
text-overflow: ellipsis;
|
|
186
|
+
white-space: nowrap;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Emphasize chosen value text in active state (but slightly less "boldy") */
|
|
190
|
+
.filterField.active .valueWrapper input {
|
|
191
|
+
font-weight: 550;
|
|
94
192
|
}
|
|
193
|
+
.filterField.active .valueWrapper :global(button[data-forminput]),
|
|
194
|
+
.filterField.active .valueWrapper > div > button {
|
|
195
|
+
font-weight: 550 !important;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Icons calmer by default; crisp on hover/focus */
|
|
199
|
+
.filterField.active .valueWrapper svg {
|
|
200
|
+
opacity: 0.72;
|
|
201
|
+
}
|
|
202
|
+
.filterField.active .valueWrapper:hover svg,
|
|
203
|
+
.filterField.active .valueWrapper :global(button[data-forminput]):focus-visible svg,
|
|
204
|
+
.filterField.active .valueWrapper > div > button:focus-visible svg {
|
|
205
|
+
opacity: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* =========================
|
|
209
|
+
INPUT styling
|
|
210
|
+
========================= */
|
|
95
211
|
|
|
96
212
|
.filterField input {
|
|
97
213
|
appearance: none;
|
|
@@ -103,6 +219,11 @@
|
|
|
103
219
|
inline-size: auto;
|
|
104
220
|
min-inline-size: 10ch;
|
|
105
221
|
block-size: 100%;
|
|
222
|
+
border-top-left-radius: 0;
|
|
223
|
+
border-bottom-left-radius: 0;
|
|
224
|
+
|
|
225
|
+
/* a tiny bit more comfort */
|
|
226
|
+
padding-block: var(--spacing-3xs);
|
|
106
227
|
}
|
|
107
228
|
|
|
108
229
|
.filterField button {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { JSX } from 'react';
|
|
2
|
+
import type { JSX, ReactNode } from 'react';
|
|
3
3
|
type Variant = 'default' | 'primary' | 'outlined';
|
|
4
4
|
type Size = 'sm' | 'md' | 'lg';
|
|
5
5
|
interface CheckboxProps {
|
|
@@ -8,7 +8,7 @@ interface CheckboxProps {
|
|
|
8
8
|
variant?: Variant;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
modified?: boolean;
|
|
11
|
-
label?:
|
|
11
|
+
label?: ReactNode;
|
|
12
12
|
size?: Size;
|
|
13
13
|
containerLabel?: string;
|
|
14
14
|
error?: string;
|
|
@@ -66,7 +66,7 @@ export function CheckboxGroup({ label, options, selectedValues, onChange, onTogg
|
|
|
66
66
|
return (_jsxs("div", { className: wrapperClassName, "data-cy": dataCy, style: { ['--checkboxgroup-action-min-width']: actionMinWidth }, children: [label && _jsx("span", { className: styles.groupLabel, children: label }), _jsx("div", { className: itemsClassName, role: "group", "aria-label": label, children: options.map(opt => {
|
|
67
67
|
const isChecked = selectedSet.has(opt.value);
|
|
68
68
|
const isDisabled = disabled || !!opt.disabled;
|
|
69
|
-
return (_jsx(Checkbox, { label: opt.label, checked: isChecked, disabled: isDisabled, variant: variant, size:
|
|
69
|
+
return (_jsx(Checkbox, { label: opt.label, checked: isChecked, disabled: isDisabled, variant: variant, size: "sm", onChange: () => {
|
|
70
70
|
if (isDisabled)
|
|
71
71
|
return;
|
|
72
72
|
toggleValue(opt.value);
|
|
@@ -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
|
+
}
|