@dbcdk/react-components 0.0.207 → 0.0.208

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/client.cjs CHANGED
@@ -8,6 +8,7 @@ var NavBar = require('./components/nav-bar/NavBar');
8
8
  var Avatar = require('./components/avatar/Avatar');
9
9
  var Popover = require('./components/popover/Popover');
10
10
  var Menu = require('./components/menu/Menu');
11
+ var Listbox = require('./components/listbox/Listbox');
11
12
  var Icon = require('./components/icon/Icon');
12
13
  var UserDisplay = require('./components/user-display/UserDisplay');
13
14
  var Tabs = require('./components/tabs/Tabs');
@@ -24,6 +25,7 @@ var useTheme = require('./hooks/useTheme');
24
25
  var usePersistentState = require('./hooks/usePersistentState');
25
26
  var useTreeViewSettings = require('./hooks/useTreeViewSettings');
26
27
  var Chip = require('./components/chip/Chip');
28
+ var StatusText = require('./components/status-text/StatusText');
27
29
  var Panel = require('./components/panel/Panel');
28
30
  var Card = require('./components/card/Card');
29
31
  var PrintFriendlyCard = require('./components/print-friendly/PrintFriendlyCard');
@@ -136,6 +138,10 @@ Object.defineProperty(exports, "Menu", {
136
138
  enumerable: true,
137
139
  get: function () { return Menu.Menu; }
138
140
  });
141
+ Object.defineProperty(exports, "Listbox", {
142
+ enumerable: true,
143
+ get: function () { return Listbox.Listbox; }
144
+ });
139
145
  Object.defineProperty(exports, "Icon", {
140
146
  enumerable: true,
141
147
  get: function () { return Icon.Icon; }
@@ -208,6 +214,10 @@ Object.defineProperty(exports, "Chip", {
208
214
  enumerable: true,
209
215
  get: function () { return Chip.Chip; }
210
216
  });
217
+ Object.defineProperty(exports, "StatusText", {
218
+ enumerable: true,
219
+ get: function () { return StatusText.StatusText; }
220
+ });
211
221
  Object.defineProperty(exports, "Panel", {
212
222
  enumerable: true,
213
223
  get: function () { return Panel.Panel; }
package/dist/client.d.ts CHANGED
@@ -11,6 +11,8 @@ export { Popover } from './components/popover/Popover';
11
11
  export type { PopoverProps, PopoverHandle } from './components/popover/Popover';
12
12
  export { Menu } from './components/menu/Menu';
13
13
  export type { MenuProps, MenuLabelProps, MenuSeparatorProps, MenuHeaderProps, MenuDescriptionProps, MenuItemProps, MenuCheckItemProps, MenuRadioItemProps, MenuSwitchItemProps, } from './components/menu/Menu';
14
+ export { Listbox } from './components/listbox/Listbox';
15
+ export type { ListboxOption, ListboxProps } from './components/listbox/Listbox';
14
16
  export { Icon, SeverityIcon } from './components/icon/Icon';
15
17
  export { UserDisplay } from './components/user-display/UserDisplay';
16
18
  export { Tabs } from './components/tabs/Tabs';
@@ -40,6 +42,8 @@ export type { PersistentStateStorage, PersistentStateHydration, UsePersistentSta
40
42
  export { useTreeViewSettings, localStorageTreeViewSettingsStorage, } from './hooks/useTreeViewSettings';
41
43
  export type { TreeViewSettingsStorage, UseTreeViewSettingsOptions, UseTreeViewSettingsResult, } from './hooks/useTreeViewSettings';
42
44
  export { Chip } from './components/chip/Chip';
45
+ export { StatusText } from './components/status-text/StatusText';
46
+ export type { StatusTextProps } from './components/status-text/StatusText';
43
47
  export { Panel } from './components/panel/Panel';
44
48
  export { Card } from './components/card/Card';
45
49
  export type { CardProps } from './components/card/Card';
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@ export { NavBar } from './components/nav-bar/NavBar';
6
6
  export { Avatar } from './components/avatar/Avatar';
7
7
  export { Popover } from './components/popover/Popover';
8
8
  export { Menu } from './components/menu/Menu';
9
+ export { Listbox } from './components/listbox/Listbox';
9
10
  export { Icon, SeverityIcon } from './components/icon/Icon';
10
11
  export { UserDisplay } from './components/user-display/UserDisplay';
11
12
  export { Tabs } from './components/tabs/Tabs';
@@ -22,6 +23,7 @@ export { useTheme } from './hooks/useTheme';
22
23
  export { usePersistentState } from './hooks/usePersistentState';
23
24
  export { localStorageTreeViewSettingsStorage, useTreeViewSettings } from './hooks/useTreeViewSettings';
24
25
  export { Chip } from './components/chip/Chip';
26
+ export { StatusText } from './components/status-text/StatusText';
25
27
  export { Panel } from './components/panel/Panel';
26
28
  export { Card } from './components/card/Card';
27
29
  export { PrintFriendlyCard } from './components/print-friendly/PrintFriendlyCard';
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var react = require('react');
6
+ var useRovingFocus = require('../../hooks/useRovingFocus');
7
+ var styles = require('./Listbox.module.css');
8
+ var menuStyles = require('../menu/Menu.module.css');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var styles__default = /*#__PURE__*/_interopDefault(styles);
13
+ var menuStyles__default = /*#__PURE__*/_interopDefault(menuStyles);
14
+
15
+ function Listbox({
16
+ options,
17
+ selectedValues,
18
+ selectionMode = "single",
19
+ autoFocusValue,
20
+ onOptionSelect,
21
+ onActiveChange,
22
+ renderAction,
23
+ actionLabel,
24
+ className,
25
+ style,
26
+ "aria-label": ariaLabel,
27
+ ...rootProps
28
+ }) {
29
+ const rootRef = react.useRef(null);
30
+ const optionRefs = react.useRef(/* @__PURE__ */ new Map());
31
+ const [activeValue, setActiveValue] = react.useState(() => {
32
+ var _a, _b;
33
+ const activeOption = (_b = (_a = options.find((option) => option.value === autoFocusValue && !option.disabled)) != null ? _a : options.find((option) => selectedValues.includes(option.value) && !option.disabled)) != null ? _b : options.find((option) => !option.disabled);
34
+ return activeOption == null ? void 0 : activeOption.value;
35
+ });
36
+ react.useEffect(() => {
37
+ var _a;
38
+ if (autoFocusValue !== void 0) (_a = optionRefs.current.get(autoFocusValue)) == null ? void 0 : _a.focus();
39
+ }, [autoFocusValue]);
40
+ const handleNavigate = react.useCallback(
41
+ (item) => {
42
+ var _a;
43
+ const value = (_a = options.find(
44
+ (option) => String(option.value) === item.dataset.listboxValue
45
+ )) == null ? void 0 : _a.value;
46
+ if (value === void 0) return;
47
+ setActiveValue(value);
48
+ onActiveChange == null ? void 0 : onActiveChange(value);
49
+ },
50
+ [onActiveChange, options]
51
+ );
52
+ const handleKeyDown = useRovingFocus.useRovingFocus({
53
+ containerRef: rootRef,
54
+ itemSelector: '[role="option"]:not([aria-disabled="true"])',
55
+ loop: true,
56
+ resolveCurrentItem: (focused, items) => {
57
+ var _a;
58
+ const value = (_a = focused.closest("[data-listbox-value]")) == null ? void 0 : _a.dataset.listboxValue;
59
+ return items.find((item) => item.dataset.listboxValue === value);
60
+ },
61
+ onNavigate: handleNavigate
62
+ });
63
+ const rowCount = options.length;
64
+ const rowStyle = rowCount ? `repeat(${rowCount}, auto)` : "none";
65
+ return /* @__PURE__ */ jsxRuntime.jsxs(
66
+ "div",
67
+ {
68
+ ...rootProps,
69
+ ref: rootRef,
70
+ className: [styles__default.default.root, className].filter(Boolean).join(" "),
71
+ style: { ...style, gridTemplateRows: rowStyle },
72
+ onKeyDown: (event) => {
73
+ var _a;
74
+ handleKeyDown(event);
75
+ (_a = rootProps.onKeyDown) == null ? void 0 : _a.call(rootProps, event);
76
+ },
77
+ children: [
78
+ /* @__PURE__ */ jsxRuntime.jsx(
79
+ "div",
80
+ {
81
+ className: styles__default.default.options,
82
+ role: "listbox",
83
+ "aria-label": ariaLabel,
84
+ "aria-multiselectable": selectionMode === "multiple" || void 0,
85
+ children: options.map((option) => {
86
+ const isSelected = selectedValues.includes(option.value);
87
+ const isActive = activeValue === option.value;
88
+ const value = String(option.value);
89
+ return /* @__PURE__ */ jsxRuntime.jsx(
90
+ "button",
91
+ {
92
+ ref: (element) => {
93
+ if (element) optionRefs.current.set(option.value, element);
94
+ else optionRefs.current.delete(option.value);
95
+ },
96
+ type: "button",
97
+ role: "option",
98
+ "aria-selected": isSelected,
99
+ "aria-disabled": option.disabled || void 0,
100
+ tabIndex: !option.disabled && isActive ? 0 : -1,
101
+ "data-listbox-value": value,
102
+ className: [
103
+ menuStyles__default.default.interactive,
104
+ menuStyles__default.default.item,
105
+ isSelected ? menuStyles__default.default.selected : "",
106
+ isActive ? menuStyles__default.default.active : "",
107
+ styles__default.default.option,
108
+ option.status === "success" ? styles__default.default.optionSuccess : "",
109
+ option.status === "info" ? styles__default.default.optionInfo : "",
110
+ option.className
111
+ ].filter(Boolean).join(" "),
112
+ disabled: option.disabled,
113
+ onFocus: () => setActiveValue(option.value),
114
+ onClick: () => {
115
+ setActiveValue(option.value);
116
+ onOptionSelect(option.value);
117
+ },
118
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.optionContent, children: [
119
+ option.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, "aria-hidden": "true", children: option.icon }) : null,
120
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.text, children: [
121
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.label, children: option.label }),
122
+ option.description ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.description, children: option.description }) : null
123
+ ] })
124
+ ] })
125
+ },
126
+ value
127
+ );
128
+ })
129
+ }
130
+ ),
131
+ renderAction ? /* @__PURE__ */ jsxRuntime.jsx(
132
+ "div",
133
+ {
134
+ className: styles__default.default.actions,
135
+ role: "group",
136
+ "aria-label": actionLabel != null ? actionLabel : `${ariaLabel} actions`,
137
+ children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
138
+ "div",
139
+ {
140
+ className: styles__default.default.actionRow,
141
+ "data-listbox-value": String(option.value),
142
+ children: renderAction(option, selectedValues.includes(option.value))
143
+ },
144
+ String(option.value)
145
+ ))
146
+ }
147
+ ) : null
148
+ ]
149
+ }
150
+ );
151
+ }
152
+
153
+ exports.Listbox = Listbox;
@@ -0,0 +1,22 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export type ListboxOption<T extends string | number = string> = {
3
+ value: T;
4
+ label: ReactNode;
5
+ description?: ReactNode;
6
+ icon?: ReactNode;
7
+ className?: string;
8
+ status?: 'info' | 'success';
9
+ disabled?: boolean;
10
+ };
11
+ export type ListboxProps<T extends string | number = string> = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onSelect' | 'aria-label'> & {
12
+ 'aria-label': string;
13
+ actionLabel?: string;
14
+ options: readonly ListboxOption<T>[];
15
+ selectedValues: readonly T[];
16
+ selectionMode?: 'single' | 'multiple';
17
+ autoFocusValue?: T;
18
+ onOptionSelect: (value: T) => void;
19
+ onActiveChange?: (value: T) => void;
20
+ renderAction?: (option: ListboxOption<T>, selected: boolean) => ReactNode;
21
+ };
22
+ export declare function Listbox<T extends string | number = string>({ options, selectedValues, selectionMode, autoFocusValue, onOptionSelect, onActiveChange, renderAction, actionLabel, className, style, 'aria-label': ariaLabel, ...rootProps }: Readonly<ListboxProps<T>>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { useRef, useState, useEffect, useCallback } from 'react';
4
+ import { useRovingFocus } from '../../hooks/useRovingFocus';
5
+ import styles from './Listbox.module.css';
6
+ import menuStyles from '../menu/Menu.module.css';
7
+
8
+ function Listbox({
9
+ options,
10
+ selectedValues,
11
+ selectionMode = "single",
12
+ autoFocusValue,
13
+ onOptionSelect,
14
+ onActiveChange,
15
+ renderAction,
16
+ actionLabel,
17
+ className,
18
+ style,
19
+ "aria-label": ariaLabel,
20
+ ...rootProps
21
+ }) {
22
+ const rootRef = useRef(null);
23
+ const optionRefs = useRef(/* @__PURE__ */ new Map());
24
+ const [activeValue, setActiveValue] = useState(() => {
25
+ var _a, _b;
26
+ const activeOption = (_b = (_a = options.find((option) => option.value === autoFocusValue && !option.disabled)) != null ? _a : options.find((option) => selectedValues.includes(option.value) && !option.disabled)) != null ? _b : options.find((option) => !option.disabled);
27
+ return activeOption == null ? void 0 : activeOption.value;
28
+ });
29
+ useEffect(() => {
30
+ var _a;
31
+ if (autoFocusValue !== void 0) (_a = optionRefs.current.get(autoFocusValue)) == null ? void 0 : _a.focus();
32
+ }, [autoFocusValue]);
33
+ const handleNavigate = useCallback(
34
+ (item) => {
35
+ var _a;
36
+ const value = (_a = options.find(
37
+ (option) => String(option.value) === item.dataset.listboxValue
38
+ )) == null ? void 0 : _a.value;
39
+ if (value === void 0) return;
40
+ setActiveValue(value);
41
+ onActiveChange == null ? void 0 : onActiveChange(value);
42
+ },
43
+ [onActiveChange, options]
44
+ );
45
+ const handleKeyDown = useRovingFocus({
46
+ containerRef: rootRef,
47
+ itemSelector: '[role="option"]:not([aria-disabled="true"])',
48
+ loop: true,
49
+ resolveCurrentItem: (focused, items) => {
50
+ var _a;
51
+ const value = (_a = focused.closest("[data-listbox-value]")) == null ? void 0 : _a.dataset.listboxValue;
52
+ return items.find((item) => item.dataset.listboxValue === value);
53
+ },
54
+ onNavigate: handleNavigate
55
+ });
56
+ const rowCount = options.length;
57
+ const rowStyle = rowCount ? `repeat(${rowCount}, auto)` : "none";
58
+ return /* @__PURE__ */ jsxs(
59
+ "div",
60
+ {
61
+ ...rootProps,
62
+ ref: rootRef,
63
+ className: [styles.root, className].filter(Boolean).join(" "),
64
+ style: { ...style, gridTemplateRows: rowStyle },
65
+ onKeyDown: (event) => {
66
+ var _a;
67
+ handleKeyDown(event);
68
+ (_a = rootProps.onKeyDown) == null ? void 0 : _a.call(rootProps, event);
69
+ },
70
+ children: [
71
+ /* @__PURE__ */ jsx(
72
+ "div",
73
+ {
74
+ className: styles.options,
75
+ role: "listbox",
76
+ "aria-label": ariaLabel,
77
+ "aria-multiselectable": selectionMode === "multiple" || void 0,
78
+ children: options.map((option) => {
79
+ const isSelected = selectedValues.includes(option.value);
80
+ const isActive = activeValue === option.value;
81
+ const value = String(option.value);
82
+ return /* @__PURE__ */ jsx(
83
+ "button",
84
+ {
85
+ ref: (element) => {
86
+ if (element) optionRefs.current.set(option.value, element);
87
+ else optionRefs.current.delete(option.value);
88
+ },
89
+ type: "button",
90
+ role: "option",
91
+ "aria-selected": isSelected,
92
+ "aria-disabled": option.disabled || void 0,
93
+ tabIndex: !option.disabled && isActive ? 0 : -1,
94
+ "data-listbox-value": value,
95
+ className: [
96
+ menuStyles.interactive,
97
+ menuStyles.item,
98
+ isSelected ? menuStyles.selected : "",
99
+ isActive ? menuStyles.active : "",
100
+ styles.option,
101
+ option.status === "success" ? styles.optionSuccess : "",
102
+ option.status === "info" ? styles.optionInfo : "",
103
+ option.className
104
+ ].filter(Boolean).join(" "),
105
+ disabled: option.disabled,
106
+ onFocus: () => setActiveValue(option.value),
107
+ onClick: () => {
108
+ setActiveValue(option.value);
109
+ onOptionSelect(option.value);
110
+ },
111
+ children: /* @__PURE__ */ jsxs("span", { className: styles.optionContent, children: [
112
+ option.icon ? /* @__PURE__ */ jsx("span", { className: styles.icon, "aria-hidden": "true", children: option.icon }) : null,
113
+ /* @__PURE__ */ jsxs("span", { className: styles.text, children: [
114
+ /* @__PURE__ */ jsx("span", { className: styles.label, children: option.label }),
115
+ option.description ? /* @__PURE__ */ jsx("span", { className: styles.description, children: option.description }) : null
116
+ ] })
117
+ ] })
118
+ },
119
+ value
120
+ );
121
+ })
122
+ }
123
+ ),
124
+ renderAction ? /* @__PURE__ */ jsx(
125
+ "div",
126
+ {
127
+ className: styles.actions,
128
+ role: "group",
129
+ "aria-label": actionLabel != null ? actionLabel : `${ariaLabel} actions`,
130
+ children: options.map((option) => /* @__PURE__ */ jsx(
131
+ "div",
132
+ {
133
+ className: styles.actionRow,
134
+ "data-listbox-value": String(option.value),
135
+ children: renderAction(option, selectedValues.includes(option.value))
136
+ },
137
+ String(option.value)
138
+ ))
139
+ }
140
+ ) : null
141
+ ]
142
+ }
143
+ );
144
+ }
145
+
146
+ export { Listbox };
@@ -0,0 +1,97 @@
1
+ .root {
2
+ display: grid;
3
+ grid-template-columns: minmax(0, 1fr) auto;
4
+ column-gap: var(--spacing-xxs);
5
+ }
6
+
7
+ .options,
8
+ .actions {
9
+ display: grid;
10
+ grid-template-rows: subgrid;
11
+ grid-row: 1 / -1;
12
+ }
13
+
14
+ .options {
15
+ grid-column: 1;
16
+ min-width: 0;
17
+ }
18
+
19
+ .actions {
20
+ grid-column: 2;
21
+ }
22
+
23
+ .option {
24
+ align-items: center;
25
+ min-width: 0;
26
+ }
27
+
28
+ .optionContent {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: var(--spacing-sm);
32
+ width: 100%;
33
+ min-width: 0;
34
+ }
35
+
36
+ .icon {
37
+ display: inline-flex;
38
+ flex: 0 0 auto;
39
+ align-items: center;
40
+ justify-content: center;
41
+ color: var(--color-fg-muted);
42
+ }
43
+
44
+ .icon svg {
45
+ width: var(--icon-size-md);
46
+ height: var(--icon-size-md);
47
+ margin: 0;
48
+ }
49
+
50
+ .text {
51
+ display: flex;
52
+ flex: 1 1 auto;
53
+ flex-direction: column;
54
+ min-width: 0;
55
+ }
56
+
57
+ .label,
58
+ .description {
59
+ min-width: 0;
60
+ overflow-wrap: anywhere;
61
+ }
62
+
63
+ .description {
64
+ color: var(--color-fg-muted);
65
+ font-size: var(--font-size-sm);
66
+ }
67
+
68
+ .option.optionSuccess[aria-selected='true'],
69
+ .option.optionSuccess.active {
70
+ background: var(--color-status-success-bg);
71
+ color: var(--color-status-success-fg);
72
+ box-shadow: inset 0 0 0 1px var(--color-status-success-border);
73
+ }
74
+
75
+ .option.optionSuccess .icon,
76
+ .option.optionSuccess .description {
77
+ color: var(--color-status-success-fg);
78
+ }
79
+
80
+ .option.optionInfo[aria-selected='true'],
81
+ .option.optionInfo.active {
82
+ background: var(--color-status-info-bg);
83
+ color: var(--color-status-info-fg);
84
+ box-shadow: inset 0 0 0 1px var(--color-status-info-border);
85
+ }
86
+
87
+ .option.optionInfo .icon,
88
+ .option.optionInfo .description {
89
+ color: var(--color-status-info-fg);
90
+ }
91
+
92
+ .actionRow {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ padding-inline-start: var(--spacing-xxs);
97
+ }
@@ -3,6 +3,7 @@
3
3
 
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var React = require('react');
6
+ var useRovingFocus = require('../../hooks/useRovingFocus');
6
7
  var styles = require('./Menu.module.css');
7
8
  var Checkbox = require('../forms/checkbox/Checkbox');
8
9
  var RadioButton = require('../forms/radio-buttons/RadioButton');
@@ -31,34 +32,16 @@ function _interopNamespace(e) {
31
32
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
33
  var styles__default = /*#__PURE__*/_interopDefault(styles);
33
34
 
34
- const INTERACTIVE_SELECTOR = 'a[href]:not([aria-disabled="true"]), button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="option"]:not([aria-disabled="true"])';
35
- function getMenuItems(el) {
36
- return Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
37
- }
38
35
  const MenuBase = React__namespace.forwardRef(
39
36
  ({ children, className, itemRole = "menuitem", gap, onKeyDown, ...props }, ref) => {
40
37
  const internalRef = React__namespace.useRef(null);
38
+ const handleRovingFocus = useRovingFocus.useRovingFocus({
39
+ containerRef: internalRef,
40
+ itemSelector: 'a[href]:not([aria-disabled="true"]), button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="option"]:not([aria-disabled="true"])',
41
+ loop: true
42
+ });
41
43
  const handleKeyDown = (e) => {
42
- const ul = internalRef.current;
43
- if (!ul) return;
44
- const items = getMenuItems(ul);
45
- if (items.length === 0) return;
46
- const focused = document.activeElement;
47
- const currentIndex = items.indexOf(focused);
48
- let nextIndex = null;
49
- if (e.key === "ArrowDown") {
50
- nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
51
- } else if (e.key === "ArrowUp") {
52
- nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
53
- } else if (e.key === "Home") {
54
- nextIndex = 0;
55
- } else if (e.key === "End") {
56
- nextIndex = items.length - 1;
57
- }
58
- if (nextIndex !== null) {
59
- e.preventDefault();
60
- items[nextIndex].focus();
61
- }
44
+ handleRovingFocus(e);
62
45
  onKeyDown == null ? void 0 : onKeyDown(e);
63
46
  };
64
47
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1,39 +1,22 @@
1
1
  'use client';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import * as React from 'react';
4
+ import { useRovingFocus } from '../../hooks/useRovingFocus';
4
5
  import styles from './Menu.module.css';
5
6
  import { Checkbox } from '../forms/checkbox/Checkbox';
6
7
  import { RadioButton } from '../forms/radio-buttons/RadioButton';
7
8
  import { Switch } from '../forms/switch/Switch';
8
9
 
9
- const INTERACTIVE_SELECTOR = 'a[href]:not([aria-disabled="true"]), button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="option"]:not([aria-disabled="true"])';
10
- function getMenuItems(el) {
11
- return Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
12
- }
13
10
  const MenuBase = React.forwardRef(
14
11
  ({ children, className, itemRole = "menuitem", gap, onKeyDown, ...props }, ref) => {
15
12
  const internalRef = React.useRef(null);
13
+ const handleRovingFocus = useRovingFocus({
14
+ containerRef: internalRef,
15
+ itemSelector: 'a[href]:not([aria-disabled="true"]), button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([aria-disabled="true"]), [role="menuitem"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="option"]:not([aria-disabled="true"])',
16
+ loop: true
17
+ });
16
18
  const handleKeyDown = (e) => {
17
- const ul = internalRef.current;
18
- if (!ul) return;
19
- const items = getMenuItems(ul);
20
- if (items.length === 0) return;
21
- const focused = document.activeElement;
22
- const currentIndex = items.indexOf(focused);
23
- let nextIndex = null;
24
- if (e.key === "ArrowDown") {
25
- nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
26
- } else if (e.key === "ArrowUp") {
27
- nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
28
- } else if (e.key === "Home") {
29
- nextIndex = 0;
30
- } else if (e.key === "End") {
31
- nextIndex = items.length - 1;
32
- }
33
- if (nextIndex !== null) {
34
- e.preventDefault();
35
- items[nextIndex].focus();
36
- }
19
+ handleRovingFocus(e);
37
20
  onKeyDown == null ? void 0 : onKeyDown(e);
38
21
  };
39
22
  return /* @__PURE__ */ jsx(
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var styles = require('./StatusText.module.css');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var styles__default = /*#__PURE__*/_interopDefault(styles);
10
+
11
+ function StatusText({
12
+ children,
13
+ severity = "neutral",
14
+ className,
15
+ ...props
16
+ }) {
17
+ return /* @__PURE__ */ jsxRuntime.jsx(
18
+ "span",
19
+ {
20
+ ...props,
21
+ className: [styles__default.default.statusText, styles__default.default[severity], className].filter(Boolean).join(" "),
22
+ children
23
+ }
24
+ );
25
+ }
26
+
27
+ exports.StatusText = StatusText;
@@ -0,0 +1,7 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import type { Severity } from '../../constants/severity.types';
3
+ export interface StatusTextProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children: ReactNode;
5
+ severity?: Severity;
6
+ }
7
+ export declare function StatusText({ children, severity, className, ...props }: Readonly<StatusTextProps>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import styles from './StatusText.module.css';
4
+
5
+ function StatusText({
6
+ children,
7
+ severity = "neutral",
8
+ className,
9
+ ...props
10
+ }) {
11
+ return /* @__PURE__ */ jsx(
12
+ "span",
13
+ {
14
+ ...props,
15
+ className: [styles.statusText, styles[severity], className].filter(Boolean).join(" "),
16
+ children
17
+ }
18
+ );
19
+ }
20
+
21
+ export { StatusText };
@@ -0,0 +1,41 @@
1
+ .statusText {
2
+ background: var(--status-text-bg);
3
+ color: var(--status-text-fg);
4
+ -webkit-box-decoration-break: clone;
5
+ box-decoration-break: clone;
6
+ }
7
+
8
+ .neutral {
9
+ --status-text-bg: var(--color-bg-surface-subtle);
10
+ --status-text-fg: var(--color-fg-default);
11
+ }
12
+
13
+ .brand {
14
+ --status-text-bg: color-mix(in srgb, var(--color-brand) 12%, var(--color-bg-surface));
15
+ --status-text-fg: var(--color-brand);
16
+ }
17
+
18
+ .accent {
19
+ --status-text-bg: var(--color-accent-bg-soft);
20
+ --status-text-fg: var(--color-accent-fg-soft);
21
+ }
22
+
23
+ .success {
24
+ --status-text-bg: var(--color-status-success-bg);
25
+ --status-text-fg: var(--color-status-success-fg);
26
+ }
27
+
28
+ .error {
29
+ --status-text-bg: var(--color-status-error-bg);
30
+ --status-text-fg: var(--color-status-error-fg);
31
+ }
32
+
33
+ .info {
34
+ --status-text-bg: var(--color-status-info-bg);
35
+ --status-text-fg: var(--color-status-info-fg);
36
+ }
37
+
38
+ .warning {
39
+ --status-text-bg: var(--color-status-warning-bg);
40
+ --status-text-fg: var(--color-status-warning-fg);
41
+ }
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ function useRovingFocus({
6
+ containerRef,
7
+ itemSelector,
8
+ loop = false,
9
+ resolveCurrentItem,
10
+ onNavigate
11
+ }) {
12
+ return react.useCallback(
13
+ (event) => {
14
+ const container = containerRef.current;
15
+ if (!container) return;
16
+ const items = Array.from(container.querySelectorAll(itemSelector));
17
+ if (items.length === 0) return;
18
+ const focused = document.activeElement;
19
+ const currentItem = items.includes(focused) ? focused : resolveCurrentItem == null ? void 0 : resolveCurrentItem(focused, items);
20
+ const currentIndex = currentItem ? items.indexOf(currentItem) : -1;
21
+ let nextIndex = null;
22
+ if (event.key === "ArrowDown") {
23
+ nextIndex = currentIndex < 0 ? 0 : currentIndex + 1;
24
+ if (loop && nextIndex >= items.length) nextIndex = 0;
25
+ else if (nextIndex >= items.length) nextIndex = items.length - 1;
26
+ } else if (event.key === "ArrowUp") {
27
+ nextIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;
28
+ if (loop && nextIndex < 0) nextIndex = items.length - 1;
29
+ else if (nextIndex < 0) nextIndex = 0;
30
+ } else if (event.key === "Home") {
31
+ nextIndex = 0;
32
+ } else if (event.key === "End") {
33
+ nextIndex = items.length - 1;
34
+ }
35
+ if (nextIndex === null) return;
36
+ event.preventDefault();
37
+ const nextItem = items[nextIndex];
38
+ nextItem.focus();
39
+ onNavigate == null ? void 0 : onNavigate(nextItem);
40
+ },
41
+ [containerRef, itemSelector, loop, onNavigate, resolveCurrentItem]
42
+ );
43
+ }
44
+
45
+ exports.useRovingFocus = useRovingFocus;
@@ -0,0 +1,10 @@
1
+ import { type KeyboardEvent, type RefObject } from 'react';
2
+ type UseRovingFocusOptions<T extends HTMLElement> = {
3
+ containerRef: RefObject<T | null>;
4
+ itemSelector: string;
5
+ loop?: boolean;
6
+ resolveCurrentItem?: (focused: HTMLElement, items: HTMLElement[]) => HTMLElement | undefined;
7
+ onNavigate?: (item: HTMLElement) => void;
8
+ };
9
+ export declare function useRovingFocus<T extends HTMLElement>({ containerRef, itemSelector, loop, resolveCurrentItem, onNavigate, }: UseRovingFocusOptions<T>): (event: KeyboardEvent) => void;
10
+ export {};
@@ -0,0 +1,43 @@
1
+ import { useCallback } from 'react';
2
+
3
+ function useRovingFocus({
4
+ containerRef,
5
+ itemSelector,
6
+ loop = false,
7
+ resolveCurrentItem,
8
+ onNavigate
9
+ }) {
10
+ return useCallback(
11
+ (event) => {
12
+ const container = containerRef.current;
13
+ if (!container) return;
14
+ const items = Array.from(container.querySelectorAll(itemSelector));
15
+ if (items.length === 0) return;
16
+ const focused = document.activeElement;
17
+ const currentItem = items.includes(focused) ? focused : resolveCurrentItem == null ? void 0 : resolveCurrentItem(focused, items);
18
+ const currentIndex = currentItem ? items.indexOf(currentItem) : -1;
19
+ let nextIndex = null;
20
+ if (event.key === "ArrowDown") {
21
+ nextIndex = currentIndex < 0 ? 0 : currentIndex + 1;
22
+ if (loop && nextIndex >= items.length) nextIndex = 0;
23
+ else if (nextIndex >= items.length) nextIndex = items.length - 1;
24
+ } else if (event.key === "ArrowUp") {
25
+ nextIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;
26
+ if (loop && nextIndex < 0) nextIndex = items.length - 1;
27
+ else if (nextIndex < 0) nextIndex = 0;
28
+ } else if (event.key === "Home") {
29
+ nextIndex = 0;
30
+ } else if (event.key === "End") {
31
+ nextIndex = items.length - 1;
32
+ }
33
+ if (nextIndex === null) return;
34
+ event.preventDefault();
35
+ const nextItem = items[nextIndex];
36
+ nextItem.focus();
37
+ onNavigate == null ? void 0 : onNavigate(nextItem);
38
+ },
39
+ [containerRef, itemSelector, loop, onNavigate, resolveCurrentItem]
40
+ );
41
+ }
42
+
43
+ export { useRovingFocus };
package/dist/index.css CHANGED
@@ -42,6 +42,7 @@
42
42
  @import './components/json-viewer/JsonViewer.module.css';
43
43
  @import './components/lightbox/Lightbox.module.css';
44
44
  @import './components/line-viewer/LineViewer.module.css';
45
+ @import './components/listbox/Listbox.module.css';
45
46
  @import './components/media-card/MediaCard.module.css';
46
47
  @import './components/menu/Menu.module.css';
47
48
  @import './components/meta-bar/MetaBar.module.css';
@@ -71,6 +72,7 @@
71
72
  @import './components/split-pane/SplitPane.module.css';
72
73
  @import './components/stack/Stack.module.css';
73
74
  @import './components/state-page/StatePage.module.css';
75
+ @import './components/status-text/StatusText.module.css';
74
76
  @import './components/table/Table.module.css';
75
77
  @import './components/table/components/column-resizer/ColumnResizer.module.css';
76
78
  @import './components/table/components/empty-state/EmptyState.module.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.207",
3
+ "version": "0.0.208",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",