@allxsmith/bestax-bulma 5.60.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/Carousel.js +3 -43
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/components/Command.js +3 -3
- package/dist/esm/components/Navbar.js +3 -1
- package/dist/esm/components/Navbar.js.map +1 -1
- package/dist/esm/components/Popover.js +7 -27
- package/dist/esm/components/Popover.js.map +1 -1
- package/dist/esm/components/Sidebar.js +17 -58
- package/dist/esm/components/Sidebar.js.map +1 -1
- package/dist/esm/components/TreeView.js +14 -10
- package/dist/esm/components/TreeView.js.map +1 -1
- package/dist/esm/form/ColorPicker.js +4 -4
- package/dist/esm/form/ColorPicker.js.map +1 -1
- package/dist/esm/form/Combobox.js +11 -16
- 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/overlayStack.js +33 -1
- package/dist/esm/helpers/overlayStack.js.map +1 -1
- package/dist/esm/helpers/treeData.js +97 -0
- package/dist/esm/helpers/treeData.js.map +1 -0
- package/dist/esm/helpers/useBulmaViewport.js +82 -0
- package/dist/esm/helpers/useBulmaViewport.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/useInertSiblings.js +7 -3
- package/dist/esm/helpers/useInertSiblings.js.map +1 -1
- package/dist/esm/helpers/useOutsidePress.js +53 -0
- package/dist/esm/helpers/useOutsidePress.js.map +1 -0
- package/dist/esm/helpers/useOverlayFocus.js +74 -0
- package/dist/esm/helpers/useOverlayFocus.js.map +1 -0
- package/dist/esm/helpers/useTreeView.js +15 -13
- package/dist/esm/helpers/useTreeView.js.map +1 -1
- package/dist/esm/layout/AppFrame.js +438 -0
- package/dist/esm/layout/AppFrame.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +1767 -518
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Navbar.d.ts +4 -3
- 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/overlayStack.d.ts +17 -0
- package/dist/types/helpers/treeData.d.ts +48 -0
- package/dist/types/helpers/useBulmaViewport.d.ts +34 -0
- package/dist/types/helpers/useControlled.d.ts +14 -0
- package/dist/types/helpers/useInertSiblings.d.ts +4 -1
- package/dist/types/helpers/useOutsidePress.d.ts +24 -0
- package/dist/types/helpers/useOverlayFocus.d.ts +44 -0
- package/dist/types/helpers/useTreeView.d.ts +30 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/layout/AppFrame.d.ts +306 -0
- package/dist/types-cjs/components/Navbar.d.ts +4 -3
- 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/overlayStack.d.ts +17 -0
- package/dist/types-cjs/helpers/treeData.d.ts +48 -0
- package/dist/types-cjs/helpers/useBulmaViewport.d.ts +34 -0
- package/dist/types-cjs/helpers/useControlled.d.ts +14 -0
- package/dist/types-cjs/helpers/useInertSiblings.d.ts +4 -1
- package/dist/types-cjs/helpers/useOutsidePress.d.ts +24 -0
- package/dist/types-cjs/helpers/useOverlayFocus.d.ts +44 -0
- package/dist/types-cjs/helpers/useTreeView.d.ts +30 -0
- package/dist/types-cjs/index.d.ts +2 -0
- package/dist/types-cjs/layout/AppFrame.d.ts +306 -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
- package/src/scss/layout/_appframe.scss +306 -0
- package/src/scss/layout/_index.scss +1 -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;;"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
2
|
'use client';
|
|
3
3
|
import { useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useLatestRef } from './useLatestRef.js';
|
|
4
5
|
|
|
5
6
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
6
7
|
/**
|
|
@@ -64,6 +65,37 @@ function useOverlayStack(inStack, modal = true) {
|
|
|
64
65
|
return !above.some(overlay => among !== 'modal' || overlay.modal);
|
|
65
66
|
}, []);
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Calls `onEscape` for an Escape while `active` and the overlay is the
|
|
70
|
+
* topmost one, as `isTopmost` (from `useOverlayStack`) says, and marks the key
|
|
71
|
+
* used with `preventDefault()`, so nothing under it acts on it too.
|
|
72
|
+
*
|
|
73
|
+
* It listens on `window`, so everything inside the overlay has had the key
|
|
74
|
+
* first, and an Escape something there used, such as a popup closing, is
|
|
75
|
+
* left to it. The listener stays put while `onEscape` changes identity, as an
|
|
76
|
+
* inline one does on every render of the parent: it reads the latest one as
|
|
77
|
+
* the key arrives.
|
|
78
|
+
*
|
|
79
|
+
* @function useOverlayEscape
|
|
80
|
+
* @param active - Whether the overlay is open and closes on Escape.
|
|
81
|
+
* @param isTopmost - The overlay's check from `useOverlayStack`.
|
|
82
|
+
* @param onEscape - What closing it does.
|
|
83
|
+
*/
|
|
84
|
+
function useOverlayEscape(active, isTopmost, onEscape) {
|
|
85
|
+
const onEscapeRef = useLatestRef(onEscape);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!active)
|
|
88
|
+
return undefined;
|
|
89
|
+
const handleKeyDown = (e) => {
|
|
90
|
+
if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost())
|
|
91
|
+
return;
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
onEscapeRef.current();
|
|
94
|
+
};
|
|
95
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
96
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
97
|
+
}, [active, isTopmost, onEscapeRef]);
|
|
98
|
+
}
|
|
67
99
|
|
|
68
|
-
export { isModalOverlayOpen, useOverlayStack };
|
|
100
|
+
export { isModalOverlayOpen, useOverlayEscape, useOverlayStack };
|
|
69
101
|
//# sourceMappingURL=overlayStack.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlayStack.js","sources":["../../../src/helpers/overlayStack.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useCallback, useEffect, useRef } from 'react';\n\n/** An open overlay's place in the stack. */\ninterface OpenOverlay {\n /**\n * Whether it shuts off the page under it, as a Modal does, so it holds Tab\n * rather than leaving it to an overlay under it. A Sidebar without an\n * overlay doesn't.\n */\n modal: boolean;\n}\n\n/**\n * The open overlays that take part, in the order they opened. Only the last\n * one answers Escape, so one keypress closes one layer, and only the last\n * modal one holds Tab. It's touched in effects and key handlers, never while\n * rendering, so a server render leaves it alone.\n */\nconst openOverlays: OpenOverlay[] = [];\n\n/**\n * Whether a modal overlay is open anywhere in the stack. One that isn't modal\n * and opens while one is leaves focus where it is, since the modal one keeps\n * Tab and a Tab from the new one's controls would only go back to it.\n *\n * @function isModalOverlayOpen\n * @returns Whether any open overlay in the stack is modal.\n */\nexport const isModalOverlayOpen = (): boolean =>\n openOverlays.some(overlay => overlay.modal);\n\n/**\n * Holds this overlay's place in the stack of open overlays while `inStack`\n * is true, from the effect after it opens until the one after it closes.\n * Opening again puts it back on top.\n *\n * Returns a check for whether nothing in the stack sits above this overlay,\n * to call when a key arrives. For an overlay in the stack, that's whether it\n * is the topmost one. For one that isn't, such as a Toast, it's whether no\n * overlay is open at all. Asked about `'modal'` overlays, it counts only the\n * modal ones, which is the check for Tab: a modal overlay keeps holding Tab\n * with one that isn't modal open over it, while that one takes Escape.\n *\n * @function useOverlayStack\n * @param inStack - Whether this overlay is open and takes part.\n * @param modal - Whether this overlay is modal, which it is unless it says\n * otherwise: a Sidebar passes whether it has an overlay. It can change while\n * the overlay is open, and its place stays where it is.\n * @returns A check for whether nothing in the stack, or no modal overlay, is\n * above this overlay.\n */\nexport function useOverlayStack(\n inStack: boolean,\n modal = true\n): (among?: 'modal') => boolean {\n const tokenRef = useRef<OpenOverlay>({ modal });\n\n useEffect(() => {\n tokenRef.current.modal = modal;\n }, [modal]);\n\n useEffect(() => {\n if (!inStack) return undefined;\n const token = tokenRef.current;\n openOverlays.push(token);\n // Takes out this effect's own push. Should it ever be gone already, the\n // stack is left as it is: splicing at -1 would take the topmost overlay\n // instead.\n return () => {\n const index = openOverlays.indexOf(token);\n if (index !== -1) openOverlays.splice(index, 1);\n };\n }, [inStack]);\n\n return useCallback((among?: 'modal') => {\n // Everything above this overlay, or the whole stack for one not in it.\n const above = openOverlays.slice(\n openOverlays.indexOf(tokenRef.current) + 1\n );\n return !above.some(overlay => among !== 'modal' || overlay.modal);\n }, []);\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"overlayStack.js","sources":["../../../src/helpers/overlayStack.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useCallback, useEffect, useRef } from 'react';\nimport { useLatestRef } from './useLatestRef';\n\n/** An open overlay's place in the stack. */\ninterface OpenOverlay {\n /**\n * Whether it shuts off the page under it, as a Modal does, so it holds Tab\n * rather than leaving it to an overlay under it. A Sidebar without an\n * overlay doesn't.\n */\n modal: boolean;\n}\n\n/**\n * The open overlays that take part, in the order they opened. Only the last\n * one answers Escape, so one keypress closes one layer, and only the last\n * modal one holds Tab. It's touched in effects and key handlers, never while\n * rendering, so a server render leaves it alone.\n */\nconst openOverlays: OpenOverlay[] = [];\n\n/**\n * Whether a modal overlay is open anywhere in the stack. One that isn't modal\n * and opens while one is leaves focus where it is, since the modal one keeps\n * Tab and a Tab from the new one's controls would only go back to it.\n *\n * @function isModalOverlayOpen\n * @returns Whether any open overlay in the stack is modal.\n */\nexport const isModalOverlayOpen = (): boolean =>\n openOverlays.some(overlay => overlay.modal);\n\n/**\n * Holds this overlay's place in the stack of open overlays while `inStack`\n * is true, from the effect after it opens until the one after it closes.\n * Opening again puts it back on top.\n *\n * Returns a check for whether nothing in the stack sits above this overlay,\n * to call when a key arrives. For an overlay in the stack, that's whether it\n * is the topmost one. For one that isn't, such as a Toast, it's whether no\n * overlay is open at all. Asked about `'modal'` overlays, it counts only the\n * modal ones, which is the check for Tab: a modal overlay keeps holding Tab\n * with one that isn't modal open over it, while that one takes Escape.\n *\n * @function useOverlayStack\n * @param inStack - Whether this overlay is open and takes part.\n * @param modal - Whether this overlay is modal, which it is unless it says\n * otherwise: a Sidebar passes whether it has an overlay. It can change while\n * the overlay is open, and its place stays where it is.\n * @returns A check for whether nothing in the stack, or no modal overlay, is\n * above this overlay.\n */\nexport function useOverlayStack(\n inStack: boolean,\n modal = true\n): (among?: 'modal') => boolean {\n const tokenRef = useRef<OpenOverlay>({ modal });\n\n useEffect(() => {\n tokenRef.current.modal = modal;\n }, [modal]);\n\n useEffect(() => {\n if (!inStack) return undefined;\n const token = tokenRef.current;\n openOverlays.push(token);\n // Takes out this effect's own push. Should it ever be gone already, the\n // stack is left as it is: splicing at -1 would take the topmost overlay\n // instead.\n return () => {\n const index = openOverlays.indexOf(token);\n if (index !== -1) openOverlays.splice(index, 1);\n };\n }, [inStack]);\n\n return useCallback((among?: 'modal') => {\n // Everything above this overlay, or the whole stack for one not in it.\n const above = openOverlays.slice(\n openOverlays.indexOf(tokenRef.current) + 1\n );\n return !above.some(overlay => among !== 'modal' || overlay.modal);\n }, []);\n}\n\n/**\n * Calls `onEscape` for an Escape while `active` and the overlay is the\n * topmost one, as `isTopmost` (from `useOverlayStack`) says, and marks the key\n * used with `preventDefault()`, so nothing under it acts on it too.\n *\n * It listens on `window`, so everything inside the overlay has had the key\n * first, and an Escape something there used, such as a popup closing, is\n * left to it. The listener stays put while `onEscape` changes identity, as an\n * inline one does on every render of the parent: it reads the latest one as\n * the key arrives.\n *\n * @function useOverlayEscape\n * @param active - Whether the overlay is open and closes on Escape.\n * @param isTopmost - The overlay's check from `useOverlayStack`.\n * @param onEscape - What closing it does.\n */\nexport function useOverlayEscape(\n active: boolean,\n isTopmost: () => boolean,\n onEscape: () => void\n): void {\n const onEscapeRef = useLatestRef(onEscape);\n\n useEffect(() => {\n if (!active) return undefined;\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key !== 'Escape' || e.defaultPrevented || !isTopmost()) return;\n e.preventDefault();\n onEscapeRef.current();\n };\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [active, isTopmost, onEscapeRef]);\n}\n"],"names":[],"mappings":";;;;;AAAA;AAcA;;;;;AAKG;AACH,MAAM,YAAY,GAAkB,EAAE;AAEtC;;;;;;;AAOG;AACI,MAAM,kBAAkB,GAAG,MAChC,YAAY,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK;AAE5C;;;;;;;;;;;;;;;;;;;AAmBG;SACa,eAAe,CAC7B,OAAgB,EAChB,KAAK,GAAG,IAAI,EAAA;IAEZ,MAAM,QAAQ,GAAG,MAAM,CAAc,EAAE,KAAK,EAAE,CAAC;IAE/C,SAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK;AAChC,IAAA,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IAEX,SAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,SAAS;AAC9B,QAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;AAC9B,QAAA,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;;;;AAIxB,QAAA,OAAO,MAAK;YACV,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC;YACzC,IAAI,KAAK,KAAK,EAAE;AAAE,gBAAA,YAAY,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;AACjD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AAEb,IAAA,OAAO,WAAW,CAAC,CAAC,KAAe,KAAI;;AAErC,QAAA,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAC9B,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAC3C;AACD,QAAA,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,KAAK,KAAK,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC;IACnE,CAAC,EAAE,EAAE,CAAC;AACR;AAEA;;;;;;;;;;;;;;;AAeG;SACa,gBAAgB,CAC9B,MAAe,EACf,SAAwB,EACxB,QAAoB,EAAA;AAEpB,IAAA,MAAM,WAAW,GAAG,YAAY,CAAC,QAAQ,CAAC;IAE1C,SAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,MAAM;AAAE,YAAA,OAAO,SAAS;AAC7B,QAAA,MAAM,aAAa,GAAG,CAAC,CAAgB,KAAI;AACzC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,gBAAgB,IAAI,CAAC,SAAS,EAAE;gBAAE;YAC9D,CAAC,CAAC,cAAc,EAAE;YAClB,WAAW,CAAC,OAAO,EAAE;AACvB,QAAA,CAAC;AACD,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC;QACjD,OAAO,MAAM,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC;IACnE,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AACtC;;"}
|
|
@@ -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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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
import { useClientLayoutEffect } from './useClientLayoutEffect.js';
|
|
5
|
+
|
|
6
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
7
|
+
/**
|
|
8
|
+
* The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
|
|
9
|
+
* mixins open with, widest first, at Bulma's default breakpoints. A
|
|
10
|
+
* stylesheet lays a component out with those mixins; these tell its script
|
|
11
|
+
* which layout that is. A theme that moves Bulma's breakpoints moves the
|
|
12
|
+
* stylesheet's switch but not these. Narrower than the last one is `mobile`.
|
|
13
|
+
*/
|
|
14
|
+
const VIEWPORT_QUERIES = [
|
|
15
|
+
'screen and (min-width: 1408px)',
|
|
16
|
+
'screen and (min-width: 1216px)',
|
|
17
|
+
'screen and (min-width: 1024px)',
|
|
18
|
+
'screen and (min-width: 769px), print',
|
|
19
|
+
];
|
|
20
|
+
/** `useBulmaViewport`'s answer below the tablet breakpoint. */
|
|
21
|
+
const MOBILE = VIEWPORT_QUERIES.length;
|
|
22
|
+
/** Where Bulma's `widescreen`, `desktop` and `tablet` sit in `VIEWPORT_QUERIES`. */
|
|
23
|
+
const VIEWPORT_INDEX = {
|
|
24
|
+
widescreen: 1,
|
|
25
|
+
desktop: 2,
|
|
26
|
+
tablet: 3,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Which of Bulma's viewports the window is in: the index of the widest
|
|
30
|
+
* `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
|
|
31
|
+
* when that isn't known (on the server, before the first layout effect, and
|
|
32
|
+
* whenever `enabled` is off, so a caller that needs no answer subscribes to
|
|
33
|
+
* nothing). It's read in a layout effect, so the first paint after hydration
|
|
34
|
+
* already has it.
|
|
35
|
+
*
|
|
36
|
+
* @function useBulmaViewport
|
|
37
|
+
* @param enabled - Whether the caller needs the viewport at all.
|
|
38
|
+
* @returns The viewport, as described above.
|
|
39
|
+
*/
|
|
40
|
+
function useBulmaViewport(enabled) {
|
|
41
|
+
const [viewport, setViewport] = useState(-1);
|
|
42
|
+
useClientLayoutEffect(() => {
|
|
43
|
+
if (!enabled || typeof window.matchMedia !== 'function')
|
|
44
|
+
return undefined;
|
|
45
|
+
const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
|
|
46
|
+
const update = () => {
|
|
47
|
+
const index = lists.findIndex(list => list.matches);
|
|
48
|
+
setViewport(index < 0 ? MOBILE : index);
|
|
49
|
+
};
|
|
50
|
+
update();
|
|
51
|
+
lists.forEach(list => list.addEventListener('change', update));
|
|
52
|
+
return () => lists.forEach(list => list.removeEventListener('change', update));
|
|
53
|
+
}, [enabled]);
|
|
54
|
+
return enabled ? viewport : -1;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether the window is at or past one of Bulma's breakpoints, at its default
|
|
58
|
+
* width, through the one media query Bulma's mixin for it opens with, so a
|
|
59
|
+
* caller re-renders only when the window crosses that breakpoint. `null`
|
|
60
|
+
* until it's known: on the server, and before the first layout effect. Where
|
|
61
|
+
* `matchMedia` isn't there, it's `true`, the wide layout a server renders.
|
|
62
|
+
*
|
|
63
|
+
* @function useBulmaBreakpoint
|
|
64
|
+
* @param breakpoint - The breakpoint: `tablet`, `desktop` or `widescreen`.
|
|
65
|
+
* @returns Whether the window is at or past it, or `null`.
|
|
66
|
+
*/
|
|
67
|
+
function useBulmaBreakpoint(breakpoint) {
|
|
68
|
+
const [atOrPast, setAtOrPast] = useState(null);
|
|
69
|
+
useClientLayoutEffect(() => {
|
|
70
|
+
const list = typeof window.matchMedia === 'function'
|
|
71
|
+
? window.matchMedia(VIEWPORT_QUERIES[VIEWPORT_INDEX[breakpoint]])
|
|
72
|
+
: null;
|
|
73
|
+
const update = () => setAtOrPast(list ? list.matches : true);
|
|
74
|
+
update();
|
|
75
|
+
list?.addEventListener('change', update);
|
|
76
|
+
return () => list?.removeEventListener('change', update);
|
|
77
|
+
}, [breakpoint]);
|
|
78
|
+
return atOrPast;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { MOBILE, useBulmaBreakpoint, useBulmaViewport };
|
|
82
|
+
//# sourceMappingURL=useBulmaViewport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBulmaViewport.js","sources":["../../../src/helpers/useBulmaViewport.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useState } from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\n\n/**\n * The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`\n * mixins open with, widest first, at Bulma's default breakpoints. A\n * stylesheet lays a component out with those mixins; these tell its script\n * which layout that is. A theme that moves Bulma's breakpoints moves the\n * stylesheet's switch but not these. Narrower than the last one is `mobile`.\n */\nconst VIEWPORT_QUERIES = [\n 'screen and (min-width: 1408px)',\n 'screen and (min-width: 1216px)',\n 'screen and (min-width: 1024px)',\n 'screen and (min-width: 769px), print',\n];\n\n/** `useBulmaViewport`'s answer below the tablet breakpoint. */\nexport const MOBILE = VIEWPORT_QUERIES.length;\n\n/** Where Bulma's `widescreen`, `desktop` and `tablet` sit in `VIEWPORT_QUERIES`. */\nconst VIEWPORT_INDEX = {\n widescreen: 1,\n desktop: 2,\n tablet: 3,\n} as const;\n\n/**\n * Which of Bulma's viewports the window is in: the index of the widest\n * `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`\n * when that isn't known (on the server, before the first layout effect, and\n * whenever `enabled` is off, so a caller that needs no answer subscribes to\n * nothing). It's read in a layout effect, so the first paint after hydration\n * already has it.\n *\n * @function useBulmaViewport\n * @param enabled - Whether the caller needs the viewport at all.\n * @returns The viewport, as described above.\n */\nexport function useBulmaViewport(enabled: boolean): number {\n const [viewport, setViewport] = useState(-1);\n\n useClientLayoutEffect(() => {\n if (!enabled || typeof window.matchMedia !== 'function') return undefined;\n const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));\n const update = () => {\n const index = lists.findIndex(list => list.matches);\n setViewport(index < 0 ? MOBILE : index);\n };\n update();\n lists.forEach(list => list.addEventListener('change', update));\n return () =>\n lists.forEach(list => list.removeEventListener('change', update));\n }, [enabled]);\n\n return enabled ? viewport : -1;\n}\n\n/**\n * Whether the window is at or past one of Bulma's breakpoints, at its default\n * width, through the one media query Bulma's mixin for it opens with, so a\n * caller re-renders only when the window crosses that breakpoint. `null`\n * until it's known: on the server, and before the first layout effect. Where\n * `matchMedia` isn't there, it's `true`, the wide layout a server renders.\n *\n * @function useBulmaBreakpoint\n * @param breakpoint - The breakpoint: `tablet`, `desktop` or `widescreen`.\n * @returns Whether the window is at or past it, or `null`.\n */\nexport function useBulmaBreakpoint(\n breakpoint: keyof typeof VIEWPORT_INDEX\n): boolean | null {\n const [atOrPast, setAtOrPast] = useState<boolean | null>(null);\n\n useClientLayoutEffect(() => {\n const list =\n typeof window.matchMedia === 'function'\n ? window.matchMedia(VIEWPORT_QUERIES[VIEWPORT_INDEX[breakpoint]])\n : null;\n const update = () => setAtOrPast(list ? list.matches : true);\n update();\n list?.addEventListener('change', update);\n return () => list?.removeEventListener('change', update);\n }, [breakpoint]);\n\n return atOrPast;\n}\n"],"names":[],"mappings":";;;;;AAAA;AAIA;;;;;;AAMG;AACH,MAAM,gBAAgB,GAAG;IACvB,gCAAgC;IAChC,gCAAgC;IAChC,gCAAgC;IAChC,sCAAsC;CACvC;AAED;AACO,MAAM,MAAM,GAAG,gBAAgB,CAAC;AAEvC;AACA,MAAM,cAAc,GAAG;AACrB,IAAA,UAAU,EAAE,CAAC;AACb,IAAA,OAAO,EAAE,CAAC;AACV,IAAA,MAAM,EAAE,CAAC;CACD;AAEV;;;;;;;;;;;AAWG;AACG,SAAU,gBAAgB,CAAC,OAAgB,EAAA;IAC/C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC;IAE5C,qBAAqB,CAAC,MAAK;QACzB,IAAI,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;AAAE,YAAA,OAAO,SAAS;AACzE,QAAA,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrE,MAAM,MAAM,GAAG,MAAK;AAClB,YAAA,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC;AACnD,YAAA,WAAW,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC;AACzC,QAAA,CAAC;AACD,QAAA,MAAM,EAAE;AACR,QAAA,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC9D,OAAO,MACL,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACrE,IAAA,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;IAEb,OAAO,OAAO,GAAG,QAAQ,GAAG,EAAE;AAChC;AAEA;;;;;;;;;;AAUG;AACG,SAAU,kBAAkB,CAChC,UAAuC,EAAA;IAEvC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC;IAE9D,qBAAqB,CAAC,MAAK;AACzB,QAAA,MAAM,IAAI,GACR,OAAO,MAAM,CAAC,UAAU,KAAK;AAC3B,cAAE,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;cAC9D,IAAI;AACV,QAAA,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;AAC5D,QAAA,MAAM,EAAE;AACR,QAAA,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC;QACxC,OAAO,MAAM,IAAI,EAAE,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC;AAC1D,IAAA,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;AAEhB,IAAA,OAAO,QAAQ;AACjB;;"}
|
|
@@ -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);
|