@dbcdk/react-components 0.0.59 → 0.0.61

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.
@@ -27,10 +27,11 @@ export interface FilterFieldProps extends Omit<React.InputHTMLAttributes<HTMLInp
27
27
  minWidth?: string;
28
28
  width?: string;
29
29
  maxWidth?: string;
30
+ popoverWidth?: string;
30
31
  debounceTime?: number;
31
32
  }
32
33
  export declare const NUMBER_OPERATORS: Operator[];
33
- export declare function FilterField({ field, control, operator, value, onChange, operators, options, single, size, variant, label, placeholder, disabled, 'data-cy': dataCy, minWidth, width, maxWidth, debounceTime, ...inputProps }: FilterFieldProps & {
34
+ export declare function FilterField({ field, control, operator, value, onChange, operators, options, single, size, variant, label, placeholder, disabled, 'data-cy': dataCy, minWidth, width, maxWidth, popoverWidth, debounceTime, ...inputProps }: FilterFieldProps & {
34
35
  'data-cy'?: string;
35
36
  }): React.ReactElement;
36
37
  export {};
@@ -69,14 +69,18 @@ function isFilterActive(value) {
69
69
  return value.trim().length > 0;
70
70
  return value != null;
71
71
  }
72
- export function FilterField({ field, control, operator, value, onChange, operators, options = [], single = true, size = 'md', variant = 'surface', label, placeholder = 'Type value…', disabled, 'data-cy': dataCy, minWidth, width, maxWidth, debounceTime = INPUT_DEBOUNCE_MS, ...inputProps }) {
72
+ export function FilterField({ field, control, operator, value, onChange, operators, options = [], single = true, size = 'md', variant = 'surface', label, placeholder = 'Type value…', disabled, 'data-cy': dataCy, minWidth, width, maxWidth, popoverWidth, debounceTime = INPUT_DEBOUNCE_MS, ...inputProps }) {
73
73
  var _a, _b, _c, _d, _e, _f;
74
74
  const filterFieldRef = useRef(null);
75
75
  const ops = useMemo(() => operators !== null && operators !== void 0 ? operators : DEFAULT_TEXT_OPERATORS, [operators]);
76
+ const shouldAutoFitTypeahead = control === 'select' && single && !width;
76
77
  const [selectedOperator, setSelectedOperator] = useState(operator);
77
78
  const active = isFilterActive(value);
78
79
  useEffect(() => {
79
80
  if (ops.includes(operator)) {
81
+ // Keep the dropdown responsive to local selection immediately, but let the
82
+ // controlled `operator` prop take back over once parent state catches up.
83
+ // eslint-disable-next-line react-hooks/set-state-in-effect
80
84
  setSelectedOperator(operator);
81
85
  }
82
86
  }, [operator, ops]);
@@ -149,6 +153,9 @@ export function FilterField({ field, control, operator, value, onChange, operato
149
153
  return;
150
154
  }
151
155
  if (incoming !== localValue) {
156
+ // Keep the embedded input responsive while debounced updates are in flight,
157
+ // then resync to the controlled value once external state has settled.
158
+ // eslint-disable-next-line react-hooks/set-state-in-effect
152
159
  setLocalValue(incoming);
153
160
  }
154
161
  }, [value, control, localValue]);
@@ -159,9 +166,13 @@ export function FilterField({ field, control, operator, value, onChange, operato
159
166
  }, []);
160
167
  return (_jsxs("div", { ref: filterFieldRef, ...(dataCy ? { 'data-cy': dataCy } : {}), className: [styles.filterField, styles[size], styles[variant], active ? styles.active : '']
161
168
  .filter(Boolean)
162
- .join(' '), 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: [styles.valueWrapper, control === 'input' ? 'dbc-flex dbc-flex-grow' : '']
169
+ .join(' '), children: [label ? _jsx("span", { className: `${styles.label} ${styles[size]}`, children: label }) : null, _jsx("div", { className: styles.operatorWrapper, children: _jsx(OperatorDropdown, { value: selectedOperator, onChange: handleOperatorChange, operators: ops, size: size, disabled: disabled }) }), _jsx("div", { className: [
170
+ styles.valueWrapper,
171
+ control === 'input' ? 'dbc-flex dbc-flex-grow' : '',
172
+ shouldAutoFitTypeahead ? styles.autoWidth : '',
173
+ ]
163
174
  .filter(Boolean)
164
- .join(' '), style: { width, maxWidth }, children: control === 'input' ? (_jsx(Input, { variant: "embedded", ...inputProps, fieldClassName: styles.embeddedInputField, inputClassName: styles.embeddedInputElement, value: localValue, onChange: e => {
175
+ .join(' '), style: shouldAutoFitTypeahead ? (maxWidth ? { maxWidth } : undefined) : { width, maxWidth }, children: control === 'input' ? (_jsx(Input, { variant: "embedded", ...inputProps, fieldClassName: styles.embeddedInputField, inputClassName: styles.embeddedInputElement, value: localValue, onChange: e => {
165
176
  const next = e.currentTarget.value;
166
177
  setLocalValue(next);
167
178
  scheduleEmitValue(next);
@@ -172,9 +183,9 @@ export function FilterField({ field, control, operator, value, onChange, operato
172
183
  pendingValueRef.current = '';
173
184
  setLocalValue('');
174
185
  emit({ value: '' });
175
- } })) : (_jsx(Typeahead, { options: options, mode: single ? 'single' : 'multi', selectedValue: single ? ((_f = value) !== null && _f !== void 0 ? _f : null) : Array.isArray(value) ? value : [], onChange: v => emit({ value: v }), minWidth: minWidth, popoverAnchorRef: filterFieldRef, placeholder: placeholder, variant: "embedded", inputProps: {
186
+ } })) : (_jsx(Typeahead, { options: options, mode: single ? 'single' : 'multi', selectedValue: single ? ((_f = value) !== null && _f !== void 0 ? _f : null) : Array.isArray(value) ? value : [], onChange: v => emit({ value: v }), minWidth: minWidth, popoverWidth: popoverWidth, popoverAnchorRef: filterFieldRef, placeholder: placeholder, variant: "embedded", inputProps: {
176
187
  inputSize: size,
177
188
  fieldClassName: styles.embeddedInputField,
178
189
  inputClassName: styles.embeddedInputElement,
179
- }, onClear: () => emit({ value: single ? '' : [] }), disabled: disabled, fullWidth: true })) })] }));
190
+ }, onClear: () => emit({ value: single ? '' : [] }), disabled: disabled, fullWidth: !shouldAutoFitTypeahead, fitContent: shouldAutoFitTypeahead })) })] }));
180
191
  }
@@ -34,8 +34,14 @@
34
34
  }
35
35
 
36
36
  .filterField.surface.active {
37
- border-color: var(--color-border-default);
37
+ background: color-mix(in srgb, var(--color-bg-selected) 45%, var(--color-bg-surface));
38
+ border-color: var(--color-border-selected);
38
39
  box-shadow: var(--shadow-sm);
40
+ --filter-operator-bg: color-mix(
41
+ in srgb,
42
+ var(--color-bg-selected) 45%,
43
+ var(--color-bg-surface)
44
+ );
39
45
  }
40
46
 
41
47
  .filterField.outlined {
@@ -50,7 +56,14 @@
50
56
  }
51
57
 
52
58
  .filterField.outlined.active {
53
- border-color: var(--color-border-default);
59
+ background: color-mix(in srgb, var(--color-bg-selected) 38%, var(--color-bg-surface));
60
+ border-color: var(--color-border-selected);
61
+ box-shadow: none;
62
+ --filter-operator-bg: color-mix(
63
+ in srgb,
64
+ var(--color-bg-selected) 38%,
65
+ var(--color-bg-surface)
66
+ );
54
67
  }
55
68
 
56
69
  .filterField.subtle {
@@ -70,34 +83,22 @@
70
83
  }
71
84
 
72
85
  .filterField.subtle.active {
73
- background: var(--color-bg-surface-strong);
86
+ background: color-mix(
87
+ in srgb,
88
+ var(--color-bg-selected) 55%,
89
+ var(--color-bg-surface-strong)
90
+ );
91
+ border-color: var(--color-border-selected);
92
+ box-shadow: inset 0 0 0 1px transparent;
74
93
  --filter-operator-bg: color-mix(
75
94
  in srgb,
76
- var(--color-fg-default) 5%,
95
+ var(--color-bg-selected) 55%,
77
96
  var(--color-bg-surface-strong)
78
97
  );
79
98
  }
80
99
 
81
- /* =========================
82
- ACTIVE INDICATOR
83
- ========================= */
84
-
85
- .filterField.active::before {
86
- content: '';
87
- position: absolute;
88
- inset-inline-start: 0;
89
- top: 0;
90
- bottom: 0;
91
- width: 3px;
92
- border-top-left-radius: inherit;
93
- border-bottom-left-radius: inherit;
94
- background: var(--color-border-selected);
95
- pointer-events: none;
96
- z-index: 2;
97
- }
98
-
99
- .filterField.outlined.active::before {
100
- width: 3px;
100
+ .filterField.active .label {
101
+ color: var(--color-fg-default);
101
102
  }
102
103
 
103
104
  /* =========================
@@ -114,7 +115,6 @@
114
115
  border-color: var(--color-border-selected);
115
116
  }
116
117
 
117
- /* subtle focus without inner outline */
118
118
  .filterField.surface:focus-within {
119
119
  box-shadow: var(--shadow-sm);
120
120
  }
@@ -124,17 +124,13 @@
124
124
  box-shadow: none;
125
125
  }
126
126
 
127
- /* stronger focus when filter is active */
128
-
129
127
  .filterField.surface.active:focus-within {
130
- box-shadow:
131
- var(--shadow-sm),
132
- inset 0 0 0 1px var(--color-border-selected);
128
+ box-shadow: var(--shadow-sm);
133
129
  }
134
130
 
135
131
  .filterField.outlined.active:focus-within,
136
132
  .filterField.subtle.active:focus-within {
137
- box-shadow: inset 0 0 0 1px var(--color-border-selected);
133
+ box-shadow: none;
138
134
  }
139
135
 
140
136
  /* =========================
@@ -173,6 +169,13 @@
173
169
  OPERATOR
174
170
  ========================= */
175
171
 
172
+ .filterField .operatorWrapper {
173
+ flex: 0 0 auto;
174
+ display: flex;
175
+ align-items: stretch;
176
+ min-width: 0;
177
+ }
178
+
176
179
  .filterField .operatorTrigger {
177
180
  display: inline-flex;
178
181
  align-items: center;
@@ -192,10 +195,10 @@
192
195
  transition:
193
196
  background-color var(--transition-fast) var(--ease-standard),
194
197
  color var(--transition-fast) var(--ease-standard);
198
+ flex: 0 0 auto;
199
+ white-space: nowrap;
195
200
  }
196
201
 
197
- /* inset operator background */
198
-
199
202
  .filterField .operatorTrigger::after {
200
203
  content: '';
201
204
  position: absolute;
@@ -221,6 +224,7 @@
221
224
  var(--filter-operator-bg, transparent)
222
225
  );
223
226
  }
227
+
224
228
  /* =========================
225
229
  SEPARATORS
226
230
  ========================= */
@@ -262,7 +266,9 @@
262
266
  .operatorTrigger svg {
263
267
  height: var(--component-size-xxs);
264
268
  width: var(--component-size-xxs);
269
+ flex: 0 0 auto;
265
270
  }
271
+
266
272
  /* =========================
267
273
  VALUE WRAPPER
268
274
  ========================= */
@@ -272,14 +278,14 @@
272
278
  align-items: center;
273
279
  padding: 0;
274
280
  height: 100%;
275
- flex: 1;
281
+ flex: 1 1 auto;
276
282
  min-width: 0;
277
283
  position: relative;
278
284
  }
279
285
 
286
+ /* Base child sizing */
280
287
  .filterField .valueWrapper > * {
281
288
  height: 100%;
282
- width: 100%;
283
289
  min-width: 0;
284
290
  }
285
291
 
@@ -287,10 +293,42 @@
287
293
  display: flex;
288
294
  align-items: center;
289
295
  height: 100%;
296
+ min-width: 0;
297
+ }
298
+
299
+ /* Fill mode: regular inputs should stretch */
300
+ .filterField .valueWrapper:not(.autoWidth) > * {
301
+ width: 100%;
302
+ flex: 1 1 auto;
303
+ }
304
+
305
+ .filterField .valueWrapper:not(.autoWidth) > div {
290
306
  width: 100%;
307
+ flex: 1 1 auto;
308
+ }
309
+
310
+ /* Auto-width mode: let Typeahead keep its measured width */
311
+ .filterField .valueWrapper.autoWidth {
312
+ flex: 1 1 auto;
313
+ width: auto;
291
314
  min-width: 0;
292
315
  }
293
316
 
317
+ .filterField .valueWrapper.autoWidth > * {
318
+ width: auto;
319
+ flex: 1 1 auto;
320
+ min-width: 0;
321
+ max-width: 100%;
322
+ }
323
+
324
+ .filterField .valueWrapper.autoWidth > div {
325
+ width: auto;
326
+ flex: 1 1 auto;
327
+ min-width: 0;
328
+ max-width: 100%;
329
+ }
330
+
331
+ /* Embedded Input internals */
294
332
  .filterField .valueWrapper .field {
295
333
  min-height: unset;
296
334
  height: 100%;
@@ -298,13 +336,23 @@
298
336
  box-shadow: none;
299
337
  border: none;
300
338
  background: transparent;
339
+ min-width: 0;
340
+ width: 100%;
341
+ flex: 1 1 auto;
301
342
  }
302
343
 
344
+ /* Keep a small breathing space inside the filter row */
303
345
  .embeddedInputField {
304
- padding: 0;
346
+ min-width: 0;
347
+ width: 100%;
348
+ flex: 1 1 auto;
305
349
  }
306
350
 
307
351
  .embeddedInputElement {
352
+ flex: 1 1 auto;
353
+ inline-size: 100%;
354
+ width: 100%;
355
+ min-width: 0;
308
356
  height: 100%;
309
357
  block-size: 100%;
310
358
  min-height: unset;
@@ -313,12 +361,14 @@
313
361
  box-shadow: none;
314
362
  padding-block: 0;
315
363
  padding-inline: 0;
316
- padding-inline-start: 0;
317
364
  margin: 0;
365
+ box-sizing: border-box;
318
366
  }
367
+
319
368
  .filterField .valueWrapper .startAdornment {
320
369
  margin-left: 0;
321
370
  gap: 2px;
371
+ min-width: 0;
322
372
  }
323
373
 
324
374
  /* =========================
@@ -333,6 +383,7 @@
333
383
 
334
384
  .filterField input {
335
385
  height: 100%;
386
+ min-width: 0;
336
387
  }
337
388
 
338
389
  .filterField input::placeholder {
@@ -28,8 +28,11 @@ export const Input = forwardRef(function Input({ label, error, helpText, orienta
28
28
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
29
29
  }, [autoFocus]);
30
30
  const hasButton = Boolean(onButtonClick || buttonLabel || buttonIcon);
31
- const hasVisibleClear = Boolean(onClear && inputProps.value);
32
- const hasInlineClear = Boolean(hasVisibleClear && endAdornment);
31
+ const hasValue = Boolean(inputProps.value);
32
+ const hasVisibleClear = Boolean(onClear && hasValue);
33
+ const hasEndAdornment = Boolean(endAdornment);
34
+ const reservesInlineClearSlot = Boolean(onClear);
35
+ const hasInlineClear = Boolean(hasVisibleClear && hasEndAdornment);
33
36
  const rootStyle = {
34
37
  ...(style !== null && style !== void 0 ? style : {}),
35
38
  ...(minWidth ? { ['--input-min-width']: minWidth } : null),
@@ -45,7 +48,7 @@ export const Input = forwardRef(function Input({ label, error, helpText, orienta
45
48
  return (_jsx(InputContainer, { label: label, htmlFor: inputId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: _jsxs("div", { style: rootStyle, className: [
46
49
  styles.container,
47
50
  fullWidth ? styles.fullWidth : '',
48
- hasVisibleClear ? styles.withClear : '',
51
+ onClear ? styles.withClear : '',
49
52
  hasInlineClear ? styles.withInlineClear : '',
50
53
  hasButton ? styles.withButton : '',
51
54
  className !== null && className !== void 0 ? className : '',
@@ -60,8 +63,8 @@ export const Input = forwardRef(function Input({ label, error, helpText, orienta
60
63
  fieldClassName !== null && fieldClassName !== void 0 ? fieldClassName : '',
61
64
  ]
62
65
  .filter(Boolean)
63
- .join(' '), "data-forminput": "field", "data-modified": modified ? 'true' : undefined, "aria-disabled": inputProps.disabled ? 'true' : undefined, ...(tooltip ? triggerProps : {}), children: [icon && _jsx("span", { className: styles.icon, children: icon }), startAdornment && _jsx("span", { className: styles.startAdornment, children: startAdornment }), _jsx("input", { ...inputProps, id: inputId, ref: mergeRefs(inputRef, ref), className: [styles.input, inputSize ? styles[inputSize] : '', inputClassName !== null && inputClassName !== void 0 ? inputClassName : '']
66
+ .join(' '), "data-forminput": "field", "data-modified": modified ? 'true' : undefined, "aria-disabled": inputProps.disabled ? 'true' : undefined, ...(tooltip ? triggerProps : {}), children: [icon && (_jsx("span", { className: styles.icon, "data-input-role": "icon", children: icon })), startAdornment && (_jsx("span", { className: styles.startAdornment, "data-input-role": "start-adornment", children: startAdornment })), _jsx("input", { ...inputProps, id: inputId, ref: mergeRefs(inputRef, ref), className: [styles.input, inputSize ? styles[inputSize] : '', inputClassName !== null && inputClassName !== void 0 ? inputClassName : '']
64
67
  .filter(Boolean)
65
- .join(' ') }), (hasInlineClear || endAdornment) && (_jsxs("span", { className: styles.endAdornment, children: [hasInlineClear && onClear ? _jsx(ClearButton, { onClick: onClear }) : null, endAdornment] })), hasVisibleClear && !hasInlineClear && onClear ? (_jsx(ClearButton, { onClick: onClear, absolute: true })) : null] }), hasButton && (_jsxs(Button, { onClick: onButtonClick, className: styles.trailingButton, type: "button", variant: trailingButtonVariant, size: inputSize, children: [buttonIcon !== null && buttonIcon !== void 0 ? buttonIcon : null, buttonLabel !== null && buttonLabel !== void 0 ? buttonLabel : null] }))] }) }));
68
+ .join(' ') }), (reservesInlineClearSlot || hasEndAdornment) && (_jsxs("span", { className: styles.endAdornment, "data-input-role": "end-adornment", children: [reservesInlineClearSlot ? (_jsx("span", { className: styles.clearSlot, "aria-hidden": hasVisibleClear ? undefined : 'true', children: hasVisibleClear && onClear ? _jsx(ClearButton, { onClick: onClear }) : null })) : null, endAdornment] })), hasVisibleClear && !hasEndAdornment && onClear ? (_jsx(ClearButton, { onClick: onClear, absolute: true })) : null] }), hasButton && (_jsxs(Button, { onClick: onButtonClick, className: styles.trailingButton, type: "button", variant: trailingButtonVariant, size: inputSize, children: [buttonIcon !== null && buttonIcon !== void 0 ? buttonIcon : null, buttonLabel !== null && buttonLabel !== void 0 ? buttonLabel : null] }))] }) }));
66
69
  });
67
70
  Input.displayName = 'Input';
@@ -73,12 +73,19 @@
73
73
  border-bottom-right-radius: 0;
74
74
  }
75
75
 
76
+ /*
77
+ When onClear exists, do not keep extra inline-end padding on the input itself.
78
+ The clear affordance already reserves the needed space via:
79
+ - the inline clearSlot inside endAdornment, or
80
+ - the absolute clear button path.
81
+ */
76
82
  .withClear .input {
77
- padding-inline-end: calc(var(--spacing-xxs) + 16px + var(--spacing-xxs));
83
+ padding-inline-end: 0;
78
84
  }
79
85
 
86
+ /* When clear is inline with other end adornments, keep the input tight as well. */
80
87
  .withInlineClear .input {
81
- padding-inline-end: var(--spacing-xs);
88
+ padding-inline-end: 0;
82
89
  }
83
90
 
84
91
  /* Global focus reset - variants own visible focus treatment */
@@ -308,7 +315,10 @@
308
315
  .xs {
309
316
  block-size: var(--component-size-xs);
310
317
  font-size: var(--font-size-xs);
311
- padding: 0 var(--spacing-xxs);
318
+ }
319
+
320
+ .input.xs {
321
+ padding-inline: var(--spacing-xxs);
312
322
  }
313
323
 
314
324
  .sm {
@@ -399,6 +409,17 @@
399
409
  .endAdornment {
400
410
  display: flex;
401
411
  align-items: center;
412
+ flex: 0 0 auto;
413
+ gap: 2px;
414
+ margin-inline-start: auto;
402
415
  margin-right: var(--spacing-xxs);
403
416
  color: var(--color-fg-subtle);
404
417
  }
418
+
419
+ .clearSlot {
420
+ display: inline-flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ min-inline-size: calc(16px + (var(--spacing-xxs) * 2));
424
+ min-block-size: calc(16px + (var(--spacing-xxs) * 2));
425
+ }
@@ -173,7 +173,9 @@ export function Select({ label, error, helpText, orientation = 'vertical', label
173
173
  ? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
174
174
  : opt.value === selectedValue;
175
175
  const isActive = index === activeIndex;
176
- return (_jsx(Menu.Item, { active: isActive, itemRole: "option", "aria-selected": isSelected, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, onClick: () => {
176
+ return (_jsx(Menu.Item, { active: isActive, itemRole: "option", "aria-selected": isSelected, children: _jsxs("button", { ref: el => {
177
+ optionRefs.current[index] = el;
178
+ }, type: "button", tabIndex: isActive ? 0 : -1, onClick: () => {
177
179
  var _a;
178
180
  onChange(opt.value);
179
181
  (_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
@@ -26,11 +26,13 @@ interface TypeaheadProps<T> {
26
26
  inputSize?: InputProps['inputSize'];
27
27
  width?: InputProps['width'];
28
28
  minWidth?: string;
29
+ popoverWidth?: string;
29
30
  autoComplete?: InputProps['autoComplete'];
30
31
  autoCorrect?: InputProps['autoCorrect'];
31
32
  autoCapitalize?: InputProps['autoCapitalize'];
32
33
  spellCheck?: InputProps['spellCheck'];
33
34
  popoverAnchorRef?: React.RefObject<HTMLElement | null>;
35
+ fitContent?: boolean;
34
36
  }
35
- export declare function Typeahead<T extends string | number>({ options, mode, multiValueDisplayMode, multiSelectedValuesDisplayMode, multiSelectedValueChipContent, selectedValue, onChange, placeholder, variant, disabled, fullWidth, onClear, emptyMessage, filterOptions, inputProps, inputSize, width, minWidth, autoComplete, autoCorrect, autoCapitalize, spellCheck, popoverAnchorRef, }: TypeaheadProps<T>): React.ReactElement;
37
+ export declare function Typeahead<T extends string | number>({ options, mode, multiValueDisplayMode, multiSelectedValuesDisplayMode, multiSelectedValueChipContent, selectedValue, onChange, placeholder, variant, disabled, fullWidth, onClear, emptyMessage, filterOptions, inputProps, inputSize, width, minWidth, popoverWidth, autoComplete, autoCorrect, autoCapitalize, spellCheck, popoverAnchorRef, fitContent, }: TypeaheadProps<T>): React.ReactElement;
36
38
  export {};
@@ -1,14 +1,93 @@
1
1
  'use client';
2
2
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
- import { useEffect, useId, useMemo, useRef, useState } from 'react';
4
+ import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import { Chip } from '../../../components/chip/Chip';
6
6
  import { Input } from '../../../components/forms/input/Input';
7
7
  import { Menu } from '../../../components/menu/Menu';
8
8
  import { Popover } from '../../../components/popover/Popover';
9
9
  import styles from './Typeahead.module.css';
10
- export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'chips', multiSelectedValuesDisplayMode = 'hidden', multiSelectedValueChipContent = 'label', selectedValue = null, onChange, placeholder, variant = 'outlined', disabled = false, fullWidth = false, onClear, emptyMessage = 'Ingen resultater', filterOptions, inputProps, inputSize, width, minWidth, autoComplete, autoCorrect, autoCapitalize, spellCheck, popoverAnchorRef, }) {
11
- var _a;
10
+ function parseLengthToPx(value, referenceFontSize = 16) {
11
+ if (!value)
12
+ return 0;
13
+ const trimmed = value.trim();
14
+ if (!trimmed)
15
+ return 0;
16
+ if (trimmed.endsWith('px')) {
17
+ const n = Number.parseFloat(trimmed);
18
+ return Number.isFinite(n) ? n : 0;
19
+ }
20
+ if (trimmed.endsWith('rem')) {
21
+ const n = Number.parseFloat(trimmed);
22
+ if (!Number.isFinite(n))
23
+ return 0;
24
+ const rootFont = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16');
25
+ return n * (Number.isFinite(rootFont) ? rootFont : 16);
26
+ }
27
+ if (trimmed.endsWith('em')) {
28
+ const n = Number.parseFloat(trimmed);
29
+ return Number.isFinite(n) ? n * referenceFontSize : 0;
30
+ }
31
+ const n = Number.parseFloat(trimmed);
32
+ return Number.isFinite(n) ? n : 0;
33
+ }
34
+ function getOuterWidthWithMargins(element) {
35
+ if (!(element instanceof HTMLElement))
36
+ return 0;
37
+ const rect = element.getBoundingClientRect();
38
+ const styles = window.getComputedStyle(element);
39
+ const marginLeft = Number.parseFloat(styles.marginLeft || '0');
40
+ const marginRight = Number.parseFloat(styles.marginRight || '0');
41
+ return Math.ceil(rect.width + marginLeft + marginRight);
42
+ }
43
+ function getInputFontShorthand(inputStyles) {
44
+ return (inputStyles.font ||
45
+ [
46
+ inputStyles.fontStyle,
47
+ inputStyles.fontVariant,
48
+ inputStyles.fontWeight,
49
+ inputStyles.fontStretch,
50
+ inputStyles.fontSize,
51
+ inputStyles.lineHeight !== 'normal' ? `/${inputStyles.lineHeight}` : '',
52
+ inputStyles.fontFamily,
53
+ ]
54
+ .filter(Boolean)
55
+ .join(' '));
56
+ }
57
+ function measureFitContentWidth(input, text, minWidth) {
58
+ const field = input.closest('[data-forminput="field"]');
59
+ if (!field)
60
+ return null;
61
+ const inputStyles = window.getComputedStyle(input);
62
+ const fieldStyles = window.getComputedStyle(field);
63
+ const canvas = document.createElement('canvas');
64
+ const ctx = canvas.getContext('2d');
65
+ if (!ctx)
66
+ return null;
67
+ ctx.font = getInputFontShorthand(inputStyles);
68
+ const textWidth = Math.ceil(ctx.measureText(text).width);
69
+ const inputFontSize = Number.parseFloat(inputStyles.fontSize || '16');
70
+ const inputPaddingLeft = Number.parseFloat(inputStyles.paddingLeft || '0');
71
+ const inputPaddingRight = Number.parseFloat(inputStyles.paddingRight || '0');
72
+ const borderLeft = Number.parseFloat(fieldStyles.borderLeftWidth || '0');
73
+ const borderRight = Number.parseFloat(fieldStyles.borderRightWidth || '0');
74
+ const startAdornmentWidth = getOuterWidthWithMargins(field.querySelector('[data-input-role="start-adornment"]'));
75
+ const endAdornmentWidth = getOuterWidthWithMargins(field.querySelector('[data-input-role="end-adornment"]'));
76
+ const iconWidth = getOuterWidthWithMargins(field.querySelector('[data-input-role="icon"]'));
77
+ const breathingRoom = 6;
78
+ const nextWidth = Math.ceil(borderLeft +
79
+ borderRight +
80
+ iconWidth +
81
+ startAdornmentWidth +
82
+ inputPaddingLeft +
83
+ textWidth +
84
+ breathingRoom +
85
+ inputPaddingRight +
86
+ endAdornmentWidth);
87
+ return Math.max(parseLengthToPx(minWidth, inputFontSize) || 120, nextWidth);
88
+ }
89
+ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'chips', multiSelectedValuesDisplayMode = 'hidden', multiSelectedValueChipContent = 'label', selectedValue = null, onChange, placeholder, variant = 'outlined', disabled = false, fullWidth = false, onClear, emptyMessage = 'Ingen resultater', filterOptions, inputProps, inputSize, width, minWidth, popoverWidth, autoComplete, autoCorrect, autoCapitalize, spellCheck, popoverAnchorRef, fitContent = false, }) {
90
+ var _a, _b;
12
91
  const rootRef = useRef(null);
13
92
  const inputRef = useRef(null);
14
93
  const listboxRef = useRef(null);
@@ -43,23 +122,67 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
43
122
  return option.label;
44
123
  }
45
124
  }, [multiSelectedValueChipContent]);
46
- const multiSelectionAdornment = mode === 'multi' && selectedOptions.length > 0 ? (multiValueDisplayMode === 'count' ? (_jsxs("span", { className: "dbc-muted-text dbc-sm-text", style: {
47
- whiteSpace: 'nowrap',
48
- flexShrink: 0,
49
- marginRight: 'var(--spacing-xxs)',
50
- }, children: ["(", selectedOptions.length, ")"] })) : ((() => {
125
+ const multiSelectionAdornment = mode === 'multi' && selectedOptions.length > 0 ? (multiValueDisplayMode === 'count' ? (_jsxs("span", { className: `dbc-muted-text dbc-sm-text ${styles.countAdornment}`, children: ["(", selectedOptions.length, ")"] })) : ((() => {
51
126
  const MAX_CHIPS = 2;
52
127
  const chipsToShow = selectedOptions.slice(0, MAX_CHIPS);
53
128
  const extraCount = selectedOptions.length - MAX_CHIPS;
54
- return (_jsxs("div", { style: {
55
- display: 'flex',
56
- alignItems: 'center',
57
- gap: 4,
58
- flexWrap: 'nowrap',
59
- overflow: 'hidden',
60
- }, children: [chipsToShow.map(option => (_jsx(Chip, { size: "sm", type: "rounded", onClose: () => commitSelection(option), children: option.label }, option.value))), extraCount > 0 && (_jsxs("span", { className: "dbc-muted-text dbc-sm-text dbc-px-xxs", children: ["+", extraCount] }))] }));
129
+ return (_jsxs("div", { className: styles.chipRow, children: [chipsToShow.map(option => (_jsx(Chip, { size: "sm", type: "rounded", onClose: () => commitSelection(option), children: option.label }, option.value))), extraCount > 0 && (_jsxs("span", { className: "dbc-muted-text dbc-sm-text dbc-px-xxs", children: ["+", extraCount] }))] }));
61
130
  })())) : undefined;
62
131
  const usesCountAdornment = mode === 'multi' && multiValueDisplayMode === 'count' && selectedOptions.length > 0;
132
+ const resolvedInputSize = (_b = inputSize !== null && inputSize !== void 0 ? inputSize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputSize) !== null && _b !== void 0 ? _b : 'md';
133
+ const shouldFitContent = fitContent && mode === 'single' && !fullWidth;
134
+ const [fittedWidthPx, setFittedWidthPx] = useState(null);
135
+ const visibleSingleValueText = useMemo(() => {
136
+ if (!shouldFitContent)
137
+ return '';
138
+ return inputValue || (selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) || placeholder || '';
139
+ }, [shouldFitContent, inputValue, selectedOption, placeholder]);
140
+ useLayoutEffect(() => {
141
+ if (!shouldFitContent)
142
+ return;
143
+ const input = inputRef.current;
144
+ if (!input)
145
+ return;
146
+ let cancelled = false;
147
+ let frameId = requestAnimationFrame(() => {
148
+ const nextWidth = measureFitContentWidth(input, visibleSingleValueText, minWidth);
149
+ if (!cancelled && nextWidth != null) {
150
+ setFittedWidthPx(current => (current !== nextWidth ? nextWidth : current));
151
+ }
152
+ });
153
+ const fonts = document.fonts;
154
+ if (fonts === null || fonts === void 0 ? void 0 : fonts.ready) {
155
+ void fonts.ready.then(() => {
156
+ if (cancelled)
157
+ return;
158
+ const liveInput = inputRef.current;
159
+ if (!liveInput)
160
+ return;
161
+ const nextWidth = measureFitContentWidth(liveInput, visibleSingleValueText, minWidth);
162
+ if (nextWidth != null) {
163
+ setFittedWidthPx(current => (current !== nextWidth ? nextWidth : current));
164
+ }
165
+ });
166
+ }
167
+ return () => {
168
+ cancelled = true;
169
+ cancelAnimationFrame(frameId);
170
+ };
171
+ }, [
172
+ shouldFitContent,
173
+ visibleSingleValueText,
174
+ minWidth,
175
+ inputPropsStartAdornment,
176
+ inputPropsEndAdornment,
177
+ ]);
178
+ const measuredRootWidth = useMemo(() => {
179
+ if (!shouldFitContent)
180
+ return undefined;
181
+ if (fittedWidthPx == null)
182
+ return minWidth !== null && minWidth !== void 0 ? minWidth : '120px';
183
+ return `${fittedWidthPx}px`;
184
+ }, [shouldFitContent, fittedWidthPx, minWidth]);
185
+ const shouldStretchInput = fullWidth || shouldFitContent;
63
186
  useEffect(() => {
64
187
  var _a;
65
188
  if (mode === 'multi') {
@@ -269,6 +392,65 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
269
392
  }
270
393
  openWithCurrentFilter();
271
394
  }, [mode, selectedOption, prepareSingleSearchInput, openWithAllOptions, openWithCurrentFilter]);
395
+ const handleTriggerMouseDown = React.useCallback((e) => {
396
+ var _a;
397
+ inputPropsOnMouseDown === null || inputPropsOnMouseDown === void 0 ? void 0 : inputPropsOnMouseDown(e);
398
+ if (e.defaultPrevented)
399
+ return;
400
+ const isAlreadyFocused = document.activeElement === inputRef.current;
401
+ if (isAlreadyFocused && open) {
402
+ e.preventDefault();
403
+ setOpen(false);
404
+ setActiveIndex(-1);
405
+ if (mode === 'single') {
406
+ setQuery('');
407
+ setInputValue((_a = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) !== null && _a !== void 0 ? _a : '');
408
+ }
409
+ else {
410
+ setQuery('');
411
+ setInputValue('');
412
+ }
413
+ return;
414
+ }
415
+ if (isAlreadyFocused && mode === 'single' && selectedOption) {
416
+ prepareSingleSearchInput();
417
+ setOpen(true);
418
+ setActiveIndex(getSelectedIndex(options));
419
+ }
420
+ else if (isAlreadyFocused && !open) {
421
+ setOpen(true);
422
+ setActiveIndex(getSelectedIndex(filteredOptions));
423
+ }
424
+ }, [
425
+ inputPropsOnMouseDown,
426
+ open,
427
+ mode,
428
+ selectedOption,
429
+ prepareSingleSearchInput,
430
+ getSelectedIndex,
431
+ options,
432
+ filteredOptions,
433
+ ]);
434
+ const handleChevronMouseDown = React.useCallback((e) => {
435
+ var _a, _b;
436
+ e.preventDefault();
437
+ e.stopPropagation();
438
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
439
+ if (open) {
440
+ setOpen(false);
441
+ setActiveIndex(-1);
442
+ if (mode === 'single') {
443
+ setQuery('');
444
+ setInputValue((_b = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) !== null && _b !== void 0 ? _b : '');
445
+ }
446
+ else {
447
+ setQuery('');
448
+ setInputValue('');
449
+ }
450
+ return;
451
+ }
452
+ handleOpen();
453
+ }, [open, mode, selectedOption, handleOpen]);
272
454
  const handleKeyDown = (e) => {
273
455
  switch (e.key) {
274
456
  case 'ArrowDown':
@@ -342,8 +524,11 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
342
524
  selectedOptions.length > 0
343
525
  ? 8
344
526
  : 0,
345
- width: fullWidth ? '100%' : undefined,
346
- }, children: [_jsx(Popover, { open: open, minWidth: minWidth, anchorRef: popoverAnchorRef, onOpenChange: nextOpen => {
527
+ width: shouldFitContent ? measuredRootWidth : fullWidth ? '100%' : undefined,
528
+ flex: fullWidth || shouldFitContent ? '1 1 auto' : undefined,
529
+ minWidth: 0,
530
+ maxWidth: shouldFitContent ? '100%' : undefined,
531
+ }, children: [_jsx(Popover, { open: open, minWidth: popoverWidth !== null && popoverWidth !== void 0 ? popoverWidth : minWidth, matchTriggerWidth: true, fillTrigger: true, anchorRef: popoverAnchorRef, onOpenChange: nextOpen => {
347
532
  setOpen(nextOpen);
348
533
  if (nextOpen) {
349
534
  if (mode === 'single' && selectedOption) {
@@ -358,43 +543,14 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
358
543
  setActiveIndex(-1);
359
544
  }
360
545
  }, fullWidth: fullWidth, autoFocusContent: false, returnFocus: false, overlayRef: popoverContentRef, trigger: (openPopover, icon) => {
361
- var _a, _b, _c, _d, _e;
362
- return (_jsx(Input, { ...passthroughInputProps, ref: inputRef, value: inputValue, startAdornment: multiSelectionAdornment || inputPropsStartAdornment ? (_jsxs(_Fragment, { children: [multiSelectionAdornment, inputPropsStartAdornment] })) : undefined, endAdornment: inputPropsEndAdornment || icon ? (_jsxs(_Fragment, { children: [inputPropsEndAdornment, icon] })) : undefined, onFocus: e => {
546
+ var _a, _b, _c, _d;
547
+ return (_jsx(Input, { ...passthroughInputProps, ref: inputRef, value: inputValue, startAdornment: multiSelectionAdornment || inputPropsStartAdornment ? (_jsxs(_Fragment, { children: [multiSelectionAdornment, inputPropsStartAdornment] })) : undefined, endAdornment: inputPropsEndAdornment || icon ? (_jsxs(_Fragment, { children: [inputPropsEndAdornment, icon ? (_jsx("span", { className: styles.chevronButton, onMouseDown: handleChevronMouseDown, children: icon })) : null] })) : undefined, onFocus: e => {
363
548
  inputPropsOnFocus === null || inputPropsOnFocus === void 0 ? void 0 : inputPropsOnFocus(e);
364
549
  if (e.defaultPrevented)
365
550
  return;
366
551
  handleOpen();
367
552
  openPopover(e);
368
- }, onMouseDown: e => {
369
- var _a;
370
- inputPropsOnMouseDown === null || inputPropsOnMouseDown === void 0 ? void 0 : inputPropsOnMouseDown(e);
371
- if (e.defaultPrevented)
372
- return;
373
- const isAlreadyFocused = document.activeElement === inputRef.current;
374
- if (isAlreadyFocused && open) {
375
- e.preventDefault();
376
- setOpen(false);
377
- setActiveIndex(-1);
378
- if (mode === 'single') {
379
- setQuery('');
380
- setInputValue((_a = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label) !== null && _a !== void 0 ? _a : '');
381
- }
382
- else {
383
- setQuery('');
384
- setInputValue('');
385
- }
386
- return;
387
- }
388
- if (isAlreadyFocused && mode === 'single' && selectedOption) {
389
- prepareSingleSearchInput();
390
- setOpen(true);
391
- setActiveIndex(getSelectedIndex(options));
392
- }
393
- else if (isAlreadyFocused && !open) {
394
- setOpen(true);
395
- setActiveIndex(getSelectedIndex(filteredOptions));
396
- }
397
- }, onChange: e => handleInputChange(e.currentTarget.value), onBlur: e => {
553
+ }, onMouseDown: handleTriggerMouseDown, onChange: e => handleInputChange(e.currentTarget.value), onBlur: e => {
398
554
  inputPropsOnBlur === null || inputPropsOnBlur === void 0 ? void 0 : inputPropsOnBlur(e);
399
555
  if (e.defaultPrevented)
400
556
  return;
@@ -407,7 +563,7 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
407
563
  if (e.defaultPrevented)
408
564
  return;
409
565
  handleKeyDown(e);
410
- }, placeholder: placeholder, variant: variant, inputSize: (_a = inputSize !== null && inputSize !== void 0 ? inputSize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputSize) !== null && _a !== void 0 ? _a : 'md', width: width !== null && width !== void 0 ? width : inputProps === null || inputProps === void 0 ? void 0 : inputProps.width, autoComplete: (_b = autoComplete !== null && autoComplete !== void 0 ? autoComplete : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoComplete) !== null && _b !== void 0 ? _b : 'off', autoCorrect: (_c = autoCorrect !== null && autoCorrect !== void 0 ? autoCorrect : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCorrect) !== null && _c !== void 0 ? _c : 'off', autoCapitalize: (_d = autoCapitalize !== null && autoCapitalize !== void 0 ? autoCapitalize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCapitalize) !== null && _d !== void 0 ? _d : 'none', spellCheck: (_e = spellCheck !== null && spellCheck !== void 0 ? spellCheck : inputProps === null || inputProps === void 0 ? void 0 : inputProps.spellCheck) !== null && _e !== void 0 ? _e : false, disabled: disabled, fullWidth: fullWidth, inputClassName: [
566
+ }, placeholder: placeholder, variant: variant, inputSize: resolvedInputSize, width: shouldFitContent ? undefined : (width !== null && width !== void 0 ? width : inputProps === null || inputProps === void 0 ? void 0 : inputProps.width), autoComplete: (_a = autoComplete !== null && autoComplete !== void 0 ? autoComplete : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoComplete) !== null && _a !== void 0 ? _a : 'off', autoCorrect: (_b = autoCorrect !== null && autoCorrect !== void 0 ? autoCorrect : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCorrect) !== null && _b !== void 0 ? _b : 'off', autoCapitalize: (_c = autoCapitalize !== null && autoCapitalize !== void 0 ? autoCapitalize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCapitalize) !== null && _c !== void 0 ? _c : 'none', spellCheck: (_d = spellCheck !== null && spellCheck !== void 0 ? spellCheck : inputProps === null || inputProps === void 0 ? void 0 : inputProps.spellCheck) !== null && _d !== void 0 ? _d : false, disabled: disabled, fullWidth: shouldStretchInput, inputClassName: [
411
567
  inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputClassName,
412
568
  usesCountAdornment ? styles.inputWithoutStartPadding : '',
413
569
  ]
@@ -447,10 +603,5 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
447
603
  }, onKeyDown: e => handleTabKeyDown(e), onClick: () => commitSelection(option), children: _jsx("span", { children: option.label }) }) }, option.value));
448
604
  })) : (_jsx(Menu.Item, { disabled: true, children: emptyMessage })) }) }), mode === 'multi' &&
449
605
  multiSelectedValuesDisplayMode === 'below-input' &&
450
- selectedOptions.length > 0 && (_jsx("div", { style: {
451
- display: 'flex',
452
- flexWrap: 'wrap',
453
- gap: 8,
454
- alignItems: 'flex-start',
455
- }, children: selectedOptions.map(option => (_jsx(Chip, { size: "sm", type: "default", severity: "neutral", onClose: () => commitSelection(option), children: getSelectedValueChipLabel(option) }, option.value))) }))] }));
606
+ selectedOptions.length > 0 && (_jsx("div", { className: styles.selectedValues, children: selectedOptions.map(option => (_jsx(Chip, { size: "sm", type: "default", severity: "neutral", onClose: () => commitSelection(option), children: getSelectedValueChipLabel(option) }, option.value))) }))] }));
456
607
  }
@@ -1,3 +1,34 @@
1
+ .countAdornment {
2
+ white-space: nowrap;
3
+ flex-shrink: 0;
4
+ margin-right: var(--spacing-xxs);
5
+ }
6
+
7
+ .chipRow {
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 4px;
11
+ flex-wrap: nowrap;
12
+ overflow: hidden;
13
+ min-width: 0;
14
+ }
15
+
16
+ .chevronButton {
17
+ display: inline-flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ height: 100%;
21
+ flex-shrink: 0;
22
+ cursor: pointer;
23
+ }
24
+
25
+ .selectedValues {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ gap: 8px;
29
+ align-items: flex-start;
30
+ }
31
+
1
32
  .inputWithoutStartPadding {
2
33
  padding-inline-start: 0;
3
34
  }
@@ -92,6 +92,8 @@ id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval',
92
92
  const isActive = index === activeIndex;
93
93
  return (_jsx(Menu.Item, { active: isActive, selected: isSelected,
94
94
  // IMPORTANT: listbox uses role="option"
95
- itemRole: "option", children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, "aria-selected": isSelected, onClick: () => handleCommit(opt), onFocus: () => setActiveIndex(index), style: { display: 'flex', alignItems: 'center', width: '100%' }, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: isSelected ? _jsx(Check, {}) : null }), opt.label] }) }, opt.minutesAgo));
95
+ itemRole: "option", children: _jsxs("button", { ref: el => {
96
+ optionRefs.current[index] = el;
97
+ }, type: "button", tabIndex: isActive ? 0 : -1, "aria-selected": isSelected, onClick: () => handleCommit(opt), onFocus: () => setActiveIndex(index), style: { display: 'flex', alignItems: 'center', width: '100%' }, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: isSelected ? _jsx(Check, {}) : null }), opt.label] }) }, opt.minutesAgo));
96
98
  }) }) }), (error || helpText) && (_jsx("span", { id: describedById, style: { display: 'none' }, children: error !== null && error !== void 0 ? error : helpText }))] }));
97
99
  }
@@ -12,7 +12,7 @@
12
12
  display: none;
13
13
  }
14
14
 
15
- @media (min-width: var(--bp-xs)) {
15
+ @media (min-width: 480px) {
16
16
  .ellipsisButton {
17
17
  display: contents;
18
18
  }
@@ -6,20 +6,13 @@ export interface PopoverProps {
6
6
  defaultOpen?: boolean;
7
7
  onOpenChange?: (open: boolean) => void;
8
8
  contentId?: string;
9
- /**
10
- * CSS length, recommended "NNpx" for predictability.
11
- * Used as a minimum, not as a forced width unless matchTriggerWidth=true.
12
- */
13
9
  minWidth?: string;
14
- /**
15
- * If true, force the overlay width to at least the trigger width.
16
- * If false, overlay width is content-driven (calendar-friendly).
17
- */
18
10
  matchTriggerWidth?: boolean;
19
11
  viewportPadding?: number;
20
12
  edgeBuffer?: number;
21
13
  dataCy?: string;
22
14
  fullWidth?: boolean;
15
+ fillTrigger?: boolean;
23
16
  autoFocusContent?: boolean;
24
17
  returnFocus?: boolean;
25
18
  anchorRef?: React.RefObject<HTMLElement | null>;
@@ -38,7 +38,7 @@ function parseMinWidthPx(minWidth, elForEm) {
38
38
  }
39
39
  return 0;
40
40
  }
41
- export const Popover = forwardRef(function Popover({ trigger: Trigger, children, open, defaultOpen = false, onOpenChange, contentId, minWidth = '200px', matchTriggerWidth = true, viewportPadding = 8, edgeBuffer = 100, dataCy, fullWidth = false, autoFocusContent = false, returnFocus = true, anchorRef, overlayRef, }, ref) {
41
+ export const Popover = forwardRef(function Popover({ trigger: Trigger, children, open, defaultOpen = false, onOpenChange, contentId, minWidth = '200px', matchTriggerWidth = true, viewportPadding = 8, edgeBuffer = 100, dataCy, fullWidth = false, fillTrigger = false, autoFocusContent = false, returnFocus = true, anchorRef, overlayRef, }, ref) {
42
42
  const internalId = useId();
43
43
  const resolvedContentId = contentId !== null && contentId !== void 0 ? contentId : `popover-${internalId}`;
44
44
  const isControlled = open !== undefined;
@@ -74,7 +74,7 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
74
74
  closePopover('trigger');
75
75
  else
76
76
  openPopover();
77
- }, [isOpen, closePopover, openPopover]);
77
+ }, [isOpen, closePopover, openPopover, anchorRef]);
78
78
  useImperativeHandle(ref, () => ({
79
79
  close: () => closePopover('api'),
80
80
  open: openPopover,
@@ -90,19 +90,15 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
90
90
  return;
91
91
  const triggerRect = triggerEl.getBoundingClientRect();
92
92
  const overlayWidthBuffer = 8;
93
- // Only compute a forced width when requested.
94
93
  let forcedWidthPx = null;
95
94
  if (matchTriggerWidth) {
96
95
  const minWidthPx = parseMinWidthPx(minWidth, triggerEl);
97
- // Make the overlay slightly wider than the trigger so it reads as a
98
- // floating layer instead of blending into adjacent form fields.
99
96
  forcedWidthPx = Math.max(triggerRect.width + overlayWidthBuffer, minWidthPx || 0);
100
97
  setTriggerWidth(forcedWidthPx);
101
98
  }
102
99
  else {
103
100
  setTriggerWidth(null);
104
101
  }
105
- // Measure height/width for collision using a temporary sizing that reflects our final sizing:
106
102
  const prevHidden = content.hidden;
107
103
  const prevVis = content.style.visibility;
108
104
  const prevDisp = content.style.display;
@@ -115,12 +111,10 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
115
111
  content.style.display = 'block';
116
112
  content.style.top = '0px';
117
113
  content.style.left = '0px';
118
- // Apply minWidth always; apply width only if matchTriggerWidth.
119
114
  content.style.minWidth = minWidth;
120
115
  content.style.width = forcedWidthPx != null ? `${forcedWidthPx}px` : 'auto';
121
116
  const contentWidth = content.offsetWidth;
122
117
  const contentHeight = content.offsetHeight;
123
- // Restore
124
118
  content.hidden = prevHidden;
125
119
  content.style.visibility = prevVis;
126
120
  content.style.display = prevDisp;
@@ -152,13 +146,12 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
152
146
  const clampedTop = Math.max(viewportPadding, Math.min(rawTop, vh - contentHeight - viewportPadding));
153
147
  setPos({ top: clampedTop, left: clampedLeft });
154
148
  setPositioned(true);
155
- }, [edgeBuffer, viewportPadding, minWidth, matchTriggerWidth]);
149
+ }, [anchorRef, edgeBuffer, viewportPadding, minWidth, matchTriggerWidth]);
156
150
  useLayoutEffect(() => {
157
151
  if (!isOpen)
158
152
  return;
159
153
  computeAndSetPosition();
160
- // eslint-disable-next-line react-hooks/exhaustive-deps
161
- }, [isOpen]);
154
+ }, [isOpen, computeAndSetPosition]);
162
155
  useEffect(() => {
163
156
  var _a, _b, _c;
164
157
  if (!isOpen)
@@ -227,7 +220,19 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
227
220
  const mutableRef = overlayRef;
228
221
  mutableRef.current = node;
229
222
  }, [overlayRef]);
230
- return (_jsxs("div", { className: [styles.container, fullWidth ? styles.fullWidth : ''].filter(Boolean).join(' '), ref: containerRef, children: [Trigger(togglePopover, icon, isOpen), mounted &&
223
+ return (_jsxs("div", { className: [
224
+ styles.container,
225
+ fullWidth ? styles.fullWidth : '',
226
+ fillTrigger ? styles.fillTrigger : '',
227
+ ]
228
+ .filter(Boolean)
229
+ .join(' '), ref: containerRef, children: [_jsx("div", { className: [
230
+ styles.triggerSlot,
231
+ fullWidth ? styles.fullWidth : '',
232
+ fillTrigger ? styles.fillTrigger : '',
233
+ ]
234
+ .filter(Boolean)
235
+ .join(' '), children: Trigger(togglePopover, icon, isOpen) }), mounted &&
231
236
  isOpen &&
232
237
  createPortal(_jsx("div", { id: resolvedContentId, ref: node => {
233
238
  const mutableContentRef = contentRef;
@@ -236,7 +241,6 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
236
241
  }, className: styles.content, style: {
237
242
  top: pos.top,
238
243
  left: pos.left,
239
- // Content-driven sizing by default.
240
244
  minWidth,
241
245
  width: triggerWidth != null ? `${triggerWidth}px` : undefined,
242
246
  maxWidth: `calc(100vw - ${viewportPadding * 2}px)`,
@@ -1,14 +1,39 @@
1
1
  .container {
2
2
  position: relative;
3
- display: inline-flex;
3
+ display: flex;
4
4
  align-items: stretch;
5
5
  height: 100%;
6
6
  min-width: 0;
7
+ width: auto;
8
+ flex: 0 0 auto;
9
+ }
10
+
11
+ .container > * {
12
+ min-width: 0;
7
13
  }
8
14
 
9
- .fullWidth {
15
+ .triggerSlot {
10
16
  display: flex;
17
+ align-items: stretch;
18
+ min-width: 0;
19
+ width: auto;
20
+ flex: 0 0 auto;
21
+ }
22
+
23
+ .triggerSlot > * {
24
+ min-width: 0;
25
+ }
26
+
27
+ .fullWidth,
28
+ .fillTrigger {
11
29
  width: 100%;
30
+ flex: 1 1 auto;
31
+ }
32
+
33
+ .fullWidth > *,
34
+ .fillTrigger > * {
35
+ width: 100%;
36
+ min-width: 0;
12
37
  }
13
38
 
14
39
  .content {
@@ -6,7 +6,7 @@ export interface SplitPaneContextValue {
6
6
  setPrimarySize: (newSize: number) => void;
7
7
  minPrimarySize: number;
8
8
  minSecondarySize: number;
9
- containerRef: React.RefObject<HTMLDivElement>;
9
+ containerRef: React.RefObject<HTMLDivElement | null>;
10
10
  storageKey?: string;
11
11
  resetDefault: () => void;
12
12
  }
@@ -20,12 +20,12 @@ export function buildColumnVisibilityFromVisibleIds(defs, visibleColumnIds) {
20
20
  }
21
21
  export function mapDefsToColumnItems(defs, columnVisibility, resolvedLayout = {}) {
22
22
  return defs.map((def, index) => {
23
- var _a, _b, _c, _d, _e, _f, _g, _h;
23
+ var _a, _b, _c, _d, _e, _f, _g;
24
24
  const id = getColumnId(def, index);
25
25
  const accessorKey = def.accessorKey;
26
26
  const accessorFn = def.accessorFn;
27
27
  const cell = def.cell;
28
- const meta = (_b = ((_a = def.meta) !== null && _a !== void 0 ? _a : {})) !== null && _b !== void 0 ? _b : {};
28
+ const meta = ((_a = def.meta) !== null && _a !== void 0 ? _a : {});
29
29
  let render;
30
30
  if (typeof cell === 'function') {
31
31
  render = (row) => cell({
@@ -46,22 +46,22 @@ export function mapDefsToColumnItems(defs, columnVisibility, resolvedLayout = {}
46
46
  else {
47
47
  render = () => null;
48
48
  }
49
- const isVisible = (_c = columnVisibility[id]) !== null && _c !== void 0 ? _c : true;
49
+ const isVisible = (_b = columnVisibility[id]) !== null && _b !== void 0 ? _b : true;
50
50
  return {
51
51
  id,
52
52
  header: def.header,
53
53
  accessor: accessorKey,
54
- sortable: (_d = def.enableSorting) !== null && _d !== void 0 ? _d : !!accessorKey,
54
+ sortable: (_c = def.enableSorting) !== null && _c !== void 0 ? _c : !!accessorKey,
55
55
  render,
56
56
  hidden: !isVisible,
57
- width: (_e = resolvedLayout[id]) === null || _e === void 0 ? void 0 : _e.width,
57
+ width: (_d = resolvedLayout[id]) === null || _d === void 0 ? void 0 : _d.width,
58
58
  align: meta.align,
59
59
  verticalAlign: meta.verticalAlign,
60
- emptyPlaceholder: (_f = meta.emptyPlaceholder) !== null && _f !== void 0 ? _f : '-',
61
- allowWrap: (_g = meta.allowWrap) !== null && _g !== void 0 ? _g : false,
60
+ emptyPlaceholder: (_e = meta.emptyPlaceholder) !== null && _e !== void 0 ? _e : '-',
61
+ allowWrap: (_f = meta.allowWrap) !== null && _f !== void 0 ? _f : false,
62
62
  severity: meta.severity,
63
63
  divider: meta.divider,
64
- allowOverflow: (_h = meta.allowOverflow) !== null && _h !== void 0 ? _h : false,
64
+ allowOverflow: (_g = meta.allowOverflow) !== null && _g !== void 0 ? _g : false,
65
65
  };
66
66
  });
67
67
  }
@@ -111,10 +111,10 @@ export function buildDistributedColumnWidths(args) {
111
111
  const leaf = table.getVisibleLeafColumns();
112
112
  const selectionWidth = hasSelection ? SELECTION_COLUMN_PX : 0;
113
113
  const tracks = leaf.map((c) => {
114
- var _a, _b, _c, _d, _e;
114
+ var _a, _b, _c, _d;
115
115
  const def = c.columnDef;
116
- const meta = ((_b = ((_a = def.meta) !== null && _a !== void 0 ? _a : {})) !== null && _b !== void 0 ? _b : {});
117
- const min = Math.max(1, Number((_c = def.minSize) !== null && _c !== void 0 ? _c : defaultMinPx));
116
+ const meta = ((_a = def.meta) !== null && _a !== void 0 ? _a : {});
117
+ const min = Math.max(1, Number((_b = def.minSize) !== null && _b !== void 0 ? _b : defaultMinPx));
118
118
  const max = def.maxSize != null ? Math.max(min, Number(def.maxSize)) : undefined;
119
119
  const resizedPxRaw = columnSizing[c.id];
120
120
  const resizedPx = resizedPxRaw != null ? Math.round(clamp(Number(resizedPxRaw), min, max)) : undefined;
@@ -127,7 +127,7 @@ export function buildDistributedColumnWidths(args) {
127
127
  fixed: true,
128
128
  };
129
129
  }
130
- const rawWeight = Number((_e = (_d = meta.weight) !== null && _d !== void 0 ? _d : def.size) !== null && _e !== void 0 ? _e : DEFAULT_COLUMN_PX);
130
+ const rawWeight = Number((_d = (_c = meta.weight) !== null && _c !== void 0 ? _c : def.size) !== null && _d !== void 0 ? _d : DEFAULT_COLUMN_PX);
131
131
  const weight = Number.isFinite(rawWeight) && rawWeight > 0 ? rawWeight : DEFAULT_COLUMN_PX;
132
132
  return {
133
133
  id: c.id,
@@ -10,7 +10,7 @@
10
10
  word-break: break-word;
11
11
  }
12
12
 
13
- @media (max-width: var(--bp-sm)) {
13
+ @media (max-width: 640px) {
14
14
  .container {
15
15
  inset-inline: var(--spacing-md);
16
16
  inset-block-end: var(--spacing-md);
@@ -14,7 +14,14 @@ function defaultDuration(ms) {
14
14
  }
15
15
  return `${sec}s`;
16
16
  }
17
- export function useTimeDuration({ start, end, dateFormat = { dateStyle: 'short', timeStyle: 'medium' }, fallback = '—', liveUpdate = false, formatDuration = defaultDuration, }) {
17
+ export function useTimeDuration({ start, end, dateFormat = {
18
+ year: '2-digit',
19
+ month: '2-digit',
20
+ day: '2-digit',
21
+ hour: '2-digit',
22
+ minute: '2-digit',
23
+ second: '2-digit',
24
+ }, fallback = '—', liveUpdate = false, formatDuration = defaultDuration, }) {
18
25
  const [hydrated, setHydrated] = useState(false);
19
26
  const [tick, setTick] = useState(0);
20
27
  useEffect(() => setHydrated(true), []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@tanstack/react-table": "^8.21.3",
64
- "lucide-react": ">=0.543.0 <1",
64
+ "lucide-react": ">=0.543.0 <2",
65
65
  "react": "^19.0.0",
66
66
  "react-dom": "^19.0.0"
67
67
  },
@@ -73,47 +73,45 @@
73
73
  "devDependencies": {
74
74
  "@changesets/cli": "^2.29.6",
75
75
  "@mdx-js/react": "^3.1.1",
76
- "@storybook/addon-a11y": "^9.1.3",
77
- "@storybook/addon-docs": "^9.1.17",
78
- "@storybook/addon-viewport": "^9.0.8",
79
- "@storybook/react-vite": "^9.1.3",
76
+ "@storybook/addon-a11y": "^10.3.5",
77
+ "@storybook/addon-docs": "^10.3.5",
78
+ "@storybook/react-vite": "^10.3.5",
80
79
  "@swc/core": "^1.15.11",
81
80
  "@swc/jest": "^0.2.39",
82
81
  "@tanstack/react-table": "^8.20.0",
83
82
  "@testing-library/jest-dom": "^6.9.1",
84
83
  "@testing-library/react": "^16.3.2",
85
84
  "@types/jest": "^30.0.0",
86
- "@types/react": "^18.2.48",
87
- "@types/react-dom": "^18.2.18",
85
+ "@types/react": "^19.2.14",
86
+ "@types/react-dom": "^19.2.3",
88
87
  "@typescript-eslint/eslint-plugin": "^8.41.0",
89
88
  "@typescript-eslint/parser": "^8.41.0",
90
- "@vitejs/plugin-react": "^5.1.3",
91
- "cpy-cli": "^6.0.0",
89
+ "@vitejs/plugin-react": "^6.0.1",
90
+ "cpy-cli": "^7.0.0",
92
91
  "esbuild-css-modules-plugin": "^3.1.5",
93
- "esbuild-plugin-css-modules": "^0.3.0",
94
- "eslint": "^9.34.0",
92
+ "eslint": "^9.39.4",
95
93
  "eslint-config-prettier": "^10.1.8",
96
94
  "eslint-import-resolver-typescript": "^4.4.4",
97
95
  "eslint-plugin-import": "^2.32.0",
98
96
  "eslint-plugin-prettier": "^5.5.5",
99
97
  "eslint-plugin-react": "^7.37.5",
100
- "eslint-plugin-react-hooks": "^5.2.0",
98
+ "eslint-plugin-react-hooks": "^7.0.1",
101
99
  "globals": "^17.3.0",
102
100
  "identity-obj-proxy": "^3.0.0",
103
101
  "jest": "^30.2.0",
104
102
  "jest-environment-jsdom": "^30.2.0",
105
- "jsdom": "^26.1.0",
103
+ "jsdom": "^29.0.2",
106
104
  "prettier": "^3.2.4",
107
- "react": "18.2.0",
108
- "react-dom": "18.2.0",
105
+ "react": "19.2.4",
106
+ "react-dom": "19.2.4",
109
107
  "rimraf": "^6.1.2",
110
- "storybook": "^9.1.3",
108
+ "storybook": "^10.3.5",
111
109
  "tsc-alias": "^1.8.16",
112
110
  "tsup": "^8.5.0",
113
- "typescript": "^5.9.3",
114
- "vite": "^6.3.5",
111
+ "typescript": "^6.0.2",
112
+ "vite": "^8.0.7",
115
113
  "vite-tsconfig-paths": "^6.0.5",
116
- "vitest": "^3.2.4"
114
+ "vitest": "^4.1.3"
117
115
  },
118
116
  "engines": {
119
117
  "node": ">=18.17"