@zeyos/zx 2.1.0 → 2.2.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/CHANGELOG.md +39 -0
- package/README.md +15 -0
- package/dist/types/compat/base.d.ts +156 -0
- package/dist/types/compat/globals.d.ts +20 -0
- package/dist/types/compat/index.d.ts +128 -0
- package/dist/types/compat/map/basic.d.ts +175 -0
- package/dist/types/compat/map/data.d.ts +102 -0
- package/dist/types/compat/map/date-time.d.ts +80 -0
- package/dist/types/compat/map/factory.d.ts +9 -0
- package/dist/types/compat/map/form.d.ts +98 -0
- package/dist/types/compat/map/helpers.d.ts +12 -0
- package/dist/types/compat/map/http.d.ts +36 -0
- package/dist/types/compat/map/layout.d.ts +106 -0
- package/dist/types/compat/map/options.d.ts +80 -0
- package/dist/types/compat/map/select.d.ts +95 -0
- package/dist/types/compat/map/stubs.d.ts +32 -0
- package/dist/types/compat/map/utilities.d.ts +37 -0
- package/dist/types/compat-entry.d.ts +5 -0
- package/dist/types/components/badge/badge.d.ts +46 -0
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
- package/dist/types/components/button/button.d.ts +42 -0
- package/dist/types/components/check-button/check-button.d.ts +98 -0
- package/dist/types/components/checklist/checklist.d.ts +143 -0
- package/dist/types/components/checklist/field-adapter.d.ts +5 -0
- package/dist/types/components/context-menu/context-menu.d.ts +130 -0
- package/dist/types/components/copy/copy-button.d.ts +48 -0
- package/dist/types/components/copy/copy-input.d.ts +91 -0
- package/dist/types/components/data-filter/data-filter.d.ts +250 -0
- package/dist/types/components/data-filter/filter-core.d.ts +52 -0
- package/dist/types/components/date-picker/date-picker.d.ts +137 -0
- package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
- package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
- package/dist/types/components/date-picker/month-picker.d.ts +75 -0
- package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
- package/dist/types/components/date-picker/time-picker.d.ts +127 -0
- package/dist/types/components/datebox/date-range-box.d.ts +218 -0
- package/dist/types/components/datebox/datebox.d.ts +160 -0
- package/dist/types/components/datebox/field-adapter.d.ts +5 -0
- package/dist/types/components/dialog/dialog.d.ts +284 -0
- package/dist/types/components/dropdown/dropdown.d.ts +118 -0
- package/dist/types/components/empty-state/empty-state.d.ts +38 -0
- package/dist/types/components/field/field.d.ts +231 -0
- package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
- package/dist/types/components/field-upload/field-upload.d.ts +213 -0
- package/dist/types/components/fieldset/fieldset.d.ts +125 -0
- package/dist/types/components/finder/finder.d.ts +252 -0
- package/dist/types/components/form/form.d.ts +194 -0
- package/dist/types/components/groupbox/groupbox.d.ts +89 -0
- package/dist/types/components/layout/layout.d.ts +132 -0
- package/dist/types/components/loading/inline-loading.d.ts +86 -0
- package/dist/types/components/loading/progress-bar.d.ts +168 -0
- package/dist/types/components/loading/spinner.d.ts +34 -0
- package/dist/types/components/master-panel/master-panel.d.ts +112 -0
- package/dist/types/components/menu-button/menu-button.d.ts +142 -0
- package/dist/types/components/message/message.d.ts +212 -0
- package/dist/types/components/modal/modal.d.ts +105 -0
- package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
- package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
- package/dist/types/components/number-field/field-adapter.d.ts +5 -0
- package/dist/types/components/number-field/number-field.d.ts +292 -0
- package/dist/types/components/pagination/pagination.d.ts +267 -0
- package/dist/types/components/panel/panel.d.ts +194 -0
- package/dist/types/components/permission/permission.d.ts +93 -0
- package/dist/types/components/rating/field-adapter.d.ts +5 -0
- package/dist/types/components/rating/rating.d.ts +213 -0
- package/dist/types/components/search/search.d.ts +98 -0
- package/dist/types/components/select/field-adapter.d.ts +5 -0
- package/dist/types/components/select/filter.d.ts +11 -0
- package/dist/types/components/select/select.d.ts +235 -0
- package/dist/types/components/skeleton/skeleton.d.ts +97 -0
- package/dist/types/components/slider/field-adapter.d.ts +5 -0
- package/dist/types/components/slider/slider.d.ts +216 -0
- package/dist/types/components/split-view/split-view.d.ts +335 -0
- package/dist/types/components/stepper/stepper.d.ts +314 -0
- package/dist/types/components/stepper/steps.d.ts +131 -0
- package/dist/types/components/tabbox/tabbox.d.ts +230 -0
- package/dist/types/components/table/sort.d.ts +49 -0
- package/dist/types/components/table/table.d.ts +872 -0
- package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
- package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
- package/dist/types/components/timebox/field-adapter.d.ts +5 -0
- package/dist/types/components/timebox/timebox.d.ts +121 -0
- package/dist/types/components/toggle/field-adapter.d.ts +5 -0
- package/dist/types/components/toggle/toggle.d.ts +94 -0
- package/dist/types/components/toolbar/toolbar.d.ts +230 -0
- package/dist/types/components/tooltip/tooltip.d.ts +183 -0
- package/dist/types/components/tree/hierarchy.d.ts +113 -0
- package/dist/types/components/tree/tree.d.ts +383 -0
- package/dist/types/components/truncate/truncate.d.ts +61 -0
- package/dist/types/components/value-list/field-adapter.d.ts +5 -0
- package/dist/types/components/value-list/value-list.d.ts +143 -0
- package/dist/types/core/breakpoint.d.ts +68 -0
- package/dist/types/core/component.d.ts +115 -0
- package/dist/types/core/date.d.ts +50 -0
- package/dist/types/core/dom.d.ts +86 -0
- package/dist/types/core/export.d.ts +70 -0
- package/dist/types/core/fontawesome.d.ts +107 -0
- package/dist/types/core/format.d.ts +107 -0
- package/dist/types/core/http.d.ts +122 -0
- package/dist/types/core/i18n.d.ts +35 -0
- package/dist/types/core/icons.d.ts +137 -0
- package/dist/types/core/keyboard.d.ts +72 -0
- package/dist/types/core/position.d.ts +45 -0
- package/dist/types/core/storage.d.ts +46 -0
- package/dist/types/core/util.d.ts +117 -0
- package/dist/types/elements/define.d.ts +73 -0
- package/dist/types/elements/reflect.d.ts +27 -0
- package/dist/types/index.d.ts +79 -0
- package/dist/types/zeyos/connect.d.ts +65 -0
- package/dist/types/zeyos/form.d.ts +140 -0
- package/dist/types/zeyos/icons.d.ts +104 -0
- package/dist/types/zeyos/index.d.ts +12 -0
- package/dist/types/zeyos/modules.d.ts +110 -0
- package/dist/types/zeyos/query.d.ts +157 -0
- package/dist/types/zeyos/schema.d.ts +79 -0
- package/dist/types/zeyos/select.d.ts +167 -0
- package/dist/types/zeyos/table.d.ts +205 -0
- package/dist/zx-compat.esm.js +362 -33
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +359 -67
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +342 -43
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +68 -6
- package/dist/zx.esm.js +346 -67
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +346 -67
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +48 -2
- package/docs/llms.md +28 -0
- package/package.json +37 -9
- package/src/compat/base.js +291 -0
- package/src/compat/globals.js +165 -0
- package/src/compat/index.js +84 -0
- package/src/compat/map/basic.js +464 -0
- package/src/compat/map/data.js +232 -0
- package/src/compat/map/date-time.js +177 -0
- package/src/compat/map/factory.js +45 -0
- package/src/compat/map/form.js +330 -0
- package/src/compat/map/helpers.js +46 -0
- package/src/compat/map/http.js +117 -0
- package/src/compat/map/layout.js +256 -0
- package/src/compat/map/options.js +267 -0
- package/src/compat/map/select.js +198 -0
- package/src/compat/map/stubs.js +26 -0
- package/src/compat/map/utilities.js +113 -0
- package/src/compat-entry.js +11 -0
- package/src/components/badge/badge.css +147 -0
- package/src/components/badge/badge.js +92 -0
- package/src/components/breadcrumb/breadcrumb.css +105 -0
- package/src/components/breadcrumb/breadcrumb.js +331 -0
- package/src/components/button/button.css +104 -0
- package/src/components/button/button.js +76 -0
- package/src/components/check-button/check-button.css +66 -0
- package/src/components/check-button/check-button.js +167 -0
- package/src/components/checklist/checklist.css +65 -0
- package/src/components/checklist/checklist.js +282 -0
- package/src/components/checklist/field-adapter.js +46 -0
- package/src/components/context-menu/context-menu.css +54 -0
- package/src/components/context-menu/context-menu.js +334 -0
- package/src/components/copy/copy-button.css +27 -0
- package/src/components/copy/copy-button.js +78 -0
- package/src/components/copy/copy-input.css +59 -0
- package/src/components/copy/copy-input.js +123 -0
- package/src/components/data-filter/data-filter.css +53 -0
- package/src/components/data-filter/data-filter.js +400 -0
- package/src/components/data-filter/filter-core.js +128 -0
- package/src/components/date-picker/date-picker.css +181 -0
- package/src/components/date-picker/date-picker.js +500 -0
- package/src/components/date-picker/date-range-picker.css +264 -0
- package/src/components/date-picker/date-range-picker.js +990 -0
- package/src/components/date-picker/field-adapter.js +36 -0
- package/src/components/date-picker/month-picker.js +223 -0
- package/src/components/date-picker/range-field-adapter.js +31 -0
- package/src/components/date-picker/time-picker.css +191 -0
- package/src/components/date-picker/time-picker.js +406 -0
- package/src/components/datebox/date-range-box.css +128 -0
- package/src/components/datebox/date-range-box.js +492 -0
- package/src/components/datebox/datebox.css +108 -0
- package/src/components/datebox/datebox.js +358 -0
- package/src/components/datebox/field-adapter.js +34 -0
- package/src/components/dialog/dialog.css +74 -0
- package/src/components/dialog/dialog.js +450 -0
- package/src/components/dropdown/dropdown.css +24 -0
- package/src/components/dropdown/dropdown.js +288 -0
- package/src/components/empty-state/empty-state.css +80 -0
- package/src/components/empty-state/empty-state.js +92 -0
- package/src/components/field/field.css +127 -0
- package/src/components/field/field.js +605 -0
- package/src/components/field-upload/field-adapter.js +21 -0
- package/src/components/field-upload/field-upload.css +108 -0
- package/src/components/field-upload/field-upload.js +475 -0
- package/src/components/fieldset/fieldset.css +37 -0
- package/src/components/fieldset/fieldset.js +307 -0
- package/src/components/finder/finder.css +98 -0
- package/src/components/finder/finder.js +509 -0
- package/src/components/form/form.css +32 -0
- package/src/components/form/form.js +302 -0
- package/src/components/groupbox/groupbox.css +51 -0
- package/src/components/groupbox/groupbox.js +199 -0
- package/src/components/layout/layout.css +117 -0
- package/src/components/layout/layout.js +136 -0
- package/src/components/loading/inline-loading.css +34 -0
- package/src/components/loading/inline-loading.js +140 -0
- package/src/components/loading/progress-bar.css +115 -0
- package/src/components/loading/progress-bar.js +259 -0
- package/src/components/loading/spinner.css +58 -0
- package/src/components/loading/spinner.js +57 -0
- package/src/components/master-panel/master-panel.css +51 -0
- package/src/components/master-panel/master-panel.js +203 -0
- package/src/components/menu-button/menu-button.css +49 -0
- package/src/components/menu-button/menu-button.js +294 -0
- package/src/components/message/message.css +122 -0
- package/src/components/message/message.js +499 -0
- package/src/components/modal/modal.css +47 -0
- package/src/components/modal/modal.js +203 -0
- package/src/components/multi-value-editor/field-adapter.js +44 -0
- package/src/components/multi-value-editor/multi-value-editor.css +56 -0
- package/src/components/multi-value-editor/multi-value-editor.js +170 -0
- package/src/components/navigation-bar/navigation-bar.css +130 -0
- package/src/components/navigation-bar/navigation-bar.js +450 -0
- package/src/components/number-field/field-adapter.js +40 -0
- package/src/components/number-field/number-field.css +77 -0
- package/src/components/number-field/number-field.js +545 -0
- package/src/components/pagination/pagination.css +95 -0
- package/src/components/pagination/pagination.js +481 -0
- package/src/components/panel/panel.css +123 -0
- package/src/components/panel/panel.js +336 -0
- package/src/components/permission/permission.css +47 -0
- package/src/components/permission/permission.js +198 -0
- package/src/components/rating/field-adapter.js +34 -0
- package/src/components/rating/rating.css +97 -0
- package/src/components/rating/rating.js +395 -0
- package/src/components/search/search.css +70 -0
- package/src/components/search/search.js +193 -0
- package/src/components/select/field-adapter.js +24 -0
- package/src/components/select/filter.js +39 -0
- package/src/components/select/select.css +180 -0
- package/src/components/select/select.js +663 -0
- package/src/components/skeleton/skeleton.css +76 -0
- package/src/components/skeleton/skeleton.js +139 -0
- package/src/components/slider/field-adapter.js +36 -0
- package/src/components/slider/slider.css +181 -0
- package/src/components/slider/slider.js +399 -0
- package/src/components/split-view/split-view.css +139 -0
- package/src/components/split-view/split-view.js +806 -0
- package/src/components/stepper/stepper.css +234 -0
- package/src/components/stepper/stepper.js +459 -0
- package/src/components/stepper/steps.js +131 -0
- package/src/components/tabbox/tabbox.css +281 -0
- package/src/components/tabbox/tabbox.js +484 -0
- package/src/components/table/sort.js +85 -0
- package/src/components/table/table.css +364 -0
- package/src/components/table/table.js +1693 -0
- package/src/components/tag-picker/field-adapter.js +35 -0
- package/src/components/tag-picker/tag-picker.css +169 -0
- package/src/components/tag-picker/tag-picker.js +837 -0
- package/src/components/timebox/field-adapter.js +24 -0
- package/src/components/timebox/timebox.css +51 -0
- package/src/components/timebox/timebox.js +276 -0
- package/src/components/toggle/field-adapter.js +24 -0
- package/src/components/toggle/toggle.css +66 -0
- package/src/components/toggle/toggle.js +150 -0
- package/src/components/toolbar/toolbar.css +81 -0
- package/src/components/toolbar/toolbar.js +511 -0
- package/src/components/tooltip/tooltip.css +36 -0
- package/src/components/tooltip/tooltip.js +420 -0
- package/src/components/tree/hierarchy.js +151 -0
- package/src/components/tree/tree.css +99 -0
- package/src/components/tree/tree.js +784 -0
- package/src/components/truncate/truncate.css +20 -0
- package/src/components/truncate/truncate.js +92 -0
- package/src/components/value-list/field-adapter.js +24 -0
- package/src/components/value-list/value-list.css +108 -0
- package/src/components/value-list/value-list.js +336 -0
- package/src/core/breakpoint.js +150 -0
- package/src/core/component.js +292 -0
- package/src/core/date.js +216 -0
- package/src/core/dom.js +260 -0
- package/src/core/export.js +204 -0
- package/src/core/fontawesome.js +177 -0
- package/src/core/format.js +283 -0
- package/src/core/http.js +246 -0
- package/src/core/i18n.js +83 -0
- package/src/core/icons.js +283 -0
- package/src/core/keyboard.js +240 -0
- package/src/core/position.js +177 -0
- package/src/core/storage.js +175 -0
- package/src/core/util.js +336 -0
- package/src/elements/define.js +715 -0
- package/src/elements/reflect.js +79 -0
- package/src/index.js +131 -0
- package/src/zeyos/connect.js +142 -0
- package/src/zeyos/form.js +254 -0
- package/src/zeyos/icons.js +92 -0
- package/src/zeyos/index.js +21 -0
- package/src/zeyos/modules.js +269 -0
- package/src/zeyos/query.js +199 -0
- package/src/zeyos/schema.js +474 -0
- package/src/zeyos/select.js +175 -0
- package/src/zeyos/table.js +272 -0
- package/styles/base.css +124 -0
- package/styles/icons.css +34 -0
- package/styles/tokens/dark.css +84 -0
- package/styles/tokens/density.css +9 -0
- package/styles/tokens/global.css +80 -0
- package/styles/tokens/modules.css +109 -0
- package/styles/tokens/semantic.css +49 -0
- package/styles/zx.css +61 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { Component } from '../../core/component.js';
|
|
2
|
+
import { h, restoreTarget, snapshotTarget } from '../../core/dom.js';
|
|
3
|
+
import { rovingTabindex } from '../../core/keyboard.js';
|
|
4
|
+
import { isElement } from '../../core/util.js';
|
|
5
|
+
import { button } from '../button/button.js';
|
|
6
|
+
import { MenuButton } from '../menu-button/menu-button.js';
|
|
7
|
+
|
|
8
|
+
/** Control selectors that can become a roving-tabindex stop. */
|
|
9
|
+
const FOCUSABLE = ['button', 'a[href]', 'input', 'select', 'textarea', '[tabindex]'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Roving-tabindex stops: focusable direct children of the toolbar, plus the controls of a grouped
|
|
13
|
+
* child one level down (`buttonGroup()` renders a `[role="group"]` wrapper). Separators carry no
|
|
14
|
+
* tabindex and therefore never match, and the child combinators keep deeper structures — a popover
|
|
15
|
+
* panel rendered inside a composite item, for instance — out of the group.
|
|
16
|
+
*/
|
|
17
|
+
const ITEM_SELECTOR = FOCUSABLE
|
|
18
|
+
.flatMap((selector) => [`.zx-toolbar > ${selector}`, `.zx-toolbar > [role="group"] > ${selector}`])
|
|
19
|
+
.join(',');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ToolbarItemDescriptor
|
|
23
|
+
* @property {string} [name] Identifier for `getItem()`, `enable()`, `disable()`, and `setActive()`.
|
|
24
|
+
* @property {string} [label] Visible button label.
|
|
25
|
+
* @property {string} [icon] Icon name from `icons.js`.
|
|
26
|
+
* @property {'default'|'primary'|'danger'|'ghost'} [kind='default'] Button kind.
|
|
27
|
+
* @property {'md'|'sm'} [size] Button size; defaults to `'sm'` in a dense toolbar.
|
|
28
|
+
* @property {boolean} [disabled=false] Whether the control starts disabled.
|
|
29
|
+
* @property {string} [title] Native title text.
|
|
30
|
+
* @property {boolean} [active=false] Renders the button as a pressed toggle (`aria-pressed`).
|
|
31
|
+
* @property {(event: MouseEvent) => void} [onclick] Click callback, called after the `action` event.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** @typedef {Element|ToolbarItemDescriptor|'-'} ToolbarItem */
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {{type: 'separator'}
|
|
38
|
+
* |{type: 'element', element: Element}
|
|
39
|
+
* |{type: 'descriptor', descriptor: ToolbarItemDescriptor}} ToolbarEntry
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} ToolbarOptions
|
|
44
|
+
* @property {ToolbarItem[]} [items=[]] Controls, separators (`'-'`), and ready-made Elements.
|
|
45
|
+
* @property {'start'|'end'|'between'} [align='start'] Horizontal distribution.
|
|
46
|
+
* @property {string} [label='Toolbar'] Accessible name of the toolbar.
|
|
47
|
+
* @property {boolean} [overflow=true] Collapses items that no longer fit into a trailing menu.
|
|
48
|
+
* @property {string} [overflowLabel='More'] Accessible name of the overflow menu trigger.
|
|
49
|
+
* @property {boolean} [dense=false] Tighter spacing and small controls.
|
|
50
|
+
* @property {(event: CustomEvent<{name: string|null, item: Element}>) => void} [onaction] Action listener.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* APG toolbar: one tab stop, arrow keys between controls, Home and End to the ends, and an
|
|
55
|
+
* optional overflow menu that swallows the items that no longer fit.
|
|
56
|
+
* @fires Toolbar#action
|
|
57
|
+
* @extends {Component<ToolbarOptions>}
|
|
58
|
+
*/
|
|
59
|
+
export class Toolbar extends Component {
|
|
60
|
+
static cssName = 'toolbar';
|
|
61
|
+
|
|
62
|
+
/** @type {Readonly<ToolbarOptions>} */
|
|
63
|
+
static defaults = {
|
|
64
|
+
items: [],
|
|
65
|
+
align: 'start',
|
|
66
|
+
label: 'Toolbar',
|
|
67
|
+
overflow: true,
|
|
68
|
+
overflowLabel: 'More',
|
|
69
|
+
dense: false
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** @type {ReturnType<typeof rovingTabindex>} */
|
|
73
|
+
#roving;
|
|
74
|
+
/** @type {MenuButton|null} */
|
|
75
|
+
#overflow = null;
|
|
76
|
+
/** @type {ResizeObserver|null} */
|
|
77
|
+
#observer = null;
|
|
78
|
+
/** Re-entrancy guard: the measure pass writes layout the observer watches. */
|
|
79
|
+
#measuring = false;
|
|
80
|
+
/** Inline size of the last completed measure pass; `-1` forces the next one. */
|
|
81
|
+
#lastWidth = -1;
|
|
82
|
+
/**
|
|
83
|
+
* Block size of the last completed measure pass. Switching density resizes the controls without
|
|
84
|
+
* changing the row width, so the height belongs in the cache key.
|
|
85
|
+
*/
|
|
86
|
+
#lastHeight = -1;
|
|
87
|
+
/** Handle of the frame a pending observer-driven measure pass is scheduled in; `0` if none. */
|
|
88
|
+
#frame = 0;
|
|
89
|
+
#destroyed = false;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a toolbar, or turns an existing element into one.
|
|
93
|
+
* @param {Element|string|null} [target=null] Existing container, selector, or null to create one.
|
|
94
|
+
* @param {ToolbarOptions} [options={}] Toolbar options.
|
|
95
|
+
*/
|
|
96
|
+
constructor(target = null, options = {}) {
|
|
97
|
+
super(target, options);
|
|
98
|
+
|
|
99
|
+
if (this.options.overflow) {
|
|
100
|
+
this.#overflow = new MenuButton(null, {
|
|
101
|
+
icon: 'dots',
|
|
102
|
+
label: '',
|
|
103
|
+
kind: 'ghost',
|
|
104
|
+
placement: 'bottom-end'
|
|
105
|
+
});
|
|
106
|
+
const trigger = /** @type {HTMLButtonElement} */ (this.#overflow.toElement());
|
|
107
|
+
trigger.classList.add('zx-toolbar__overflow');
|
|
108
|
+
trigger.setAttribute('aria-label', String(this.options.overflowLabel));
|
|
109
|
+
trigger.setAttribute('title', String(this.options.overflowLabel));
|
|
110
|
+
if (this.options.dense) trigger.setAttribute('data-size', 'sm');
|
|
111
|
+
trigger.hidden = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.#roving = rovingTabindex(this.el, ITEM_SELECTOR, { orientation: 'horizontal' });
|
|
115
|
+
this.listen(this.el, 'click', (event) => this.#onClick(/** @type {MouseEvent} */ (event)));
|
|
116
|
+
this.setItems(this._initialItems);
|
|
117
|
+
|
|
118
|
+
if (this.#overflow && typeof ResizeObserver === 'function') {
|
|
119
|
+
this.#observer = new ResizeObserver(() => this.#scheduleMeasure());
|
|
120
|
+
this.#observer.observe(this.el);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Prepares the toolbar root. Instance state is initialized here because `render()` runs inside
|
|
126
|
+
* the base constructor, before any class-field initializer.
|
|
127
|
+
* @returns {HTMLElement}
|
|
128
|
+
*/
|
|
129
|
+
render() {
|
|
130
|
+
/** @type {ToolbarRecord[]} */
|
|
131
|
+
this._records = [];
|
|
132
|
+
/** @type {Map<string, ToolbarRecord>} */
|
|
133
|
+
this._named = new Map();
|
|
134
|
+
this._createdRoot = !this.el;
|
|
135
|
+
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
136
|
+
this.el = root;
|
|
137
|
+
this._snapshot = this._createdRoot ? null : snapshotTarget(root);
|
|
138
|
+
// An empty item list adopts whatever the target already contains; anything else is validated
|
|
139
|
+
// by normalizeItems() rather than silently ignored here.
|
|
140
|
+
const items = this.options.items;
|
|
141
|
+
this._initialItems = Array.isArray(items) && items.length === 0 ?
|
|
142
|
+
Array.from(root.children) : items;
|
|
143
|
+
|
|
144
|
+
root.setAttribute('role', 'toolbar');
|
|
145
|
+
root.setAttribute('aria-label', String(this.options.label));
|
|
146
|
+
root.dataset.align = normalizeAlign(this.options.align);
|
|
147
|
+
root.dataset.overflow = this.options.overflow ? 'true' : 'false';
|
|
148
|
+
if (this.options.dense) root.dataset.dense = 'true';
|
|
149
|
+
return root;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Replaces every item and remeasures the overflow.
|
|
154
|
+
* @param {ToolbarItem[]} list Controls, separators, and Elements.
|
|
155
|
+
* @returns {this}
|
|
156
|
+
*/
|
|
157
|
+
setItems(list) {
|
|
158
|
+
for (const record of this._records) {
|
|
159
|
+
if (record.type === 'element') record.element.hidden = record.userHidden;
|
|
160
|
+
}
|
|
161
|
+
this._records = normalizeItems(list).map((entry) => this.#createRecord(entry));
|
|
162
|
+
this._named = new Map();
|
|
163
|
+
for (const record of this._records) {
|
|
164
|
+
if (record.name !== null && !this._named.has(record.name)) this._named.set(record.name, record);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const children = this._records.map((record) => record.element);
|
|
168
|
+
if (this.#overflow) {
|
|
169
|
+
this.#overflow.setItems([]);
|
|
170
|
+
const trigger = /** @type {HTMLElement} */ (this.#overflow.toElement());
|
|
171
|
+
trigger.hidden = true;
|
|
172
|
+
children.push(trigger);
|
|
173
|
+
}
|
|
174
|
+
this.el.replaceChildren(...children);
|
|
175
|
+
this.#lastWidth = -1;
|
|
176
|
+
this.#lastHeight = -1;
|
|
177
|
+
this.#measure();
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Looks up an item element by name.
|
|
183
|
+
* @param {string} name Descriptor name, or the `data-name` of an Element item.
|
|
184
|
+
* @returns {Element|null}
|
|
185
|
+
*/
|
|
186
|
+
getItem(name) {
|
|
187
|
+
return this._named.get(String(name))?.element ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Enables an item.
|
|
192
|
+
* @param {string} name Item name.
|
|
193
|
+
* @returns {this}
|
|
194
|
+
*/
|
|
195
|
+
enable(name) {
|
|
196
|
+
return this.#setDisabled(name, false);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Disables an item.
|
|
201
|
+
* @param {string} name Item name.
|
|
202
|
+
* @returns {this}
|
|
203
|
+
*/
|
|
204
|
+
disable(name) {
|
|
205
|
+
return this.#setDisabled(name, true);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sets the pressed state of a toggle item.
|
|
210
|
+
* @param {string} name Item name.
|
|
211
|
+
* @param {boolean} [active=true] Whether the item reads as pressed.
|
|
212
|
+
* @returns {this}
|
|
213
|
+
*/
|
|
214
|
+
setActive(name, active = true) {
|
|
215
|
+
const record = this._named.get(String(name));
|
|
216
|
+
if (record) record.element.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Removes listeners, the resize observer, the overflow menu, and any DOM changes made to an
|
|
222
|
+
* adopted target.
|
|
223
|
+
* @returns {void}
|
|
224
|
+
*/
|
|
225
|
+
destroy() {
|
|
226
|
+
if (this.#destroyed) return;
|
|
227
|
+
this.#destroyed = true;
|
|
228
|
+
this.#observer?.disconnect();
|
|
229
|
+
this.#observer = null;
|
|
230
|
+
if (this.#frame !== 0) cancelAnimationFrame(this.#frame);
|
|
231
|
+
this.#frame = 0;
|
|
232
|
+
this.#roving.destroy();
|
|
233
|
+
this.#overflow?.destroy();
|
|
234
|
+
this.#overflow = null;
|
|
235
|
+
for (const record of this._records) {
|
|
236
|
+
if (record.type === 'element') record.element.hidden = record.userHidden;
|
|
237
|
+
}
|
|
238
|
+
super.destroy();
|
|
239
|
+
if (!this._createdRoot) restoreTarget(this.el, this._snapshot);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Builds the element and bookkeeping for one normalized entry.
|
|
244
|
+
* @param {ToolbarEntry} entry Normalized entry.
|
|
245
|
+
* @returns {ToolbarRecord}
|
|
246
|
+
*/
|
|
247
|
+
#createRecord(entry) {
|
|
248
|
+
if (entry.type === 'separator') {
|
|
249
|
+
return {
|
|
250
|
+
type: 'separator',
|
|
251
|
+
element: h('span', {
|
|
252
|
+
class: 'zx-toolbar__separator',
|
|
253
|
+
role: 'separator',
|
|
254
|
+
ariaOrientation: 'vertical',
|
|
255
|
+
dataset: { separator: 'true' }
|
|
256
|
+
}),
|
|
257
|
+
name: null,
|
|
258
|
+
label: '',
|
|
259
|
+
icon: null,
|
|
260
|
+
onclick: null,
|
|
261
|
+
userHidden: false
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (entry.type === 'element') {
|
|
265
|
+
const element = entry.element;
|
|
266
|
+
return {
|
|
267
|
+
type: 'element',
|
|
268
|
+
element,
|
|
269
|
+
name: /** @type {HTMLElement} */ (element).dataset?.name ?? null,
|
|
270
|
+
label: elementLabel(element),
|
|
271
|
+
icon: null,
|
|
272
|
+
onclick: null,
|
|
273
|
+
userHidden: Boolean(/** @type {HTMLElement} */ (element).hidden)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { name = null, active, onclick, ...rest } = entry.descriptor;
|
|
278
|
+
const element = button({ size: this.options.dense ? 'sm' : 'md', ...rest });
|
|
279
|
+
if (active !== undefined) element.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
280
|
+
if (name !== null) element.dataset.name = String(name);
|
|
281
|
+
return {
|
|
282
|
+
type: 'descriptor',
|
|
283
|
+
element,
|
|
284
|
+
name: name === null ? null : String(name),
|
|
285
|
+
label: String(rest.label ?? rest.title ?? ''),
|
|
286
|
+
icon: rest.icon ?? null,
|
|
287
|
+
onclick: typeof onclick === 'function' ? onclick : null,
|
|
288
|
+
userHidden: false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Emits `action` for descriptor-built items and runs their own callback.
|
|
294
|
+
* @param {MouseEvent} event Click event.
|
|
295
|
+
* @returns {void}
|
|
296
|
+
* @fires Toolbar#action
|
|
297
|
+
*/
|
|
298
|
+
#onClick(event) {
|
|
299
|
+
const target = /** @type {Node|null} */ (event.target);
|
|
300
|
+
if (!target) return;
|
|
301
|
+
const record = this._records.find((item) =>
|
|
302
|
+
item.type === 'descriptor' && item.element.contains(target));
|
|
303
|
+
if (!record) return;
|
|
304
|
+
this.emit('action', { name: record.name, item: record.element });
|
|
305
|
+
record.onclick?.(event);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Applies a disabled state to an item and mirrors it in the overflow menu.
|
|
310
|
+
* @param {string} name Item name.
|
|
311
|
+
* @param {boolean} disabled Next disabled state.
|
|
312
|
+
* @returns {this}
|
|
313
|
+
*/
|
|
314
|
+
#setDisabled(name, disabled) {
|
|
315
|
+
const record = this._named.get(String(name));
|
|
316
|
+
if (!record) return this;
|
|
317
|
+
const element = /** @type {HTMLButtonElement} */ (record.element);
|
|
318
|
+
if ('disabled' in element) element.disabled = disabled;
|
|
319
|
+
else if (disabled) element.setAttribute('aria-disabled', 'true');
|
|
320
|
+
else element.removeAttribute('aria-disabled');
|
|
321
|
+
this.#syncOverflowItems();
|
|
322
|
+
return this;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Queues one measure pass for the next frame. Observer notifications arriving in the same frame
|
|
327
|
+
* collapse into that single pass, so the writes the pass makes can never drive the observer in a
|
|
328
|
+
* tight loop — the worst case is one pass per frame, and the size check in `#measure()` turns a
|
|
329
|
+
* pass triggered by our own writes into a no-op.
|
|
330
|
+
* @returns {void}
|
|
331
|
+
*/
|
|
332
|
+
#scheduleMeasure() {
|
|
333
|
+
if (this.#frame !== 0 || this.#destroyed) return;
|
|
334
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
335
|
+
this.#measure();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.#frame = requestAnimationFrame(() => {
|
|
339
|
+
this.#frame = 0;
|
|
340
|
+
this.#measure();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Hides the items that no longer fit and mirrors them into the overflow menu.
|
|
346
|
+
*
|
|
347
|
+
* Every pass starts by showing all items again, so its outcome depends only on the available
|
|
348
|
+
* width and the intrinsic item widths — never on what the previous pass hid. That makes the pass
|
|
349
|
+
* idempotent; combined with the size check below and the frame coalescing in
|
|
350
|
+
* `#scheduleMeasure()`, the writes this makes can never drive the `ResizeObserver` in a loop.
|
|
351
|
+
* @returns {void}
|
|
352
|
+
*/
|
|
353
|
+
#measure() {
|
|
354
|
+
if (this.#measuring || this.#destroyed || !this.#overflow) return;
|
|
355
|
+
const root = /** @type {HTMLElement} */ (this.el);
|
|
356
|
+
if (typeof getComputedStyle !== 'function' || !root.isConnected) return;
|
|
357
|
+
|
|
358
|
+
const styles = getComputedStyle(root);
|
|
359
|
+
const gap = parseFloat(styles.columnGap) || 0;
|
|
360
|
+
const padding = (parseFloat(styles.paddingInlineStart) || 0)
|
|
361
|
+
+ (parseFloat(styles.paddingInlineEnd) || 0);
|
|
362
|
+
const available = root.clientWidth - padding;
|
|
363
|
+
const height = root.clientHeight;
|
|
364
|
+
if (!(available > 0) || (available === this.#lastWidth && height === this.#lastHeight)) return;
|
|
365
|
+
|
|
366
|
+
this.#measuring = true;
|
|
367
|
+
try {
|
|
368
|
+
const trigger = /** @type {HTMLElement} */ (this.#overflow.toElement());
|
|
369
|
+
const candidates = this._records.filter((record) => !record.userHidden);
|
|
370
|
+
for (const record of candidates) record.element.hidden = false;
|
|
371
|
+
trigger.hidden = false;
|
|
372
|
+
|
|
373
|
+
const widths = candidates.map((record) => record.element.getBoundingClientRect().width);
|
|
374
|
+
const total = widths.reduce((sum, width) => sum + width, 0)
|
|
375
|
+
+ gap * Math.max(0, widths.length - 1);
|
|
376
|
+
const triggerWidth = trigger.getBoundingClientRect().width;
|
|
377
|
+
|
|
378
|
+
/** @type {ToolbarRecord[]} */
|
|
379
|
+
const hidden = [];
|
|
380
|
+
if (total > available) {
|
|
381
|
+
const budget = available - triggerWidth - gap;
|
|
382
|
+
let used = 0;
|
|
383
|
+
let collapsed = false;
|
|
384
|
+
candidates.forEach((record, index) => {
|
|
385
|
+
const next = used + widths[index] + (used > 0 ? gap : 0);
|
|
386
|
+
if (!collapsed && next <= budget) {
|
|
387
|
+
used = next;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
collapsed = true;
|
|
391
|
+
record.element.hidden = true;
|
|
392
|
+
hidden.push(record);
|
|
393
|
+
});
|
|
394
|
+
while (candidates.length > hidden.length) {
|
|
395
|
+
const last = candidates[candidates.length - hidden.length - 1];
|
|
396
|
+
if (last.type !== 'separator') break;
|
|
397
|
+
last.element.hidden = true;
|
|
398
|
+
hidden.unshift(last);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const menuItems = overflowMenuItems(hidden);
|
|
403
|
+
this.#overflow.setItems(menuItems);
|
|
404
|
+
trigger.hidden = menuItems.length === 0;
|
|
405
|
+
this.#lastWidth = available;
|
|
406
|
+
this.#lastHeight = root.clientHeight;
|
|
407
|
+
} finally {
|
|
408
|
+
this.#measuring = false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Rebuilds the overflow menu from the items currently hidden.
|
|
414
|
+
* @returns {void}
|
|
415
|
+
*/
|
|
416
|
+
#syncOverflowItems() {
|
|
417
|
+
if (!this.#overflow) return;
|
|
418
|
+
const hidden = this._records.filter((record) =>
|
|
419
|
+
!record.userHidden && /** @type {HTMLElement} */ (record.element).hidden);
|
|
420
|
+
this.#overflow.setItems(overflowMenuItems(hidden));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Emitted when a descriptor-built item is activated, from the toolbar or from the overflow menu.
|
|
426
|
+
* @event Toolbar#action
|
|
427
|
+
* @type {CustomEvent<{name: string|null, item: Element}>}
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @typedef {Object} ToolbarRecord
|
|
432
|
+
* @property {'separator'|'element'|'descriptor'} type Entry kind.
|
|
433
|
+
* @property {Element} element Rendered element.
|
|
434
|
+
* @property {string|null} name Lookup name.
|
|
435
|
+
* @property {string} label Label mirrored into the overflow menu.
|
|
436
|
+
* @property {string|null} icon Icon name mirrored into the overflow menu.
|
|
437
|
+
* @property {((event: MouseEvent) => void)|null} onclick Descriptor callback.
|
|
438
|
+
* @property {boolean} userHidden Whether the caller handed the item over already hidden.
|
|
439
|
+
*/
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Normalizes a raw item list: drops nullish entries, classifies each item, and collapses leading,
|
|
443
|
+
* trailing, and repeated separators. Pure — it touches no DOM.
|
|
444
|
+
* @param {ToolbarItem[]} list Raw item list.
|
|
445
|
+
* @returns {ToolbarEntry[]}
|
|
446
|
+
*/
|
|
447
|
+
export function normalizeItems(list) {
|
|
448
|
+
if (list != null && !Array.isArray(list)) throw new TypeError('Toolbar items must be an array');
|
|
449
|
+
/** @type {ToolbarEntry[]} */
|
|
450
|
+
const entries = [];
|
|
451
|
+
for (const item of list ?? []) {
|
|
452
|
+
if (item == null) continue;
|
|
453
|
+
if (item === '-') {
|
|
454
|
+
if (entries.length > 0 && entries[entries.length - 1].type !== 'separator') {
|
|
455
|
+
entries.push({ type: 'separator' });
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (isElement(item)) {
|
|
460
|
+
entries.push({ type: 'element', element: /** @type {Element} */ (item) });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (typeof item !== 'object' || Array.isArray(item)) {
|
|
464
|
+
throw new TypeError('Toolbar items must be Elements, item descriptors, or "-"');
|
|
465
|
+
}
|
|
466
|
+
entries.push({ type: 'descriptor', descriptor: { .../** @type {object} */ (item) } });
|
|
467
|
+
}
|
|
468
|
+
while (entries.length > 0 && entries[entries.length - 1].type === 'separator') entries.pop();
|
|
469
|
+
return entries;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Mirrors hidden toolbar items as menu-button items. Selecting one clicks the original control, so
|
|
474
|
+
* a collapsed item behaves exactly like the visible one.
|
|
475
|
+
* @param {ToolbarRecord[]} records Hidden records, in toolbar order.
|
|
476
|
+
* @returns {Array<import('../menu-button/menu-button.js').MenuButtonItem>}
|
|
477
|
+
*/
|
|
478
|
+
function overflowMenuItems(records) {
|
|
479
|
+
/** @type {Array<import('../menu-button/menu-button.js').MenuButtonItem>} */
|
|
480
|
+
const items = [];
|
|
481
|
+
for (const record of records) {
|
|
482
|
+
if (record.type === 'separator') {
|
|
483
|
+
if (items.length > 0 && items[items.length - 1] !== '-') items.push('-');
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const element = /** @type {HTMLButtonElement} */ (record.element);
|
|
487
|
+
items.push({
|
|
488
|
+
label: record.label || elementLabel(element) || '…',
|
|
489
|
+
icon: record.icon ?? undefined,
|
|
490
|
+
value: record.name,
|
|
491
|
+
disabled: Boolean(element.disabled) || element.getAttribute('aria-disabled') === 'true',
|
|
492
|
+
onselect: () => element.click()
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
while (items.length > 0 && items[items.length - 1] === '-') items.pop();
|
|
496
|
+
return items;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** @param {Element} element @returns {string} */
|
|
500
|
+
function elementLabel(element) {
|
|
501
|
+
return (element.getAttribute('aria-label')
|
|
502
|
+
?? element.getAttribute('title')
|
|
503
|
+
?? element.textContent
|
|
504
|
+
?? '').trim();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** @param {unknown} value @returns {'start'|'end'|'between'} */
|
|
508
|
+
function normalizeAlign(value) {
|
|
509
|
+
return ['end', 'between'].includes(/** @type {string} */ (value)) ?
|
|
510
|
+
/** @type {'end'|'between'} */ (value) : 'start';
|
|
511
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The bubble is a manual popover, so it lives in the top layer and the UA stylesheet supplies its
|
|
3
|
+
* `display: none` while closed — nothing here may set `display`, or a closed tooltip would show.
|
|
4
|
+
* Everything else in the UA popover rule (border, padding, system colours) is replaced below.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.zx-tooltip {
|
|
8
|
+
max-inline-size: var(--zx-tooltip-max-inline-size, 260px);
|
|
9
|
+
border: 1px solid var(--zx-color-border);
|
|
10
|
+
border-radius: var(--zx-radius-md);
|
|
11
|
+
background: var(--zx-color-bg-raised);
|
|
12
|
+
box-shadow: var(--zx-overlay-shadow);
|
|
13
|
+
padding: var(--zx-space-1) var(--zx-space-2);
|
|
14
|
+
color: var(--zx-color-text);
|
|
15
|
+
font-size: var(--zx-text-sm);
|
|
16
|
+
line-height: 1.45;
|
|
17
|
+
text-align: start;
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
/* A tooltip describes its anchor; it must never become a target of its own. Without this, the
|
|
20
|
+
bubble would steal the hover that keeps it open the moment it covered the anchor. */
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
user-select: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
26
|
+
.zx-tooltip:popover-open {
|
|
27
|
+
animation: zx-tooltip-enter var(--zx-dur-fast) var(--zx-ease);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes zx-tooltip-enter {
|
|
32
|
+
from {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transform: translateY(calc(-1 * var(--zx-space-1)));
|
|
35
|
+
}
|
|
36
|
+
}
|