@dbcdk/react-components 0.0.57 → 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.
@@ -9,8 +9,10 @@ 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();
@@ -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,7 +357,7 @@ 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, icon) => {
360
+ }, fullWidth: fullWidth, autoFocusContent: false, returnFocus: false, overlayRef: popoverContentRef, trigger: (openPopover, icon) => {
288
361
  var _a, _b, _c, _d, _e;
289
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);
@@ -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.57",
3
+ "version": "0.0.58",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",