@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.
- 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 +9 -18
- package/dist/components/card/Card.js +34 -23
- package/dist/components/card/Card.module.css +22 -87
- 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/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.module.css +5 -5
- 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 +0 -3
- package/dist/components/forms/input/Input.module.css +7 -7
- package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
- package/dist/components/forms/select/Select.js +55 -16
- 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 +11 -14
- package/dist/components/menu/Menu.js +18 -33
- package/dist/components/menu/Menu.module.css +2 -2
- package/dist/components/overlay/modal/Modal.module.css +2 -1
- package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
- package/dist/components/overlay/side-panel/SidePanel.js +1 -1
- package/dist/components/overlay/side-panel/SidePanel.module.css +1 -1
- 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/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/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 +3 -8
- package/dist/components/table/Table.js +37 -76
- package/dist/components/table/Table.module.css +45 -42
- package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +5 -12
- 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/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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/src/styles/styles.css +0 -1
- package/dist/styles/styles.css +0 -1
- 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/package.json +4 -4
- 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(--
|
|
47
|
-
padding-block:
|
|
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(--
|
|
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:
|
|
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:
|
|
108
|
+
block-size: var(--component-size-sm);
|
|
109
109
|
font-size: var(--font-size-sm);
|
|
110
110
|
}
|
|
111
111
|
.md {
|
|
112
|
-
block-size:
|
|
112
|
+
block-size: var(--component-size-md);
|
|
113
113
|
font-size: var(--font-size-sm);
|
|
114
114
|
}
|
|
115
115
|
.lg {
|
|
116
|
-
block-size:
|
|
116
|
+
block-size: var(--component-size-lg);
|
|
117
117
|
font-size: var(--font-size-lg);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -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,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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,
|
|
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
|
-
|
|
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 {};
|