@allxsmith/bestax-bulma 5.61.0 → 5.62.0
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/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/TreeView.js +14 -10
- package/dist/esm/components/TreeView.js.map +1 -1
- package/dist/esm/form/Combobox.js +8 -13
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/Field.js.map +1 -1
- package/dist/esm/form/TreeSelect.js +395 -0
- package/dist/esm/form/TreeSelect.js.map +1 -0
- package/dist/esm/form/_pickerInternals/PickerPopover.js +5 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
- package/dist/esm/form/_selectInternals/DialogSelect.js +208 -0
- package/dist/esm/form/_selectInternals/DialogSelect.js.map +1 -0
- package/dist/esm/form/_selectInternals/ValueTags.js +28 -0
- package/dist/esm/form/_selectInternals/ValueTags.js.map +1 -0
- package/dist/esm/helpers/treeData.js +97 -0
- package/dist/esm/helpers/treeData.js.map +1 -0
- package/dist/esm/helpers/useComboboxEngine.js +2 -2
- package/dist/esm/helpers/useComboboxEngine.js.map +1 -1
- package/dist/esm/helpers/useControlled.js +30 -0
- package/dist/esm/helpers/useControlled.js.map +1 -0
- package/dist/esm/helpers/useTreeView.js +15 -13
- package/dist/esm/helpers/useTreeView.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +1099 -392
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/form/Field.d.ts +1 -1
- package/dist/types/form/TreeSelect.d.ts +308 -0
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +8 -0
- package/dist/types/form/_selectInternals/DialogSelect.d.ts +86 -0
- package/dist/types/form/_selectInternals/ValueTags.d.ts +40 -0
- package/dist/types/helpers/treeData.d.ts +48 -0
- package/dist/types/helpers/useControlled.d.ts +14 -0
- package/dist/types/helpers/useTreeView.d.ts +30 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types-cjs/form/Field.d.ts +1 -1
- package/dist/types-cjs/form/TreeSelect.d.ts +308 -0
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +8 -0
- package/dist/types-cjs/form/_selectInternals/DialogSelect.d.ts +86 -0
- package/dist/types-cjs/form/_selectInternals/ValueTags.d.ts +40 -0
- package/dist/types-cjs/helpers/treeData.d.ts +48 -0
- package/dist/types-cjs/helpers/useControlled.d.ts +14 -0
- package/dist/types-cjs/helpers/useTreeView.d.ts +30 -0
- package/dist/types-cjs/index.d.ts +1 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/form/_index.scss +1 -0
- package/src/scss/form/_picker-popover.scss +6 -0
- package/src/scss/form/_treeselect.scss +146 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
3
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
4
|
+
import { useId, useRef, useState, useEffect } from 'react';
|
|
5
|
+
import { usePrefixedClassNames, classNames } from '../../helpers/classNames.js';
|
|
6
|
+
import { useBulmaClasses } from '../../helpers/useBulmaClasses.js';
|
|
7
|
+
import { useLatestRef } from '../../helpers/useLatestRef.js';
|
|
8
|
+
import { Delete } from '../../elements/Delete.js';
|
|
9
|
+
import { Icon } from '../../elements/Icon.js';
|
|
10
|
+
import { Control } from '../Control.js';
|
|
11
|
+
import { Field } from '../Field.js';
|
|
12
|
+
import { useInsideField, useInsideControl, rendersOwnField } from '../FormContext.js';
|
|
13
|
+
import { useAutoLabelId } from '../useAutoLabelId.js';
|
|
14
|
+
import { PickerPopover } from '../_pickerInternals/PickerPopover.js';
|
|
15
|
+
import { isPopoverToggleKey } from '../_pickerInternals/pickerTypes.js';
|
|
16
|
+
import { usePickerLocaleScope } from '../_pickerInternals/useLocaleDefaults.js';
|
|
17
|
+
import { HiddenValues } from './ValueTags.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A field laid over the real one, unseen and letting presses through, so the
|
|
21
|
+
* browser's `required` check reads it and its message points at the field.
|
|
22
|
+
*/
|
|
23
|
+
const requiredProxyStyle = {
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
inset: 0,
|
|
26
|
+
width: '100%',
|
|
27
|
+
height: '100%',
|
|
28
|
+
margin: 0,
|
|
29
|
+
padding: 0,
|
|
30
|
+
border: 0,
|
|
31
|
+
opacity: 0,
|
|
32
|
+
pointerEvents: 'none',
|
|
33
|
+
// Focused, it opens no on-screen keyboard (`inputMode`), and iOS zooms
|
|
34
|
+
// in on a field set under 16px. It's unseen, so the size shows nowhere.
|
|
35
|
+
fontSize: '16px',
|
|
36
|
+
};
|
|
37
|
+
/** Keys pressed on their own that don't act, such as Ctrl to stop speech. */
|
|
38
|
+
const MODIFIER_KEYS = new Set([
|
|
39
|
+
'Alt',
|
|
40
|
+
'AltGraph',
|
|
41
|
+
'CapsLock',
|
|
42
|
+
'Control',
|
|
43
|
+
'Fn',
|
|
44
|
+
'Meta',
|
|
45
|
+
'NumLock',
|
|
46
|
+
'ScrollLock',
|
|
47
|
+
'Shift',
|
|
48
|
+
]);
|
|
49
|
+
const ignoreChange = () => { };
|
|
50
|
+
/**
|
|
51
|
+
* A read-only `role="combobox"` field, wired as the date pickers' are, that
|
|
52
|
+
* opens a `PickerPopover` dialog panel: in a `Field` of its own unless one is
|
|
53
|
+
* around it, with a chevron at its inline end, a clear button, the hidden
|
|
54
|
+
* inputs for `name` and what the browser's `required` check reads. A click,
|
|
55
|
+
* Enter, Space, Down or Alt+Down opens the panel, which holds focus, as the
|
|
56
|
+
* date pickers' does: Tab and Shift+Tab wrap inside it. Escape and Alt+Up
|
|
57
|
+
* close it and put focus back on the field, and a press outside closes it
|
|
58
|
+
* where the press put focus.
|
|
59
|
+
*/
|
|
60
|
+
function DialogSelect({ component, rootClass, panelClass, text, values, hasValue, open, onOpenChange, onFormReset, onPrintableKey, onClear, clearable, clearLabel, chooseLabel, placeholder, name, form, required = false, disabled, color, size, isRounded, position = 'bottom-left', appendToBody = false, inputRef, fieldRef, describedBy, below, initialFocusSelector, children, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...props }) {
|
|
61
|
+
const insideField = useInsideField();
|
|
62
|
+
const insideControl = useInsideControl();
|
|
63
|
+
const ownField = rendersOwnField(component, {
|
|
64
|
+
insideField,
|
|
65
|
+
insideControl,
|
|
66
|
+
label,
|
|
67
|
+
message,
|
|
68
|
+
horizontal,
|
|
69
|
+
fieldClassName,
|
|
70
|
+
});
|
|
71
|
+
const { controlId, fieldLabelProps, labelId } = useAutoLabelId({
|
|
72
|
+
label,
|
|
73
|
+
id,
|
|
74
|
+
labelProps,
|
|
75
|
+
rendersLabel: ownField,
|
|
76
|
+
needsLabelId: true,
|
|
77
|
+
});
|
|
78
|
+
const reactId = useId();
|
|
79
|
+
const panelId = `${controlId ?? reactId}-panel`;
|
|
80
|
+
const localeScope = usePickerLocaleScope(undefined, props.dir);
|
|
81
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
82
|
+
const rootRef = useRef(null);
|
|
83
|
+
// The field's name, which the panel takes too unless it's given its own.
|
|
84
|
+
const fieldName = ariaLabel
|
|
85
|
+
? { 'aria-label': ariaLabel }
|
|
86
|
+
: ariaLabelledBy
|
|
87
|
+
? { 'aria-labelledby': ariaLabelledBy }
|
|
88
|
+
: labelId
|
|
89
|
+
? { 'aria-labelledby': labelId }
|
|
90
|
+
: {};
|
|
91
|
+
const panelName = chooseLabel
|
|
92
|
+
? { 'aria-label': chooseLabel }
|
|
93
|
+
: fieldName;
|
|
94
|
+
// Whether the field `required` hangs on has focus, which the browser gives
|
|
95
|
+
// it when it stops a submit. Taking it away or disabling it while it has
|
|
96
|
+
// focus fires no blur, so that clears it too.
|
|
97
|
+
const [proxyFocused, setProxyFocused] = useState(false);
|
|
98
|
+
if (proxyFocused && (!required || disabled))
|
|
99
|
+
setProxyFocused(false);
|
|
100
|
+
const showClear = !!clearable && hasValue && !disabled;
|
|
101
|
+
const rootClasses = usePrefixedClassNames(rootClass, {
|
|
102
|
+
'is-disabled': disabled,
|
|
103
|
+
'has-clear': showClear,
|
|
104
|
+
});
|
|
105
|
+
const inputClass = usePrefixedClassNames('input', {
|
|
106
|
+
[`is-${color}`]: !!color,
|
|
107
|
+
[`is-${size}`]: !!size,
|
|
108
|
+
'is-rounded': !!isRounded,
|
|
109
|
+
'is-focused': proxyFocused,
|
|
110
|
+
});
|
|
111
|
+
const chevronClass = usePrefixedClassNames(`${rootClass}-chevron`);
|
|
112
|
+
const clearClass = usePrefixedClassNames(`${rootClass}-clear`);
|
|
113
|
+
const panelClasses = usePrefixedClassNames(panelClass, {
|
|
114
|
+
[`is-${size}`]: !!size,
|
|
115
|
+
});
|
|
116
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
117
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
118
|
+
});
|
|
119
|
+
const isOpen = open && !disabled;
|
|
120
|
+
// A reset of the field's form puts the value back, as it does a native
|
|
121
|
+
// field's. The reset event fires before the form resets, and a listener
|
|
122
|
+
// after this one can still cancel it, so the value goes back a task later,
|
|
123
|
+
// and only if the reset went ahead, as File's and Dropzone's do. It's
|
|
124
|
+
// heard on the field's document, or the shadow root it's in, and matched
|
|
125
|
+
// to the field's form as it is then, so a form its `form` names that
|
|
126
|
+
// mounts later, or a `form` that changes, is followed, and a reset
|
|
127
|
+
// already under way still lands.
|
|
128
|
+
const onFormResetRef = useLatestRef(onFormReset);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const root = inputRef.current.getRootNode();
|
|
131
|
+
let timer;
|
|
132
|
+
const onReset = (event) => {
|
|
133
|
+
if (event.target !== inputRef.current?.form)
|
|
134
|
+
return;
|
|
135
|
+
timer = setTimeout(() => {
|
|
136
|
+
if (!event.defaultPrevented)
|
|
137
|
+
onFormResetRef.current();
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
root.addEventListener('reset', onReset, true);
|
|
141
|
+
return () => {
|
|
142
|
+
root.removeEventListener('reset', onReset, true);
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
};
|
|
145
|
+
}, [inputRef, onFormResetRef]);
|
|
146
|
+
const handleKeyDown = (event) => {
|
|
147
|
+
if (disabled)
|
|
148
|
+
return;
|
|
149
|
+
if (isPopoverToggleKey(event)) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
if (event.key === 'ArrowDown')
|
|
152
|
+
onOpenChange(true);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
156
|
+
return;
|
|
157
|
+
if (event.key === 'Enter' ||
|
|
158
|
+
event.key === ' ' ||
|
|
159
|
+
event.key === 'ArrowDown') {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
onOpenChange(true);
|
|
162
|
+
}
|
|
163
|
+
else if (event.key.length === 1 &&
|
|
164
|
+
!event.nativeEvent.isComposing &&
|
|
165
|
+
onPrintableKey?.(event.key)) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const fieldDescription = [ariaDescribedBy, describedBy].filter(Boolean).join(' ') || undefined;
|
|
170
|
+
const field = (jsxs(Fragment, { children: [jsx("input", { ref: fieldRef, type: "text", className: inputClass, id: controlId, readOnly: true, role: "combobox", "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-controls": isOpen ? panelId : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": fieldDescription, "aria-required": required || undefined, autoComplete: "off", value: text, placeholder: placeholder, disabled: disabled, form: form,
|
|
171
|
+
// A disabled field takes no clicks.
|
|
172
|
+
onClick: () => onOpenChange(!isOpen), onKeyDown: handleKeyDown }), showClear && (jsx(Delete, { className: clearClass, size: size, ariaLabel: clearLabel, onClick: () => {
|
|
173
|
+
onClear();
|
|
174
|
+
onOpenChange(false);
|
|
175
|
+
inputRef.current?.focus();
|
|
176
|
+
} })), jsx("span", { className: chevronClass, "aria-hidden": "true", children: jsx(Icon, { name: "chevron-down", size: size }) })] }));
|
|
177
|
+
const root = (jsxs("div", { ...localeScope.rootAttrs, ...rest, ref: rootRef, className: classNames(rootClasses, bulmaHelperClasses, className), children: [insideControl ? field : jsx(Control, { size: size, children: field }), required && (jsx("input", { ...(proxyFocused
|
|
178
|
+
? {
|
|
179
|
+
...fieldName,
|
|
180
|
+
role: 'combobox',
|
|
181
|
+
'aria-haspopup': 'dialog',
|
|
182
|
+
'aria-expanded': false,
|
|
183
|
+
'aria-describedby': fieldDescription,
|
|
184
|
+
}
|
|
185
|
+
: { 'aria-hidden': true }), tabIndex: -1, inputMode: "none", required: true, value: hasValue ? 'picked' : '', onChange: ignoreChange, form: form, disabled: disabled,
|
|
186
|
+
// When the browser stops a submit, it focuses this and shows its
|
|
187
|
+
// message beside it, and every browser takes the message down as
|
|
188
|
+
// this loses focus. So focus stays here, over the field, which
|
|
189
|
+
// looks focused, and a press goes where it lands, as from any
|
|
190
|
+
// field. The next key hands focus to the field and does there
|
|
191
|
+
// what it would have. Firefox also stops taking clicks if focus
|
|
192
|
+
// leaves while it's still focusing this.
|
|
193
|
+
onFocus: () => setProxyFocused(true), onBlur: () => setProxyFocused(false), onKeyDown: event => {
|
|
194
|
+
if (MODIFIER_KEYS.has(event.key))
|
|
195
|
+
return;
|
|
196
|
+
inputRef.current?.focus();
|
|
197
|
+
handleKeyDown(event);
|
|
198
|
+
}, style: requiredProxyStyle })), jsx(HiddenValues, { name: name, values: values, form: form, disabled: disabled }), jsx(PickerPopover, { localeScope: localeScope.panel, isOpen: isOpen, onClose: () => onOpenChange(false), anchorRef: rootRef, position: position, appendToBody: appendToBody, className: panelClasses, ariaLabel: panelName['aria-label'], ariaLabelledBy: panelName['aria-labelledby'], id: panelId, restoreFocusRef: inputRef, initialFocusSelector: initialFocusSelector, matchWidth: true, followContent: true, children: children(panelName) })] }));
|
|
199
|
+
const fieldElement = below === undefined ? (root) : (jsxs("div", { children: [root, below] }));
|
|
200
|
+
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
201
|
+
if (ownField) {
|
|
202
|
+
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fieldElement, messageEl] }));
|
|
203
|
+
}
|
|
204
|
+
return (jsxs(Fragment, { children: [fieldElement, messageEl] }));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { DialogSelect };
|
|
208
|
+
//# sourceMappingURL=DialogSelect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DialogSelect.js","sources":["../../../../src/form/_selectInternals/DialogSelect.tsx"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts. The field a picker\n// of tree data opens its panel from: TreeSelect's, and Cascader's after it.\n\nimport React, { useEffect, useId, useRef, useState } from 'react';\nimport { classNames, usePrefixedClassNames } from '../../helpers/classNames';\nimport {\n useBulmaClasses,\n type BulmaClassesProps,\n} from '../../helpers/useBulmaClasses';\nimport { useLatestRef } from '../../helpers/useLatestRef';\nimport { Delete } from '../../elements/Delete';\nimport { Icon } from '../../elements/Icon';\nimport { Control } from '../Control';\nimport { Field } from '../Field';\nimport type { FormFieldProps } from '../fieldProps';\nimport {\n rendersOwnField,\n useInsideControl,\n useInsideField,\n} from '../FormContext';\nimport { useAutoLabelId } from '../useAutoLabelId';\nimport { PickerPopover } from '../_pickerInternals/PickerPopover';\nimport {\n isPopoverToggleKey,\n type PickerPosition,\n} from '../_pickerInternals/pickerTypes';\nimport { usePickerLocaleScope } from '../_pickerInternals/useLocaleDefaults';\nimport { HiddenValues } from './ValueTags';\n\n/** What names the panel, for what's in it to take as well. */\nexport interface PanelName {\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\n/** Props for the field a tree picker opens its panel from. */\nexport interface DialogSelectProps\n extends\n Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'color' | 'onChange' | 'defaultValue' | 'children'\n >,\n Omit<BulmaClassesProps, 'color'>,\n FormFieldProps {\n /** The component's name, for development warnings. */\n component: string;\n /** The root's own class, before any prefix, such as `tree-select`. */\n rootClass: string;\n /** The panel's own class, beside `picker-popover`, before any prefix. */\n panelClass: string;\n /** What the field shows. */\n text: string;\n /** What `name` posts, a hidden input each. */\n values: string[];\n /** Something is picked: `required` is met, and the clear button shows. */\n hasValue: boolean;\n /** Whether the panel is open. */\n open: boolean;\n /**\n * Asked to open or close the panel: by a click on the field, its keys,\n * Escape, Alt+Up, a press outside and the clear button.\n */\n onOpenChange: (open: boolean) => void;\n /**\n * Called once a reset of the field's form has gone ahead, so the picker\n * can put its value back.\n */\n onFormReset: () => void;\n /**\n * A printable key on the field. Returns whether it used the key, as a\n * search box opening with it does.\n */\n onPrintableKey?: (key: string) => boolean;\n /** Empties the value, from the clear button. */\n onClear: () => void;\n /** Shows the clear button while something is picked. */\n clearable?: boolean;\n /** Names the clear button. */\n clearLabel: string;\n /** Names the panel in place of the field's label. */\n chooseLabel?: string;\n placeholder?: string;\n name?: string;\n form?: string;\n required?: boolean;\n /** Already including a disabled `Fieldset` around it. */\n disabled: boolean;\n color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n size?: 'small' | 'medium' | 'large';\n isRounded?: boolean;\n position?: PickerPosition;\n appendToBody?: boolean;\n /** Holds the field, which focus goes back to. */\n inputRef: React.RefObject<HTMLInputElement | null>;\n /**\n * Set on the field: the picker's callback that fills `inputRef` and the\n * ref its caller gave.\n */\n fieldRef: React.RefCallback<HTMLInputElement>;\n /** The ids of the tags that describe the field. */\n describedBy?: string;\n /** Shown under the field, inside its Field: the tags. */\n below?: React.ReactNode;\n /** Selects what takes focus as the panel opens. */\n initialFocusSelector?: string;\n /** The panel's content, handed the panel's name. */\n children: (name: PanelName) => React.ReactNode;\n}\n\n/**\n * A field laid over the real one, unseen and letting presses through, so the\n * browser's `required` check reads it and its message points at the field.\n */\nconst requiredProxyStyle: React.CSSProperties = {\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n margin: 0,\n padding: 0,\n border: 0,\n opacity: 0,\n pointerEvents: 'none',\n // Focused, it opens no on-screen keyboard (`inputMode`), and iOS zooms\n // in on a field set under 16px. It's unseen, so the size shows nowhere.\n fontSize: '16px',\n};\n\n/** Keys pressed on their own that don't act, such as Ctrl to stop speech. */\nconst MODIFIER_KEYS = new Set([\n 'Alt',\n 'AltGraph',\n 'CapsLock',\n 'Control',\n 'Fn',\n 'Meta',\n 'NumLock',\n 'ScrollLock',\n 'Shift',\n]);\n\nconst ignoreChange = () => {};\n\n/**\n * A read-only `role=\"combobox\"` field, wired as the date pickers' are, that\n * opens a `PickerPopover` dialog panel: in a `Field` of its own unless one is\n * around it, with a chevron at its inline end, a clear button, the hidden\n * inputs for `name` and what the browser's `required` check reads. A click,\n * Enter, Space, Down or Alt+Down opens the panel, which holds focus, as the\n * date pickers' does: Tab and Shift+Tab wrap inside it. Escape and Alt+Up\n * close it and put focus back on the field, and a press outside closes it\n * where the press put focus.\n */\nexport function DialogSelect({\n component,\n rootClass,\n panelClass,\n text,\n values,\n hasValue,\n open,\n onOpenChange,\n onFormReset,\n onPrintableKey,\n onClear,\n clearable,\n clearLabel,\n chooseLabel,\n placeholder,\n name,\n form,\n required = false,\n disabled,\n color,\n size,\n isRounded,\n position = 'bottom-left',\n appendToBody = false,\n inputRef,\n fieldRef,\n describedBy,\n below,\n initialFocusSelector,\n children,\n label,\n labelSize,\n labelProps,\n horizontal,\n message,\n messageColor,\n fieldClassName,\n className,\n id,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: DialogSelectProps) {\n const insideField = useInsideField();\n const insideControl = useInsideControl();\n const ownField = rendersOwnField(component, {\n insideField,\n insideControl,\n label,\n message,\n horizontal,\n fieldClassName,\n });\n const { controlId, fieldLabelProps, labelId } = useAutoLabelId({\n label,\n id,\n labelProps,\n rendersLabel: ownField,\n needsLabelId: true,\n });\n const reactId = useId();\n const panelId = `${controlId ?? reactId}-panel`;\n const localeScope = usePickerLocaleScope(undefined, props.dir);\n const { bulmaHelperClasses, rest } = useBulmaClasses(props);\n const rootRef = useRef<HTMLDivElement>(null);\n\n // The field's name, which the panel takes too unless it's given its own.\n const fieldName: PanelName = ariaLabel\n ? { 'aria-label': ariaLabel }\n : ariaLabelledBy\n ? { 'aria-labelledby': ariaLabelledBy }\n : labelId\n ? { 'aria-labelledby': labelId }\n : {};\n const panelName: PanelName = chooseLabel\n ? { 'aria-label': chooseLabel }\n : fieldName;\n\n // Whether the field `required` hangs on has focus, which the browser gives\n // it when it stops a submit. Taking it away or disabling it while it has\n // focus fires no blur, so that clears it too.\n const [proxyFocused, setProxyFocused] = useState(false);\n if (proxyFocused && (!required || disabled)) setProxyFocused(false);\n\n const showClear = !!clearable && hasValue && !disabled;\n const rootClasses = usePrefixedClassNames(rootClass, {\n 'is-disabled': disabled,\n 'has-clear': showClear,\n });\n const inputClass = usePrefixedClassNames('input', {\n [`is-${color}`]: !!color,\n [`is-${size}`]: !!size,\n 'is-rounded': !!isRounded,\n 'is-focused': proxyFocused,\n });\n const chevronClass = usePrefixedClassNames(`${rootClass}-chevron`);\n const clearClass = usePrefixedClassNames(`${rootClass}-clear`);\n const panelClasses = usePrefixedClassNames(panelClass, {\n [`is-${size}`]: !!size,\n });\n const helpClass = usePrefixedClassNames('help', {\n [`is-${messageColor}`]: !!messageColor,\n });\n const isOpen = open && !disabled;\n\n // A reset of the field's form puts the value back, as it does a native\n // field's. The reset event fires before the form resets, and a listener\n // after this one can still cancel it, so the value goes back a task later,\n // and only if the reset went ahead, as File's and Dropzone's do. It's\n // heard on the field's document, or the shadow root it's in, and matched\n // to the field's form as it is then, so a form its `form` names that\n // mounts later, or a `form` that changes, is followed, and a reset\n // already under way still lands.\n const onFormResetRef = useLatestRef(onFormReset);\n useEffect(() => {\n const root = inputRef.current!.getRootNode();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const onReset = (event: Event) => {\n if (event.target !== inputRef.current?.form) return;\n timer = setTimeout(() => {\n if (!event.defaultPrevented) onFormResetRef.current();\n });\n };\n root.addEventListener('reset', onReset, true);\n return () => {\n root.removeEventListener('reset', onReset, true);\n clearTimeout(timer);\n };\n }, [inputRef, onFormResetRef]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return;\n if (isPopoverToggleKey(event)) {\n event.preventDefault();\n if (event.key === 'ArrowDown') onOpenChange(true);\n return;\n }\n if (event.altKey || event.ctrlKey || event.metaKey) return;\n if (\n event.key === 'Enter' ||\n event.key === ' ' ||\n event.key === 'ArrowDown'\n ) {\n event.preventDefault();\n onOpenChange(true);\n } else if (\n event.key.length === 1 &&\n !event.nativeEvent.isComposing &&\n onPrintableKey?.(event.key)\n ) {\n event.preventDefault();\n }\n };\n\n const fieldDescription =\n [ariaDescribedBy, describedBy].filter(Boolean).join(' ') || undefined;\n\n const field = (\n <>\n <input\n ref={fieldRef}\n type=\"text\"\n className={inputClass}\n id={controlId}\n readOnly\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-expanded={isOpen}\n aria-controls={isOpen ? panelId : undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-describedby={fieldDescription}\n aria-required={required || undefined}\n autoComplete=\"off\"\n value={text}\n placeholder={placeholder}\n disabled={disabled}\n form={form}\n // A disabled field takes no clicks.\n onClick={() => onOpenChange(!isOpen)}\n onKeyDown={handleKeyDown}\n />\n {showClear && (\n <Delete\n className={clearClass}\n size={size}\n ariaLabel={clearLabel}\n onClick={() => {\n onClear();\n onOpenChange(false);\n inputRef.current?.focus();\n }}\n />\n )}\n <span className={chevronClass} aria-hidden=\"true\">\n <Icon name=\"chevron-down\" size={size} />\n </span>\n </>\n );\n\n const root = (\n <div\n {...localeScope.rootAttrs}\n {...rest}\n ref={rootRef}\n className={classNames(rootClasses, bulmaHelperClasses, className)}\n >\n {insideControl ? field : <Control size={size}>{field}</Control>}\n {required && (\n <input\n // Focus puts it in the accessibility tree, and Chromium won't\n // leave focus hidden, so it's hidden only until it's focused, and\n // then reads as the field, shut.\n {...(proxyFocused\n ? {\n ...fieldName,\n role: 'combobox',\n 'aria-haspopup': 'dialog' as const,\n 'aria-expanded': false,\n 'aria-describedby': fieldDescription,\n }\n : { 'aria-hidden': true })}\n tabIndex={-1}\n inputMode=\"none\"\n required\n value={hasValue ? 'picked' : ''}\n onChange={ignoreChange}\n form={form}\n disabled={disabled}\n // When the browser stops a submit, it focuses this and shows its\n // message beside it, and every browser takes the message down as\n // this loses focus. So focus stays here, over the field, which\n // looks focused, and a press goes where it lands, as from any\n // field. The next key hands focus to the field and does there\n // what it would have. Firefox also stops taking clicks if focus\n // leaves while it's still focusing this.\n onFocus={() => setProxyFocused(true)}\n onBlur={() => setProxyFocused(false)}\n onKeyDown={event => {\n if (MODIFIER_KEYS.has(event.key)) return;\n inputRef.current?.focus();\n handleKeyDown(event);\n }}\n style={requiredProxyStyle}\n />\n )}\n <HiddenValues\n name={name}\n values={values}\n form={form}\n disabled={disabled}\n />\n <PickerPopover\n localeScope={localeScope.panel}\n isOpen={isOpen}\n onClose={() => onOpenChange(false)}\n anchorRef={rootRef}\n position={position}\n appendToBody={appendToBody}\n className={panelClasses}\n ariaLabel={panelName['aria-label']}\n ariaLabelledBy={panelName['aria-labelledby']}\n id={panelId}\n restoreFocusRef={inputRef}\n initialFocusSelector={initialFocusSelector}\n matchWidth\n followContent\n >\n {children(panelName)}\n </PickerPopover>\n </div>\n );\n\n const fieldElement =\n below === undefined ? (\n root\n ) : (\n <div>\n {root}\n {below}\n </div>\n );\n const messageEl = message ? <p className={helpClass}>{message}</p> : null;\n\n if (ownField) {\n return (\n <Field\n label={label}\n labelSize={labelSize}\n labelProps={fieldLabelProps}\n horizontal={horizontal}\n className={fieldClassName}\n >\n {fieldElement}\n {messageEl}\n </Field>\n );\n }\n return (\n <>\n {fieldElement}\n {messageEl}\n </>\n );\n}\n"],"names":["_jsxs","_Fragment","_jsx"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
3
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import { Delete } from '../../elements/Delete.js';
|
|
5
|
+
import { Tag } from '../../elements/Tag.js';
|
|
6
|
+
import { Tags } from '../../elements/Tags.js';
|
|
7
|
+
|
|
8
|
+
const everyValue = () => true;
|
|
9
|
+
/**
|
|
10
|
+
* The values of a field holding several, as Bulma tags under it, each with a
|
|
11
|
+
* `Delete` that takes its value off. Nothing while there are none.
|
|
12
|
+
*/
|
|
13
|
+
function ValueTags({ values, ids, labelFor, disabled, ariaCloseLabel, onRemove, removable = everyValue, }) {
|
|
14
|
+
if (values.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
return (jsx(Tags, { mt: "2", children: values.map((value, index) => (jsxs(Tag, { children: [jsx("span", { id: ids[index], children: labelFor(value) }), !disabled && removable(value) && (jsx(Delete, { size: "small", ariaLabel: ariaCloseLabel || `Remove ${labelFor(value)}`, "aria-describedby": ariaCloseLabel ? ids[index] : undefined, onClick: () => onRemove(value) }))] }, value))) }));
|
|
17
|
+
}
|
|
18
|
+
/** The ids `ValueTags` gives its labels, built from one id of the field's. */
|
|
19
|
+
const tagIds = (base, values) => values.map((_, index) => `${base}-${index}`);
|
|
20
|
+
/** A hidden input for each value, so a form posts them under `name`. */
|
|
21
|
+
function HiddenValues({ name, values, form, disabled, }) {
|
|
22
|
+
if (!name)
|
|
23
|
+
return null;
|
|
24
|
+
return (jsx(Fragment, { children: values.map(value => (jsx("input", { type: "hidden", name: name, value: value, form: form, disabled: disabled }, value))) }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { HiddenValues, ValueTags, tagIds };
|
|
28
|
+
//# sourceMappingURL=ValueTags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ValueTags.js","sources":["../../../../src/form/_selectInternals/ValueTags.tsx"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts. The parts a field\n// holding values shows and posts them with, shared by Combobox and TreeSelect.\n\nimport { Delete } from '../../elements/Delete';\nimport { Tag } from '../../elements/Tag';\nimport { Tags } from '../../elements/Tags';\n\n/** Props for the tags of a field holding several values. */\nexport interface ValueTagsProps {\n /** The values, in the order they show. */\n values: string[];\n /** The id of each tag's label, from `tagIds`, which the field's description lists. */\n ids: string[];\n /** What a value shows. */\n labelFor: (value: string) => string;\n /** The field can't be used, so the tags have no buttons. */\n disabled?: boolean;\n /**\n * Names each button in place of \"Remove\" and the value's label. The\n * button is then described by its tag's label, so it still says which.\n */\n ariaCloseLabel?: string;\n /** Called with a value when its tag's button is pressed. */\n onRemove: (value: string) => void;\n /** Whether a value's tag has a button. Every tag has one without it. */\n removable?: (value: string) => boolean;\n}\n\nconst everyValue = () => true;\n\n/**\n * The values of a field holding several, as Bulma tags under it, each with a\n * `Delete` that takes its value off. Nothing while there are none.\n */\nexport function ValueTags({\n values,\n ids,\n labelFor,\n disabled,\n ariaCloseLabel,\n onRemove,\n removable = everyValue,\n}: ValueTagsProps) {\n if (values.length === 0) return null;\n return (\n <Tags mt=\"2\">\n {values.map((value, index) => (\n <Tag key={value}>\n <span id={ids[index]}>{labelFor(value)}</span>\n {!disabled && removable(value) && (\n <Delete\n size=\"small\"\n ariaLabel={ariaCloseLabel || `Remove ${labelFor(value)}`}\n aria-describedby={ariaCloseLabel ? ids[index] : undefined}\n onClick={() => onRemove(value)}\n />\n )}\n </Tag>\n ))}\n </Tags>\n );\n}\n\n/** The ids `ValueTags` gives its labels, built from one id of the field's. */\nexport const tagIds = (base: string, values: string[]): string[] =>\n values.map((_, index) => `${base}-${index}`);\n\n/** Props for the hidden inputs that post a field's values. */\nexport interface HiddenValuesProps {\n /** The form field name. Nothing renders without one. */\n name?: string;\n /** One hidden input each. */\n values: string[];\n /** The id of the form they post with, for a field outside it. */\n form?: string;\n /** They don't post, as a disabled field's value doesn't. */\n disabled?: boolean;\n}\n\n/** A hidden input for each value, so a form posts them under `name`. */\nexport function HiddenValues({\n name,\n values,\n form,\n disabled,\n}: HiddenValuesProps) {\n if (!name) return null;\n return (\n <>\n {values.map(value => (\n <input\n key={value}\n type=\"hidden\"\n name={name}\n value={value}\n form={form}\n disabled={disabled}\n />\n ))}\n </>\n );\n}\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":";;;;;;;AA4BA,MAAM,UAAU,GAAG,MAAM,IAAI;AAE7B;;;AAGG;SACa,SAAS,CAAC,EACxB,MAAM,EACN,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,GAAG,UAAU,GACP,EAAA;AACf,IAAA,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,IAAI;IACpC,QACEA,IAAC,IAAI,EAAA,EAAC,EAAE,EAAC,GAAG,YACT,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,MACvBC,IAAA,CAAC,GAAG,eACFD,GAAA,CAAA,MAAA,EAAA,EAAM,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,EAAA,QAAA,EAAG,QAAQ,CAAC,KAAK,CAAC,EAAA,CAAQ,EAC7C,CAAC,QAAQ,IAAI,SAAS,CAAC,KAAK,CAAC,KAC5BA,GAAA,CAAC,MAAM,EAAA,EACL,IAAI,EAAC,OAAO,EACZ,SAAS,EAAE,cAAc,IAAI,CAAA,OAAA,EAAU,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAA,kBAAA,EACtC,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,EACzD,OAAO,EAAE,MAAM,QAAQ,CAAC,KAAK,CAAC,EAAA,CAC9B,CACH,CAAA,EAAA,EATO,KAAK,CAUT,CACP,CAAC,EAAA,CACG;AAEX;AAEA;AACO,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,MAAgB,KACnD,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAE;AAc7C;AACM,SAAU,YAAY,CAAC,EAC3B,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,QAAQ,GACU,EAAA;AAClB,IAAA,IAAI,CAAC,IAAI;AAAE,QAAA,OAAO,IAAI;AACtB,IAAA,QACEA,GAAA,CAAAE,QAAA,EAAA,EAAA,QAAA,EACG,MAAM,CAAC,GAAG,CAAC,KAAK,KACfF,GAAA,CAAA,OAAA,EAAA,EAEE,IAAI,EAAC,QAAQ,EACb,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,IALb,KAAK,CAMV,CACH,CAAC,EAAA,CACD;AAEP;;"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { readChecks } from './useTreeView.js';
|
|
3
|
+
|
|
4
|
+
// INTERNAL: deliberately not exported from src/index.ts. Walking the nodes a
|
|
5
|
+
// TreeSelect takes as `data`, so a picker can name, check and search a tree
|
|
6
|
+
// while it isn't on the page.
|
|
7
|
+
/**
|
|
8
|
+
* Every node by its value, in the order the tree reads from top to bottom.
|
|
9
|
+
* Values are meant to be unique, and the first node with a value wins.
|
|
10
|
+
*/
|
|
11
|
+
function indexTree(data) {
|
|
12
|
+
const index = new Map();
|
|
13
|
+
const walk = (nodes, path) => {
|
|
14
|
+
for (const node of nodes) {
|
|
15
|
+
if (!index.has(node.value))
|
|
16
|
+
index.set(node.value, { node, path });
|
|
17
|
+
if (node.children)
|
|
18
|
+
walk(node.children, [...path, node.value]);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
walk(data, []);
|
|
22
|
+
return index;
|
|
23
|
+
}
|
|
24
|
+
/** What a node is called: its `label`, or its `value` when that's empty. */
|
|
25
|
+
const nodeLabel = (node) => node.label || node.value;
|
|
26
|
+
/** The nodes under a node, none when it has no `children`. */
|
|
27
|
+
const childNodes = (node) => node.children ?? [];
|
|
28
|
+
/** Whether a node is a parent: it has children, or `hasChildren` says it will. */
|
|
29
|
+
const isParentNode = (node) => childNodes(node).length > 0 || !!node.hasChildren;
|
|
30
|
+
/**
|
|
31
|
+
* The records TreeView's check rules read, built from the nodes rather than
|
|
32
|
+
* from the items on the page, so the checked list can be worked out while
|
|
33
|
+
* the tree isn't there.
|
|
34
|
+
*/
|
|
35
|
+
function checkRecords(index) {
|
|
36
|
+
const records = new Map();
|
|
37
|
+
index.forEach(({ node, path }, value) => records.set(value, {
|
|
38
|
+
parent: path[path.length - 1] ?? null,
|
|
39
|
+
disabled: !!node.disabled,
|
|
40
|
+
isParent: isParentNode(node),
|
|
41
|
+
}));
|
|
42
|
+
return records;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The checked list a `checkedStrategy` reports for the items `listed` checks
|
|
46
|
+
* by TreeView's rules, in the tree's order. `all` is every checked item;
|
|
47
|
+
* `child` the checked items with no enabled item under them; `parent` the
|
|
48
|
+
* checked items whose parent isn't. A disabled item is checked by its own
|
|
49
|
+
* value only, so a checked one is in every list. Values no node has are
|
|
50
|
+
* kept, at the end. Passed back in, each list checks the same items.
|
|
51
|
+
*/
|
|
52
|
+
function checkedByStrategy(records, listed, strategy) {
|
|
53
|
+
const { states, ends } = readChecks(records, new Set(listed));
|
|
54
|
+
const isEnd = new Set(ends);
|
|
55
|
+
const on = (value) => value !== null && states.get(value) === true;
|
|
56
|
+
const kept = [];
|
|
57
|
+
records.forEach(({ parent, disabled }, value) => {
|
|
58
|
+
if (!on(value))
|
|
59
|
+
return;
|
|
60
|
+
if (strategy === 'all' ||
|
|
61
|
+
disabled ||
|
|
62
|
+
(strategy === 'child' ? isEnd.has(value) : !on(parent))) {
|
|
63
|
+
kept.push(value);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const unknown = new Set(listed.filter(value => !records.has(value)));
|
|
67
|
+
return [...kept, ...unknown];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What a search shows of a tree: the nodes that match, the nodes above them,
|
|
71
|
+
* so the results stay a tree, and the nodes under them, so a matching branch
|
|
72
|
+
* can still be opened.
|
|
73
|
+
*/
|
|
74
|
+
function searchTree(data, matches) {
|
|
75
|
+
const hidden = new Set();
|
|
76
|
+
const open = [];
|
|
77
|
+
// Whether the nodes, or any node under them, match. `above` is the path
|
|
78
|
+
// down to them.
|
|
79
|
+
const walk = (nodes, underMatch, above) => {
|
|
80
|
+
let found = false;
|
|
81
|
+
for (const node of nodes) {
|
|
82
|
+
const path = [...above, node];
|
|
83
|
+
const self = matches(node, path);
|
|
84
|
+
const below = walk(childNodes(node), underMatch || self, path);
|
|
85
|
+
if (below)
|
|
86
|
+
open.push(node.value);
|
|
87
|
+
if (!underMatch && !self && !below)
|
|
88
|
+
hidden.add(node.value);
|
|
89
|
+
found = found || self || below;
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
};
|
|
93
|
+
return { hidden, open, found: walk(data, false, []) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { checkRecords, checkedByStrategy, childNodes, indexTree, isParentNode, nodeLabel, searchTree };
|
|
97
|
+
//# sourceMappingURL=treeData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"treeData.js","sources":["../../../src/helpers/treeData.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts. Walking the nodes a\n// TreeSelect takes as `data`, so a picker can name, check and search a tree\n// while it isn't on the page.\n\nimport type {\n TreeItemData,\n TreeSelectCheckedStrategy,\n} from '../form/TreeSelect';\nimport { readChecks, type TreeViewItemRecord } from './useTreeView';\n\n/** A node, and the values of the nodes above it, from the top. */\nexport interface TreeNodeEntry {\n node: TreeItemData;\n path: string[];\n}\n\n/**\n * Every node by its value, in the order the tree reads from top to bottom.\n * Values are meant to be unique, and the first node with a value wins.\n */\nexport function indexTree(data: TreeItemData[]): Map<string, TreeNodeEntry> {\n const index = new Map<string, TreeNodeEntry>();\n const walk = (nodes: TreeItemData[], path: string[]) => {\n for (const node of nodes) {\n if (!index.has(node.value)) index.set(node.value, { node, path });\n if (node.children) walk(node.children, [...path, node.value]);\n }\n };\n walk(data, []);\n return index;\n}\n\n/** What a node is called: its `label`, or its `value` when that's empty. */\nexport const nodeLabel = (node: TreeItemData): string =>\n node.label || node.value;\n\n/** The nodes under a node, none when it has no `children`. */\nexport const childNodes = (node: TreeItemData): TreeItemData[] =>\n node.children ?? [];\n\n/** Whether a node is a parent: it has children, or `hasChildren` says it will. */\nexport const isParentNode = (node: TreeItemData): boolean =>\n childNodes(node).length > 0 || !!node.hasChildren;\n\n/**\n * The records TreeView's check rules read, built from the nodes rather than\n * from the items on the page, so the checked list can be worked out while\n * the tree isn't there.\n */\nexport function checkRecords(\n index: Map<string, TreeNodeEntry>\n): Map<string, TreeViewItemRecord> {\n const records = new Map<string, TreeViewItemRecord>();\n index.forEach(({ node, path }, value) =>\n records.set(value, {\n parent: path[path.length - 1] ?? null,\n disabled: !!node.disabled,\n isParent: isParentNode(node),\n })\n );\n return records;\n}\n\n/**\n * The checked list a `checkedStrategy` reports for the items `listed` checks\n * by TreeView's rules, in the tree's order. `all` is every checked item;\n * `child` the checked items with no enabled item under them; `parent` the\n * checked items whose parent isn't. A disabled item is checked by its own\n * value only, so a checked one is in every list. Values no node has are\n * kept, at the end. Passed back in, each list checks the same items.\n */\nexport function checkedByStrategy(\n records: Map<string, TreeViewItemRecord>,\n listed: string[],\n strategy: TreeSelectCheckedStrategy\n): string[] {\n const { states, ends } = readChecks(records, new Set(listed));\n const isEnd = new Set(ends);\n const on = (value: string | null) =>\n value !== null && states.get(value) === true;\n const kept: string[] = [];\n records.forEach(({ parent, disabled }, value) => {\n if (!on(value)) return;\n if (\n strategy === 'all' ||\n disabled ||\n (strategy === 'child' ? isEnd.has(value) : !on(parent))\n ) {\n kept.push(value);\n }\n });\n const unknown = new Set(listed.filter(value => !records.has(value)));\n return [...kept, ...unknown];\n}\n\n/** What a search leaves of a tree. */\nexport interface TreeSearch {\n /** The values of the nodes it hides. */\n hidden: Set<string>;\n /** The values of the nodes with a match under them, which open. */\n open: string[];\n /** Whether anything matched. */\n found: boolean;\n}\n\n/**\n * What a search shows of a tree: the nodes that match, the nodes above them,\n * so the results stay a tree, and the nodes under them, so a matching branch\n * can still be opened.\n */\nexport function searchTree(\n data: TreeItemData[],\n matches: (node: TreeItemData, path: TreeItemData[]) => boolean\n): TreeSearch {\n const hidden = new Set<string>();\n const open: string[] = [];\n // Whether the nodes, or any node under them, match. `above` is the path\n // down to them.\n const walk = (\n nodes: TreeItemData[],\n underMatch: boolean,\n above: TreeItemData[]\n ): boolean => {\n let found = false;\n for (const node of nodes) {\n const path = [...above, node];\n const self = matches(node, path);\n const below = walk(childNodes(node), underMatch || self, path);\n if (below) open.push(node.value);\n if (!underMatch && !self && !below) hidden.add(node.value);\n found = found || self || below;\n }\n return found;\n };\n return { hidden, open, found: walk(data, false, []) };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ import { getActiveElementInTree, isEventInside } from './shadowDom.js';
|
|
|
7
7
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
8
8
|
/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
|
|
9
9
|
const isComposing$2 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
|
|
10
|
-
const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
10
|
+
const sameValues$1 = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
11
11
|
/**
|
|
12
12
|
* The keys, the open state, the highlight and the ARIA of an editable
|
|
13
13
|
* combobox whose list the app renders and filters (#1083). DOM focus stays
|
|
@@ -109,7 +109,7 @@ function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, on
|
|
|
109
109
|
}
|
|
110
110
|
const options = shownOptions();
|
|
111
111
|
const values = options.map(option => option.value);
|
|
112
|
-
const changed = !sameValues(values, shownValuesRef.current);
|
|
112
|
+
const changed = !sameValues$1(values, shownValuesRef.current);
|
|
113
113
|
shownValuesRef.current = values;
|
|
114
114
|
setListShown(values.length > 0);
|
|
115
115
|
const landing = landingRef.current ?? (opened ? 'open' : null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useComboboxEngine.js","sources":["../../../src/helpers/useComboboxEngine.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport {\n useCallback,\n useEffect,\n useId,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport type React from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getActiveElementInTree, isEventInside } from './shadowDom';\n\n/** What an item tells the engine about itself. */\nexport interface ComboboxItemState {\n /** What a pick hands back. Unique within the combobox. */\n value: string;\n /** Reachable with the arrows, never picked. */\n disabled: boolean;\n /** The value, or one of them. */\n checked: boolean;\n}\n\n/** An option on the page, with its element. */\ninterface ShownOption extends ComboboxItemState {\n element: HTMLElement;\n}\n\n/**\n * Where the highlight lands once an opened list has rendered, or once typing\n * has changed what's in it:\n *\n * - `first` and `last` (Down and Up Arrow): the checked option, otherwise the\n * first or last enabled one;\n * - `open` (a click, or any other way of opening): the checked option,\n * otherwise the first enabled one with `keepFirst`, otherwise nothing;\n * - `typed`: the first enabled option with `keepFirst`, otherwise nothing;\n * - `none` (Alt+Down Arrow): nothing.\n *\n * Only an enabled option takes it, so a disabled checked one is passed over.\n */\ntype Landing = 'first' | 'last' | 'open' | 'typed' | 'none';\n\n/** Options for `useComboboxEngine`. */\nexport interface ComboboxEngineOptions {\n /** Controlled open state. When set, opening and closing only report through `onOpenChange`. */\n open?: boolean;\n /** Initial open state when uncontrolled. */\n defaultOpen: boolean;\n /** Called with the open state asked for, when it differs from the current one. */\n onOpenChange?: (open: boolean) => void;\n /** Called with an item's `value` when it's picked. */\n onSelect?: (value: string) => void;\n /**\n * Called when Enter goes on to the form with nothing to pick, before the\n * list closes, so an owner that shows something other than the typing can\n * put it back before the form reads the field. An `inline` list doesn't\n * close, so this is the one word it gets.\n */\n onSettle?: () => void;\n /** Highlights the first enabled item whenever the list opens or its items change. */\n keepFirst: boolean;\n /** Several values: a pick keeps the list open. */\n multiple: boolean;\n /** The list is always on screen, so nothing opens or closes it. */\n inline: boolean;\n /**\n * The list's panel is portaled, so it has no server markup and shows only\n * from the commit after hydration. Until it has rendered, an open list\n * isn't taken to be on the page, so no `aria-controls` names it early.\n */\n deferred: boolean;\n}\n\n/** What `useComboboxEngine` hands the parts of a combobox. */\nexport interface ComboboxEngine {\n /** Whether the list is open. Always, with `inline`. */\n open: boolean;\n /**\n * The field's `aria-expanded`: open, with a listbox that has an option in\n * it on the page. Until the open list has rendered, it's taken to have\n * one, so server markup for an open list in place says it's expanded. A\n * `deferred` list isn't in server markup, so it's taken to have none.\n */\n expanded: boolean;\n /** The highlighted option's value, if any. */\n highlighted: string | null;\n /** Whether the user has typed since the list opened. */\n typed: boolean;\n /** The listbox's id. */\n listboxId: string;\n /** An item's id, which follows from its value. */\n itemId: (value: string) => string;\n /** For the element around the field and, unless it's portaled, the list. */\n rootRef: React.MutableRefObject<HTMLDivElement | null>;\n /** For the field. */\n inputRef: React.MutableRefObject<HTMLInputElement | null>;\n /** For the listbox. */\n listboxRef: React.MutableRefObject<HTMLDivElement | null>;\n /**\n * Records the floating panel while it's on screen, which makes it count as\n * inside, and makes an Escape that closes it stop there. Returns the\n * function that forgets it.\n */\n registerPopup: (element: HTMLElement) => () => void;\n /** Records an item's element. Returns the function that forgets it. */\n registerItem: (element: HTMLElement, state: ComboboxItemState) => () => void;\n /** The field's keys, to run after the caller's own handler. */\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;\n /** A change to the field's value: the user typed. */\n onChange: () => void;\n /** A click in the field. */\n onClick: () => void;\n /** Focus leaving the field. */\n onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;\n /** The pointer moved over an enabled option. */\n hover: (value: string) => void;\n /** A click on an enabled option. Focus ends up in the field. */\n pick: (value: string) => void;\n /** For anything in the list: a press there doesn't take focus from the field. */\n keepFocus: (event: React.MouseEvent) => void;\n /**\n * For the floating panel: marks a press as inside, which React reports from\n * content the panel portals elsewhere as well.\n */\n markInside: (event: React.PointerEvent) => void;\n}\n\n/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */\nconst isComposing = (event: React.KeyboardEvent): boolean =>\n event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;\n\nconst sameValues = (a: string[], b: string[]): boolean =>\n a.length === b.length && a.every((value, i) => value === b[i]);\n\n/**\n * The keys, the open state, the highlight and the ARIA of an editable\n * combobox whose list the app renders and filters (#1083). DOM focus stays\n * in the field the whole time: the highlight is `aria-activedescendant`, as\n * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves\n * real focus, as a menu should.\n *\n * The options are read from the listbox in document order whenever they're\n * needed, so options in groups and options that come and go are found, and\n * the highlight is remembered by value, so it survives a re-render. The text\n * in the field and any value are the app's: a pick only calls\n * `onSelect`.\n *\n * @function useComboboxEngine\n * @param options - The open state, the pick callback and the shape.\n * @returns The state and handlers the parts wire up.\n */\nexport function useComboboxEngine({\n open: controlledOpen,\n defaultOpen,\n onOpenChange,\n onSelect,\n onSettle,\n keepFirst,\n multiple,\n inline,\n deferred,\n}: ComboboxEngineOptions): ComboboxEngine {\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const itemPrefix = `${baseId}-item-`;\n // The value goes into the id encoded, so a value with a space in it still\n // makes one id, not two.\n const itemId = useCallback(\n (value: string) => itemPrefix + encodeURIComponent(value),\n [itemPrefix]\n );\n\n const rootRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useRef<HTMLElement | null>(null);\n const insidePressRef = useRef<Event | null>(null);\n const optionsRef = useRef(new Map<HTMLElement, ComboboxItemState>());\n const landingRef = useRef<Landing | null>(null);\n const wasOpenRef = useRef(false);\n const shownValuesRef = useRef<string[]>([]);\n // Whether the next highlight change is one to scroll to: a key's or a\n // landing's. The pointer's isn't, since the pointer is already on that\n // option, and scrolling would move the list out from under it.\n const scrollToHighlightRef = useRef(false);\n\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = inline || (controlledOpen ?? internalOpen);\n\n const [highlighted, setHighlighted] = useState<string | null>(null);\n const [typed, setTyped] = useState(false);\n // Whether a listbox with an option in it is on the page, which only the\n // page can say, so it's read once the open list has rendered.\n const [listShown, setListShown] = useState<boolean | null>(null);\n // Options register from their own effects, which needn't render the\n // component that holds this hook, so they ask for a render to be read.\n const [, refresh] = useReducer((n: number) => n + 1, 0);\n\n // Closing forgets the highlight, the typing and what was on the page,\n // during render, React's pattern for adjusting state when other state\n // changes.\n if (!open) {\n if (highlighted !== null) setHighlighted(null);\n if (typed) setTyped(false);\n if (listShown !== null) setListShown(null);\n }\n const expanded = open && (listShown ?? !deferred);\n\n /** The options on the page, in document order, groups included. */\n const shownOptions = useCallback((): ShownOption[] => {\n const listbox = listboxRef.current;\n if (!listbox) return [];\n const shown: ShownOption[] = [];\n listbox\n .querySelectorAll<HTMLElement>('[role=\"option\"]')\n .forEach(element => {\n // Only the Combobox's own options: an element given the role by\n // hand isn't one.\n const state = optionsRef.current.get(element);\n if (state) shown.push({ element, ...state });\n });\n return shown;\n }, []);\n\n const requestOpen = useCallback(\n (next: boolean, landing: Landing = 'open') => {\n if (inline || next === open) return;\n landingRef.current = next ? landing : null;\n onOpenChange?.(next);\n if (!isControlled) setInternalOpen(next);\n },\n [inline, open, onOpenChange, isControlled]\n );\n const close = useCallback(() => requestOpen(false), [requestOpen]);\n\n // After every render, read the list: whether it has options, and where\n // the highlight goes now. A highlighted option that has left takes the\n // highlight with it, or hands it to the first enabled one under\n // `keepFirst`, which also moves it there whenever the options change.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose\n useClientLayoutEffect(() => {\n const opened = open && !wasOpenRef.current;\n wasOpenRef.current = open;\n if (!open) {\n landingRef.current = null;\n shownValuesRef.current = [];\n return;\n }\n const options = shownOptions();\n const values = options.map(option => option.value);\n const changed = !sameValues(values, shownValuesRef.current);\n shownValuesRef.current = values;\n setListShown(values.length > 0);\n\n const landing = landingRef.current ?? (opened ? 'open' : null);\n landingRef.current = null;\n const enabled = options.filter(option => !option.disabled);\n const checked = enabled.find(option => option.checked)?.value;\n const first = enabled.length > 0 ? enabled[0].value : null;\n const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;\n let next = highlighted;\n if (landing === 'first') next = checked ?? first;\n else if (landing === 'last') next = checked ?? last;\n else if (landing === 'open') next = checked ?? (keepFirst ? first : null);\n else if (landing === 'typed') next = keepFirst ? first : null;\n else if (landing === 'none') next = null;\n else if (keepFirst && changed) next = first;\n // Without `keepFirst`, which moved it above, a highlighted option\n // that left takes the highlight with it.\n else if (next !== null && !values.includes(next)) next = null;\n // Only a change is set. StrictMode runs this twice on mount, and the\n // second run, with the landing used up, works out the rendered highlight\n // again, so it leaves the first run's landing in place.\n if (next !== highlighted) {\n scrollToHighlightRef.current = true;\n setHighlighted(next);\n }\n });\n\n // A browser scrolls to what has focus, not to an active descendant, so\n // the highlighted option is brought into view here, when a key or a\n // landing moved it there.\n useClientLayoutEffect(() => {\n if (highlighted === null || !scrollToHighlightRef.current) return;\n scrollToHighlightRef.current = false;\n const option = shownOptions().find(shown => shown.value === highlighted);\n option?.element.scrollIntoView?.({ block: 'nearest' });\n }, [highlighted, shownOptions]);\n\n // A press outside the field and the list closes it. The path is checked\n // rather than the target, so it works in a shadow root.\n useEffect(() => {\n const root = rootRef.current;\n if (!open || inline || !root) return undefined;\n const onPress = (event: PointerEvent) => {\n if (\n event === insidePressRef.current ||\n isEventInside(event, rootRef.current) ||\n isEventInside(event, popupRef.current)\n ) {\n return;\n }\n close();\n };\n // Its document, an iframe's when the tree renders into one.\n const doc = root.ownerDocument;\n doc.addEventListener('pointerdown', onPress);\n return () => doc.removeEventListener('pointerdown', onPress);\n }, [open, inline, close]);\n\n const submit = useCallback(\n (value: string) => {\n onSelect?.(value);\n if (!multiple) close();\n },\n [onSelect, multiple, close]\n );\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n // The caller's handler ran first and can keep a key, and an IME that's\n // composing owns every key.\n if (event.defaultPrevented || isComposing(event)) return;\n const { key, altKey } = event;\n\n if (!open) {\n if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {\n event.preventDefault();\n requestOpen(\n true,\n altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last'\n );\n }\n return;\n }\n\n switch (key) {\n case 'ArrowDown':\n case 'ArrowUp': {\n if (altKey) {\n if (key === 'ArrowUp' && !inline) {\n event.preventDefault();\n close();\n }\n return;\n }\n event.preventDefault();\n // Disabled options too, so a screen reader user finds them.\n const options = shownOptions();\n if (options.length === 0) return;\n const at = options.findIndex(option => option.value === highlighted);\n const step = key === 'ArrowDown' ? 1 : -1;\n const index =\n at === -1\n ? step === 1\n ? 0\n : options.length - 1\n : (at + step + options.length) % options.length;\n scrollToHighlightRef.current = true;\n setHighlighted(options[index].value);\n return;\n }\n case 'Enter': {\n const option = shownOptions().find(\n shown => shown.value === highlighted\n );\n if (option) {\n event.preventDefault();\n if (!option.disabled) submit(option.value);\n return;\n }\n // Nothing to pick: the key goes on, so a form submits.\n onSettle?.();\n close();\n return;\n }\n case 'Escape':\n if (inline) return;\n // It stops here only when there was something to close, so a\n // Popover or Modal around it keeps its own Escape otherwise. Nothing\n // stops it going on: a handler further out reads `defaultPrevented`.\n if (popupRef.current) event.preventDefault();\n close();\n return;\n case 'Tab':\n close();\n return;\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n // The field's own keys, which take the field back from the list.\n setHighlighted(null);\n return;\n default:\n }\n };\n\n const onChange = () => {\n setTyped(true);\n if (open) {\n landingRef.current = 'typed';\n setHighlighted(null);\n refresh();\n } else {\n requestOpen(true, 'typed');\n }\n };\n\n const onClick = () => {\n requestOpen(true, 'open');\n };\n\n const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n const next = event.relatedTarget as Node | null;\n if (\n next &&\n (rootRef.current?.contains(next) || popupRef.current?.contains(next))\n ) {\n return;\n }\n close();\n };\n\n const hover = (value: string) => {\n scrollToHighlightRef.current = false;\n setHighlighted(value);\n };\n\n const pick = (value: string) => {\n submit(value);\n // A press in the list keeps focus in the field, but a browser that\n // ignores that, or a tap, can leave it elsewhere.\n const input = inputRef.current;\n if (input && input.isConnected && getActiveElementInTree(input) !== input) {\n input.focus();\n }\n };\n\n const keepFocus = (event: React.MouseEvent) => event.preventDefault();\n\n const markInside = (event: React.PointerEvent) => {\n insidePressRef.current = event.nativeEvent;\n };\n\n const registerPopup = useCallback((element: HTMLElement) => {\n popupRef.current = element;\n return () => {\n popupRef.current = null;\n };\n }, []);\n\n const registerItem = useCallback(\n (element: HTMLElement, state: ComboboxItemState) => {\n const options = optionsRef.current;\n options.set(element, state);\n refresh();\n return () => {\n options.delete(element);\n refresh();\n };\n },\n []\n );\n\n return {\n open,\n expanded,\n highlighted,\n typed,\n listboxId,\n itemId,\n rootRef,\n inputRef,\n listboxRef,\n registerPopup,\n registerItem,\n onKeyDown,\n onChange,\n onClick,\n onBlur,\n hover,\n pick,\n keepFocus,\n markInside,\n };\n}\n"],"names":["isComposing"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useComboboxEngine.js","sources":["../../../src/helpers/useComboboxEngine.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport {\n useCallback,\n useEffect,\n useId,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport type React from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getActiveElementInTree, isEventInside } from './shadowDom';\n\n/** What an item tells the engine about itself. */\nexport interface ComboboxItemState {\n /** What a pick hands back. Unique within the combobox. */\n value: string;\n /** Reachable with the arrows, never picked. */\n disabled: boolean;\n /** The value, or one of them. */\n checked: boolean;\n}\n\n/** An option on the page, with its element. */\ninterface ShownOption extends ComboboxItemState {\n element: HTMLElement;\n}\n\n/**\n * Where the highlight lands once an opened list has rendered, or once typing\n * has changed what's in it:\n *\n * - `first` and `last` (Down and Up Arrow): the checked option, otherwise the\n * first or last enabled one;\n * - `open` (a click, or any other way of opening): the checked option,\n * otherwise the first enabled one with `keepFirst`, otherwise nothing;\n * - `typed`: the first enabled option with `keepFirst`, otherwise nothing;\n * - `none` (Alt+Down Arrow): nothing.\n *\n * Only an enabled option takes it, so a disabled checked one is passed over.\n */\ntype Landing = 'first' | 'last' | 'open' | 'typed' | 'none';\n\n/** Options for `useComboboxEngine`. */\nexport interface ComboboxEngineOptions {\n /** Controlled open state. When set, opening and closing only report through `onOpenChange`. */\n open?: boolean;\n /** Initial open state when uncontrolled. */\n defaultOpen: boolean;\n /** Called with the open state asked for, when it differs from the current one. */\n onOpenChange?: (open: boolean) => void;\n /** Called with an item's `value` when it's picked. */\n onSelect?: (value: string) => void;\n /**\n * Called when Enter goes on to the form with nothing to pick, before the\n * list closes, so an owner that shows something other than the typing can\n * put it back before the form reads the field. An `inline` list doesn't\n * close, so this is the one word it gets.\n */\n onSettle?: () => void;\n /** Highlights the first enabled item whenever the list opens or its items change. */\n keepFirst: boolean;\n /** Several values: a pick keeps the list open. */\n multiple: boolean;\n /** The list is always on screen, so nothing opens or closes it. */\n inline: boolean;\n /**\n * The list's panel is portaled, so it has no server markup and shows only\n * from the commit after hydration. Until it has rendered, an open list\n * isn't taken to be on the page, so no `aria-controls` names it early.\n */\n deferred: boolean;\n}\n\n/** What `useComboboxEngine` hands the parts of a combobox. */\nexport interface ComboboxEngine {\n /** Whether the list is open. Always, with `inline`. */\n open: boolean;\n /**\n * The field's `aria-expanded`: open, with a listbox that has an option in\n * it on the page. Until the open list has rendered, it's taken to have\n * one, so server markup for an open list in place says it's expanded. A\n * `deferred` list isn't in server markup, so it's taken to have none.\n */\n expanded: boolean;\n /** The highlighted option's value, if any. */\n highlighted: string | null;\n /** Whether the user has typed since the list opened. */\n typed: boolean;\n /** The listbox's id. */\n listboxId: string;\n /** An item's id, which follows from its value. */\n itemId: (value: string) => string;\n /** For the element around the field and, unless it's portaled, the list. */\n rootRef: React.MutableRefObject<HTMLDivElement | null>;\n /** For the field. */\n inputRef: React.MutableRefObject<HTMLInputElement | null>;\n /** For the listbox. */\n listboxRef: React.MutableRefObject<HTMLDivElement | null>;\n /**\n * Records the floating panel while it's on screen, which makes it count as\n * inside, and makes an Escape that closes it stop there. Returns the\n * function that forgets it.\n */\n registerPopup: (element: HTMLElement) => () => void;\n /** Records an item's element. Returns the function that forgets it. */\n registerItem: (element: HTMLElement, state: ComboboxItemState) => () => void;\n /** The field's keys, to run after the caller's own handler. */\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;\n /** A change to the field's value: the user typed. */\n onChange: () => void;\n /** A click in the field. */\n onClick: () => void;\n /** Focus leaving the field. */\n onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;\n /** The pointer moved over an enabled option. */\n hover: (value: string) => void;\n /** A click on an enabled option. Focus ends up in the field. */\n pick: (value: string) => void;\n /** For anything in the list: a press there doesn't take focus from the field. */\n keepFocus: (event: React.MouseEvent) => void;\n /**\n * For the floating panel: marks a press as inside, which React reports from\n * content the panel portals elsewhere as well.\n */\n markInside: (event: React.PointerEvent) => void;\n}\n\n/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */\nconst isComposing = (event: React.KeyboardEvent): boolean =>\n event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;\n\nconst sameValues = (a: string[], b: string[]): boolean =>\n a.length === b.length && a.every((value, i) => value === b[i]);\n\n/**\n * The keys, the open state, the highlight and the ARIA of an editable\n * combobox whose list the app renders and filters (#1083). DOM focus stays\n * in the field the whole time: the highlight is `aria-activedescendant`, as\n * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves\n * real focus, as a menu should.\n *\n * The options are read from the listbox in document order whenever they're\n * needed, so options in groups and options that come and go are found, and\n * the highlight is remembered by value, so it survives a re-render. The text\n * in the field and any value are the app's: a pick only calls\n * `onSelect`.\n *\n * @function useComboboxEngine\n * @param options - The open state, the pick callback and the shape.\n * @returns The state and handlers the parts wire up.\n */\nexport function useComboboxEngine({\n open: controlledOpen,\n defaultOpen,\n onOpenChange,\n onSelect,\n onSettle,\n keepFirst,\n multiple,\n inline,\n deferred,\n}: ComboboxEngineOptions): ComboboxEngine {\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const itemPrefix = `${baseId}-item-`;\n // The value goes into the id encoded, so a value with a space in it still\n // makes one id, not two.\n const itemId = useCallback(\n (value: string) => itemPrefix + encodeURIComponent(value),\n [itemPrefix]\n );\n\n const rootRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useRef<HTMLElement | null>(null);\n const insidePressRef = useRef<Event | null>(null);\n const optionsRef = useRef(new Map<HTMLElement, ComboboxItemState>());\n const landingRef = useRef<Landing | null>(null);\n const wasOpenRef = useRef(false);\n const shownValuesRef = useRef<string[]>([]);\n // Whether the next highlight change is one to scroll to: a key's or a\n // landing's. The pointer's isn't, since the pointer is already on that\n // option, and scrolling would move the list out from under it.\n const scrollToHighlightRef = useRef(false);\n\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = inline || (controlledOpen ?? internalOpen);\n\n const [highlighted, setHighlighted] = useState<string | null>(null);\n const [typed, setTyped] = useState(false);\n // Whether a listbox with an option in it is on the page, which only the\n // page can say, so it's read once the open list has rendered.\n const [listShown, setListShown] = useState<boolean | null>(null);\n // Options register from their own effects, which needn't render the\n // component that holds this hook, so they ask for a render to be read.\n const [, refresh] = useReducer((n: number) => n + 1, 0);\n\n // Closing forgets the highlight, the typing and what was on the page,\n // during render, React's pattern for adjusting state when other state\n // changes.\n if (!open) {\n if (highlighted !== null) setHighlighted(null);\n if (typed) setTyped(false);\n if (listShown !== null) setListShown(null);\n }\n const expanded = open && (listShown ?? !deferred);\n\n /** The options on the page, in document order, groups included. */\n const shownOptions = useCallback((): ShownOption[] => {\n const listbox = listboxRef.current;\n if (!listbox) return [];\n const shown: ShownOption[] = [];\n listbox\n .querySelectorAll<HTMLElement>('[role=\"option\"]')\n .forEach(element => {\n // Only the Combobox's own options: an element given the role by\n // hand isn't one.\n const state = optionsRef.current.get(element);\n if (state) shown.push({ element, ...state });\n });\n return shown;\n }, []);\n\n const requestOpen = useCallback(\n (next: boolean, landing: Landing = 'open') => {\n if (inline || next === open) return;\n landingRef.current = next ? landing : null;\n onOpenChange?.(next);\n if (!isControlled) setInternalOpen(next);\n },\n [inline, open, onOpenChange, isControlled]\n );\n const close = useCallback(() => requestOpen(false), [requestOpen]);\n\n // After every render, read the list: whether it has options, and where\n // the highlight goes now. A highlighted option that has left takes the\n // highlight with it, or hands it to the first enabled one under\n // `keepFirst`, which also moves it there whenever the options change.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose\n useClientLayoutEffect(() => {\n const opened = open && !wasOpenRef.current;\n wasOpenRef.current = open;\n if (!open) {\n landingRef.current = null;\n shownValuesRef.current = [];\n return;\n }\n const options = shownOptions();\n const values = options.map(option => option.value);\n const changed = !sameValues(values, shownValuesRef.current);\n shownValuesRef.current = values;\n setListShown(values.length > 0);\n\n const landing = landingRef.current ?? (opened ? 'open' : null);\n landingRef.current = null;\n const enabled = options.filter(option => !option.disabled);\n const checked = enabled.find(option => option.checked)?.value;\n const first = enabled.length > 0 ? enabled[0].value : null;\n const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;\n let next = highlighted;\n if (landing === 'first') next = checked ?? first;\n else if (landing === 'last') next = checked ?? last;\n else if (landing === 'open') next = checked ?? (keepFirst ? first : null);\n else if (landing === 'typed') next = keepFirst ? first : null;\n else if (landing === 'none') next = null;\n else if (keepFirst && changed) next = first;\n // Without `keepFirst`, which moved it above, a highlighted option\n // that left takes the highlight with it.\n else if (next !== null && !values.includes(next)) next = null;\n // Only a change is set. StrictMode runs this twice on mount, and the\n // second run, with the landing used up, works out the rendered highlight\n // again, so it leaves the first run's landing in place.\n if (next !== highlighted) {\n scrollToHighlightRef.current = true;\n setHighlighted(next);\n }\n });\n\n // A browser scrolls to what has focus, not to an active descendant, so\n // the highlighted option is brought into view here, when a key or a\n // landing moved it there.\n useClientLayoutEffect(() => {\n if (highlighted === null || !scrollToHighlightRef.current) return;\n scrollToHighlightRef.current = false;\n const option = shownOptions().find(shown => shown.value === highlighted);\n option?.element.scrollIntoView?.({ block: 'nearest' });\n }, [highlighted, shownOptions]);\n\n // A press outside the field and the list closes it. The path is checked\n // rather than the target, so it works in a shadow root.\n useEffect(() => {\n const root = rootRef.current;\n if (!open || inline || !root) return undefined;\n const onPress = (event: PointerEvent) => {\n if (\n event === insidePressRef.current ||\n isEventInside(event, rootRef.current) ||\n isEventInside(event, popupRef.current)\n ) {\n return;\n }\n close();\n };\n // Its document, an iframe's when the tree renders into one.\n const doc = root.ownerDocument;\n doc.addEventListener('pointerdown', onPress);\n return () => doc.removeEventListener('pointerdown', onPress);\n }, [open, inline, close]);\n\n const submit = useCallback(\n (value: string) => {\n onSelect?.(value);\n if (!multiple) close();\n },\n [onSelect, multiple, close]\n );\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n // The caller's handler ran first and can keep a key, and an IME that's\n // composing owns every key.\n if (event.defaultPrevented || isComposing(event)) return;\n const { key, altKey } = event;\n\n if (!open) {\n if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {\n event.preventDefault();\n requestOpen(\n true,\n altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last'\n );\n }\n return;\n }\n\n switch (key) {\n case 'ArrowDown':\n case 'ArrowUp': {\n if (altKey) {\n if (key === 'ArrowUp' && !inline) {\n event.preventDefault();\n close();\n }\n return;\n }\n event.preventDefault();\n // Disabled options too, so a screen reader user finds them.\n const options = shownOptions();\n if (options.length === 0) return;\n const at = options.findIndex(option => option.value === highlighted);\n const step = key === 'ArrowDown' ? 1 : -1;\n const index =\n at === -1\n ? step === 1\n ? 0\n : options.length - 1\n : (at + step + options.length) % options.length;\n scrollToHighlightRef.current = true;\n setHighlighted(options[index].value);\n return;\n }\n case 'Enter': {\n const option = shownOptions().find(\n shown => shown.value === highlighted\n );\n if (option) {\n event.preventDefault();\n if (!option.disabled) submit(option.value);\n return;\n }\n // Nothing to pick: the key goes on, so a form submits.\n onSettle?.();\n close();\n return;\n }\n case 'Escape':\n if (inline) return;\n // It stops here only when there was something to close, so a\n // Popover or Modal around it keeps its own Escape otherwise. Nothing\n // stops it going on: a handler further out reads `defaultPrevented`.\n if (popupRef.current) event.preventDefault();\n close();\n return;\n case 'Tab':\n close();\n return;\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n // The field's own keys, which take the field back from the list.\n setHighlighted(null);\n return;\n default:\n }\n };\n\n const onChange = () => {\n setTyped(true);\n if (open) {\n landingRef.current = 'typed';\n setHighlighted(null);\n refresh();\n } else {\n requestOpen(true, 'typed');\n }\n };\n\n const onClick = () => {\n requestOpen(true, 'open');\n };\n\n const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n const next = event.relatedTarget as Node | null;\n if (\n next &&\n (rootRef.current?.contains(next) || popupRef.current?.contains(next))\n ) {\n return;\n }\n close();\n };\n\n const hover = (value: string) => {\n scrollToHighlightRef.current = false;\n setHighlighted(value);\n };\n\n const pick = (value: string) => {\n submit(value);\n // A press in the list keeps focus in the field, but a browser that\n // ignores that, or a tap, can leave it elsewhere.\n const input = inputRef.current;\n if (input && input.isConnected && getActiveElementInTree(input) !== input) {\n input.focus();\n }\n };\n\n const keepFocus = (event: React.MouseEvent) => event.preventDefault();\n\n const markInside = (event: React.PointerEvent) => {\n insidePressRef.current = event.nativeEvent;\n };\n\n const registerPopup = useCallback((element: HTMLElement) => {\n popupRef.current = element;\n return () => {\n popupRef.current = null;\n };\n }, []);\n\n const registerItem = useCallback(\n (element: HTMLElement, state: ComboboxItemState) => {\n const options = optionsRef.current;\n options.set(element, state);\n refresh();\n return () => {\n options.delete(element);\n refresh();\n };\n },\n []\n );\n\n return {\n open,\n expanded,\n highlighted,\n typed,\n listboxId,\n itemId,\n rootRef,\n inputRef,\n listboxRef,\n registerPopup,\n registerItem,\n onKeyDown,\n onChange,\n onClick,\n onBlur,\n hover,\n pick,\n keepFocus,\n markInside,\n };\n}\n"],"names":["isComposing","sameValues"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
6
|
+
/**
|
|
7
|
+
* A value the caller holds, or one this hook holds when the caller doesn't:
|
|
8
|
+
* the `x` / `defaultX` pair of a component's props. The caller holds it
|
|
9
|
+
* whenever `controlled` isn't `undefined`, so `null` is a held value. The
|
|
10
|
+
* setter changes only the value held here, so a held one changes when the
|
|
11
|
+
* caller passes the new value, and it reports nothing: the component calls
|
|
12
|
+
* its own `onXChange`.
|
|
13
|
+
*
|
|
14
|
+
* @function useControlled
|
|
15
|
+
* @param controlled - The caller's value, or `undefined` to hold one here.
|
|
16
|
+
* @param initial - The value held here to begin with.
|
|
17
|
+
* @returns The current value, and the setter for the one held here.
|
|
18
|
+
*/
|
|
19
|
+
function useControlled(controlled, initial) {
|
|
20
|
+
const [own, setOwn] = useState(initial);
|
|
21
|
+
const isControlled = controlled !== undefined;
|
|
22
|
+
const set = (next) => {
|
|
23
|
+
if (!isControlled)
|
|
24
|
+
setOwn(next);
|
|
25
|
+
};
|
|
26
|
+
return [isControlled ? controlled : own, set];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { useControlled };
|
|
30
|
+
//# sourceMappingURL=useControlled.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useControlled.js","sources":["../../../src/helpers/useControlled.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useState } from 'react';\n\n/**\n * A value the caller holds, or one this hook holds when the caller doesn't:\n * the `x` / `defaultX` pair of a component's props. The caller holds it\n * whenever `controlled` isn't `undefined`, so `null` is a held value. The\n * setter changes only the value held here, so a held one changes when the\n * caller passes the new value, and it reports nothing: the component calls\n * its own `onXChange`.\n *\n * @function useControlled\n * @param controlled - The caller's value, or `undefined` to hold one here.\n * @param initial - The value held here to begin with.\n * @returns The current value, and the setter for the one held here.\n */\nexport function useControlled<T>(controlled: T | undefined, initial: T) {\n const [own, setOwn] = useState(initial);\n const isControlled = controlled !== undefined;\n const set = (next: T) => {\n if (!isControlled) setOwn(next);\n };\n return [isControlled ? controlled : own, set] as const;\n}\n"],"names":[],"mappings":";;;;AAAA;AAGA;;;;;;;;;;;;AAYG;AACG,SAAU,aAAa,CAAI,UAAyB,EAAE,OAAU,EAAA;IACpE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;AACvC,IAAA,MAAM,YAAY,GAAG,UAAU,KAAK,SAAS;AAC7C,IAAA,MAAM,GAAG,GAAG,CAAC,IAAO,KAAI;AACtB,QAAA,IAAI,CAAC,YAAY;YAAE,MAAM,CAAC,IAAI,CAAC;AACjC,IAAA,CAAC;AACD,IAAA,OAAO,CAAC,YAAY,GAAG,UAAU,GAAG,GAAG,EAAE,GAAG,CAAU;AACxD;;"}
|