@dbcdk/react-components 0.0.56 → 0.0.58

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.
@@ -28,6 +28,8 @@ 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
33
  const rootStyle = {
32
34
  ...(style !== null && style !== void 0 ? style : {}),
33
35
  ...(minWidth ? { ['--input-min-width']: minWidth } : null),
@@ -43,7 +45,8 @@ export const Input = forwardRef(function Input({ label, error, helpText, orienta
43
45
  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: [
44
46
  styles.container,
45
47
  fullWidth ? styles.fullWidth : '',
46
- onClear ? styles.withClear : '',
48
+ hasVisibleClear ? styles.withClear : '',
49
+ hasInlineClear ? styles.withInlineClear : '',
47
50
  hasButton ? styles.withButton : '',
48
51
  className !== null && className !== void 0 ? className : '',
49
52
  ]
@@ -59,6 +62,6 @@ export const Input = forwardRef(function Input({ label, error, helpText, orienta
59
62
  .filter(Boolean)
60
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 : '']
61
64
  .filter(Boolean)
62
- .join(' ') }), endAdornment && _jsx("span", { className: styles.endAdornment, children: endAdornment }), onClear && inputProps.value && _jsx(ClearButton, { onClick: onClear, absolute: true })] }), 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] }))] }) }));
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] }))] }) }));
63
66
  });
64
67
  Input.displayName = 'Input';
@@ -77,6 +77,10 @@
77
77
  padding-inline-end: calc(var(--spacing-xxs) + 16px + var(--spacing-xxs));
78
78
  }
79
79
 
80
+ .withInlineClear .input {
81
+ padding-inline-end: var(--spacing-xs);
82
+ }
83
+
80
84
  /* Global focus reset - variants own visible focus treatment */
81
85
  .input:focus-visible {
82
86
  outline: none;
@@ -395,6 +399,6 @@
395
399
  .endAdornment {
396
400
  display: flex;
397
401
  align-items: center;
398
- gap: 4px;
399
- margin-right: 4px;
402
+ margin-right: var(--spacing-xxs);
403
+ color: var(--color-fg-subtle);
400
404
  }
@@ -101,7 +101,7 @@ export function MultiSelect({ options, selectedValues = [], onChange, placeholde
101
101
  else {
102
102
  setSearchQuery('');
103
103
  }
104
- }, dataCy: dataCy, fullWidth: fullWidth, autoFocusContent: false, returnFocus: true, trigger: (onClick, icon, isOpen) => (_jsx(Button, { variant: variant, onClick: onClick, onKeyDown: handleCombinedKeyDown, size: size, fullWidth: fullWidth, disabled: disabled, "aria-haspopup": "menu", "aria-expanded": !!isOpen, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [_jsx("span", { children: children !== null && children !== void 0 ? children : placeholder }), selectedValues.length > 0 ? _jsx(Chip, { size: "sm", children: selectedValues.length }) : null] }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selectedValues.length > 0 ? _jsx(ClearButton, { onClick: onClear }) : null, icon] })] }) })), children: _jsxs(Menu, { onKeyDown: handleCombinedKeyDown, children: [searchable ? (_jsx(Menu.Item, { children: _jsx(Input, { ref: searchInputRef, value: searchQuery, onChange: e => setSearchQuery(e.target.value), onKeyDown: handleCombinedKeyDown, placeholder: searchPlaceholder, icon: _jsx(Search, { size: 16 }), fullWidth: true }) })) : null, filteredOptions.map((option, index) => {
104
+ }, dataCy: dataCy, fullWidth: fullWidth, autoFocusContent: false, returnFocus: true, trigger: (onClick, icon, isOpen) => (_jsx(Button, { variant: variant, onClick: onClick, onKeyDown: handleCombinedKeyDown, size: size, fullWidth: fullWidth, disabled: disabled, "aria-haspopup": "menu", "aria-expanded": !!isOpen, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [_jsx("span", { children: children !== null && children !== void 0 ? children : placeholder }), selectedValues.length > 0 ? _jsx(Chip, { size: "sm", children: selectedValues.length }) : null] }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selectedValues.length > 0 ? _jsx(ClearButton, { onClick: onClear }) : null, _jsx("span", { style: { color: 'var(--color-fg-subtle)', display: 'inline-flex' }, children: icon })] })] }) })), children: _jsxs(Menu, { onKeyDown: handleCombinedKeyDown, children: [searchable ? (_jsx(Menu.Item, { children: _jsx(Input, { ref: searchInputRef, value: searchQuery, onChange: e => setSearchQuery(e.target.value), onKeyDown: handleCombinedKeyDown, placeholder: searchPlaceholder, icon: _jsx(Search, { size: 16 }), fullWidth: true }) })) : null, filteredOptions.map((option, index) => {
105
105
  const isSelected = selectedSet.has(option.value);
106
106
  const isActive = index === activeIndex;
107
107
  return (_jsx(Menu.Item, { active: isActive, children: _jsxs("button", { ref: el => {
@@ -168,7 +168,7 @@ export function Select({ label, error, helpText, orientation = 'vertical', label
168
168
  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 => {
169
169
  resetActiveToSelected();
170
170
  toggle(e);
171
- }, 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 : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] })] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
171
+ }, 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 : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selected && _jsx(ClearButton, { onClick: onClear }), _jsx("span", { style: { color: 'var(--color-fg-subtle)', display: 'inline-flex' }, children: icon })] })] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
172
172
  const isSelected = typeof opt.value === 'object' && typeof selectedValue === 'object' && datakey
173
173
  ? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
174
174
  : opt.value === selectedValue;
@@ -9,12 +9,14 @@ import { Popover } from '../../../components/popover/Popover';
9
9
  import styles from './Typeahead.module.css';
10
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
11
  var _a;
12
+ const rootRef = useRef(null);
12
13
  const inputRef = useRef(null);
13
14
  const listboxRef = useRef(null);
15
+ const popoverContentRef = useRef(null);
14
16
  const optionRefs = useRef([]);
15
17
  const interactingWithOptionsRef = useRef(false);
16
18
  const listboxId = useId();
17
- const { onFocus: inputPropsOnFocus, onBlur: inputPropsOnBlur, onKeyDown: inputPropsOnKeyDown, onMouseDown: inputPropsOnMouseDown, onClear: inputPropsOnClear, startAdornment: inputPropsStartAdornment, ...passthroughInputProps } = inputProps !== null && inputProps !== void 0 ? inputProps : {};
19
+ const { onFocus: inputPropsOnFocus, onBlur: inputPropsOnBlur, onKeyDown: inputPropsOnKeyDown, onMouseDown: inputPropsOnMouseDown, onClear: inputPropsOnClear, startAdornment: inputPropsStartAdornment, endAdornment: inputPropsEndAdornment, ...passthroughInputProps } = inputProps !== null && inputProps !== void 0 ? inputProps : {};
18
20
  const selectedOption = useMemo(() => {
19
21
  var _a;
20
22
  if (mode === 'multi')
@@ -105,6 +107,75 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
105
107
  const activeEl = optionRefs.current[activeIndex];
106
108
  (_a = activeEl === null || activeEl === void 0 ? void 0 : activeEl.scrollIntoView) === null || _a === void 0 ? void 0 : _a.call(activeEl, { block: 'nearest' });
107
109
  }, [open, activeIndex, filteredOptions]);
110
+ const getFocusableElements = React.useCallback(() => {
111
+ const selector = [
112
+ 'a[href]',
113
+ 'button:not([disabled])',
114
+ 'input:not([disabled])',
115
+ 'select:not([disabled])',
116
+ 'textarea:not([disabled])',
117
+ '[tabindex]:not([tabindex="-1"])',
118
+ ].join(',');
119
+ const seen = new Set();
120
+ const focusables = [];
121
+ for (const container of [rootRef.current, popoverContentRef.current]) {
122
+ if (!container)
123
+ continue;
124
+ const elements = container.matches(selector)
125
+ ? [container]
126
+ : Array.from(container.querySelectorAll(selector));
127
+ for (const element of elements) {
128
+ if (seen.has(element) ||
129
+ element.getAttribute('aria-hidden') === 'true' ||
130
+ element.tabIndex < 0) {
131
+ continue;
132
+ }
133
+ seen.add(element);
134
+ focusables.push(element);
135
+ }
136
+ }
137
+ return focusables;
138
+ }, []);
139
+ const isFocusWithinTypeahead = React.useCallback((target) => {
140
+ var _a, _b;
141
+ if (!(target instanceof Node))
142
+ return false;
143
+ return Boolean(((_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.contains(target)) || ((_b = popoverContentRef.current) === null || _b === void 0 ? void 0 : _b.contains(target)));
144
+ }, []);
145
+ const handleTabKeyDown = React.useCallback((event) => {
146
+ var _a, _b, _c;
147
+ if (event.key !== 'Tab' || !open)
148
+ return;
149
+ const focusables = getFocusableElements();
150
+ if (focusables.length === 0)
151
+ return;
152
+ const eventTarget = event.target instanceof HTMLElement
153
+ ? event.target
154
+ : document.activeElement;
155
+ const activeElement = eventTarget && focusables.includes(eventTarget)
156
+ ? eventTarget
157
+ : ((_a = document.activeElement) !== null && _a !== void 0 ? _a : null);
158
+ const activeIndexInScope = activeElement ? focusables.indexOf(activeElement) : -1;
159
+ const boundaryIndex = event.shiftKey ? 0 : focusables.length - 1;
160
+ const boundaryElement = focusables[boundaryIndex];
161
+ if (mode === 'multi') {
162
+ if (activeIndexInScope === -1) {
163
+ event.preventDefault();
164
+ (_b = focusables[0]) === null || _b === void 0 ? void 0 : _b.focus();
165
+ return;
166
+ }
167
+ event.preventDefault();
168
+ const nextIndex = event.shiftKey
169
+ ? (activeIndexInScope - 1 + focusables.length) % focusables.length
170
+ : (activeIndexInScope + 1) % focusables.length;
171
+ (_c = focusables[nextIndex]) === null || _c === void 0 ? void 0 : _c.focus();
172
+ return;
173
+ }
174
+ if (activeIndexInScope !== -1 && activeElement === boundaryElement) {
175
+ setOpen(false);
176
+ setActiveIndex(-1);
177
+ }
178
+ }, [open, getFocusableElements, mode]);
108
179
  const commitSelection = (option) => {
109
180
  var _a, _b;
110
181
  if (mode === 'multi') {
@@ -144,7 +215,9 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
144
215
  onChange(null);
145
216
  }
146
217
  };
147
- const handleBlur = () => {
218
+ const handleBlur = (nextFocusedTarget) => {
219
+ if (isFocusWithinTypeahead(nextFocusedTarget))
220
+ return;
148
221
  if (mode === 'multi') {
149
222
  if (interactingWithOptionsRef.current) {
150
223
  interactingWithOptionsRef.current = false;
@@ -261,7 +334,7 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
261
334
  return;
262
335
  }
263
336
  };
264
- return (_jsxs("div", { style: {
337
+ return (_jsxs("div", { ref: rootRef, style: {
265
338
  display: 'flex',
266
339
  flexDirection: 'column',
267
340
  gap: mode === 'multi' &&
@@ -284,9 +357,9 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
284
357
  else {
285
358
  setActiveIndex(-1);
286
359
  }
287
- }, fullWidth: fullWidth, autoFocusContent: false, returnFocus: false, trigger: openPopover => {
360
+ }, fullWidth: fullWidth, autoFocusContent: false, returnFocus: false, overlayRef: popoverContentRef, trigger: (openPopover, icon) => {
288
361
  var _a, _b, _c, _d, _e;
289
- return (_jsx(Input, { ...passthroughInputProps, ref: inputRef, value: inputValue, startAdornment: multiSelectionAdornment || inputPropsStartAdornment ? (_jsxs(_Fragment, { children: [multiSelectionAdornment, inputPropsStartAdornment] })) : undefined, onFocus: 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 => {
290
363
  inputPropsOnFocus === null || inputPropsOnFocus === void 0 ? void 0 : inputPropsOnFocus(e);
291
364
  if (e.defaultPrevented)
292
365
  return;
@@ -325,9 +398,12 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
325
398
  inputPropsOnBlur === null || inputPropsOnBlur === void 0 ? void 0 : inputPropsOnBlur(e);
326
399
  if (e.defaultPrevented)
327
400
  return;
328
- handleBlur();
401
+ handleBlur(e.relatedTarget);
329
402
  }, onKeyDown: e => {
330
403
  inputPropsOnKeyDown === null || inputPropsOnKeyDown === void 0 ? void 0 : inputPropsOnKeyDown(e);
404
+ if (e.defaultPrevented)
405
+ return;
406
+ handleTabKeyDown(e);
331
407
  if (e.defaultPrevented)
332
408
  return;
333
409
  handleKeyDown(e);
@@ -363,11 +439,12 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
363
439
  interactingWithOptionsRef.current = true;
364
440
  e.preventDefault();
365
441
  },
442
+ onKeyDown: e => handleTabKeyDown(e),
366
443
  }, label: _jsx("span", { children: option.label }), onCheckedChange: () => commitSelection(option) }, option.value)) : (_jsx(Menu.Item, { active: isActive, selected: isSelected, children: _jsx("button", { ref: node => {
367
444
  optionRefs.current[index] = node;
368
445
  }, id: optionId, type: "button", role: "option", "aria-selected": isSelected, onMouseEnter: () => setActiveIndex(index), onMouseDown: e => {
369
446
  e.preventDefault();
370
- }, onClick: () => commitSelection(option), children: _jsx("span", { children: option.label }) }) }, option.value));
447
+ }, onKeyDown: e => handleTabKeyDown(e), onClick: () => commitSelection(option), children: _jsx("span", { children: option.label }) }) }, option.value));
371
448
  })) : (_jsx(Menu.Item, { disabled: true, children: emptyMessage })) }) }), mode === 'multi' &&
372
449
  multiSelectedValuesDisplayMode === 'below-input' &&
373
450
  selectedOptions.length > 0 && (_jsx("div", { style: {
@@ -23,6 +23,7 @@ export interface PopoverProps {
23
23
  autoFocusContent?: boolean;
24
24
  returnFocus?: boolean;
25
25
  anchorRef?: React.RefObject<HTMLElement | null>;
26
+ overlayRef?: React.Ref<HTMLDivElement>;
26
27
  }
27
28
  export interface PopoverHandle {
28
29
  close: () => void;
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { ChevronDown, ChevronUp } from 'lucide-react';
4
+ import * as React from 'react';
4
5
  import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
5
6
  import { createPortal } from 'react-dom';
6
7
  import styles from './Popover.module.css';
@@ -37,7 +38,7 @@ function parseMinWidthPx(minWidth, elForEm) {
37
38
  }
38
39
  return 0;
39
40
  }
40
- 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, }, 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, autoFocusContent = false, returnFocus = true, anchorRef, overlayRef, }, ref) {
41
42
  const internalId = useId();
42
43
  const resolvedContentId = contentId !== null && contentId !== void 0 ? contentId : `popover-${internalId}`;
43
44
  const isControlled = open !== undefined;
@@ -216,9 +217,23 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
216
217
  (_b = (_a = triggerElRef.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a);
217
218
  }, [isOpen, returnFocus]);
218
219
  const icon = isOpen ? _jsx(ChevronUp, { size: 20 }) : _jsx(ChevronDown, { size: 20 });
220
+ const setOverlayRef = React.useCallback((node) => {
221
+ if (!overlayRef)
222
+ return;
223
+ if (typeof overlayRef === 'function') {
224
+ overlayRef(node);
225
+ return;
226
+ }
227
+ const mutableRef = overlayRef;
228
+ mutableRef.current = node;
229
+ }, [overlayRef]);
219
230
  return (_jsxs("div", { className: [styles.container, fullWidth ? styles.fullWidth : ''].filter(Boolean).join(' '), ref: containerRef, children: [Trigger(togglePopover, icon, isOpen), mounted &&
220
231
  isOpen &&
221
- createPortal(_jsx("div", { id: resolvedContentId, ref: contentRef, className: styles.content, style: {
232
+ createPortal(_jsx("div", { id: resolvedContentId, ref: node => {
233
+ const mutableContentRef = contentRef;
234
+ mutableContentRef.current = node;
235
+ setOverlayRef(node);
236
+ }, className: styles.content, style: {
222
237
  top: pos.top,
223
238
  left: pos.left,
224
239
  // Content-driven sizing by default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",