@zeyos/zx 2.2.1 → 3.0.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 +197 -0
- package/DESIGN-SYSTEM.md +136 -0
- package/MIGRATION.md +155 -4
- package/README.md +51 -14
- package/dist/types/compat/map/data.d.ts +3 -2
- package/dist/types/components/account-menu/account-menu.d.ts +144 -0
- package/dist/types/components/app-rail/app-rail.d.ts +250 -0
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
- package/dist/types/components/avatar/avatar.d.ts +100 -0
- package/dist/types/components/chart/chart.d.ts +321 -0
- package/dist/types/components/date-picker/month-picker.d.ts +6 -0
- package/dist/types/components/dialog/dialog.d.ts +9 -0
- package/dist/types/components/dock/dock.d.ts +495 -0
- package/dist/types/components/dropdown/dropdown.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +14 -0
- package/dist/types/components/field-upload/field-upload.d.ts +37 -4
- package/dist/types/components/form/form.d.ts +25 -3
- package/dist/types/components/grid/grid.d.ts +172 -0
- package/dist/types/components/launcher/launcher.d.ts +300 -0
- package/dist/types/components/layout/layout.d.ts +4 -4
- package/dist/types/components/menu-button/menu-button.d.ts +10 -2
- package/dist/types/components/modal/modal.d.ts +25 -0
- package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +74 -9
- package/dist/types/components/questionnaire/items.d.ts +327 -0
- package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
- package/dist/types/components/select/select.d.ts +36 -1
- package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
- package/dist/types/components/sheet/sheet.d.ts +294 -0
- package/dist/types/components/split-view/split-view.d.ts +2 -21
- package/dist/types/components/tabbox/tabbox.d.ts +12 -3
- package/dist/types/components/table/table.d.ts +219 -12
- package/dist/types/components/tooltip/tooltip.d.ts +2 -2
- package/dist/types/components/value-list/value-list.d.ts +24 -3
- package/dist/types/core/dom.d.ts +114 -0
- package/dist/types/core/drag-axis.d.ts +111 -0
- package/dist/types/core/position.d.ts +4 -2
- package/dist/types/index.d.ts +12 -2
- package/dist/zx-compat.esm.js +943 -311
- package/dist/zx-compat.esm.min.js +1 -1
- package/dist/zx-compat.global.js +7893 -2252
- package/dist/zx-compat.global.min.js +3 -3
- package/dist/zx-zeyos.esm.js +826 -114
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +1720 -243
- package/dist/zx.esm.js +8227 -2599
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +8227 -2599
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +2597 -255
- package/docs/llms.md +475 -37
- package/docs/llms.txt +21 -9
- package/package.json +4 -2
- package/src/compat/globals.js +3 -1
- package/src/compat/map/data.js +31 -9
- package/src/components/account-menu/account-menu.css +70 -0
- package/src/components/account-menu/account-menu.js +208 -0
- package/src/components/app-rail/app-rail.css +107 -0
- package/src/components/app-rail/app-rail.js +668 -0
- package/src/components/app-sidebar/app-sidebar.css +68 -0
- package/src/components/app-sidebar/app-sidebar.js +388 -0
- package/src/components/avatar/avatar.css +61 -0
- package/src/components/avatar/avatar.js +222 -0
- package/src/components/button/button.css +0 -1
- package/src/components/chart/chart.css +61 -0
- package/src/components/chart/chart.js +518 -0
- package/src/components/date-picker/date-range-picker.css +7 -0
- package/src/components/date-picker/month-picker.js +9 -0
- package/src/components/dialog/dialog.js +11 -4
- package/src/components/dock/dock.css +228 -0
- package/src/components/dock/dock.js +977 -0
- package/src/components/dropdown/dropdown.js +1 -1
- package/src/components/field/field.js +2 -0
- package/src/components/field-upload/field-upload.js +9 -4
- package/src/components/fieldset/fieldset.css +7 -0
- package/src/components/form/form.css +7 -0
- package/src/components/form/form.js +6 -3
- package/src/components/grid/grid.css +6 -0
- package/src/components/grid/grid.js +213 -0
- package/src/components/launcher/launcher.css +100 -0
- package/src/components/launcher/launcher.js +557 -0
- package/src/components/layout/layout.js +2 -2
- package/src/components/menu-button/menu-button.js +27 -2
- package/src/components/modal/modal.css +5 -1
- package/src/components/modal/modal.js +37 -2
- package/src/components/multi-value-editor/multi-value-editor.css +99 -15
- package/src/components/multi-value-editor/multi-value-editor.js +237 -19
- package/src/components/navigation-bar/navigation-bar.css +7 -0
- package/src/components/questionnaire/items.js +370 -0
- package/src/components/questionnaire/questionnaire.css +387 -0
- package/src/components/questionnaire/questionnaire.js +1033 -0
- package/src/components/search/search.css +10 -2
- package/src/components/select/select.css +6 -1
- package/src/components/select/select.js +99 -11
- package/src/components/sheet/sheet-stack.js +304 -0
- package/src/components/sheet/sheet.css +351 -0
- package/src/components/sheet/sheet.js +551 -0
- package/src/components/split-view/split-view.js +76 -161
- package/src/components/stepper/stepper.css +12 -2
- package/src/components/tabbox/tabbox.css +13 -3
- package/src/components/tabbox/tabbox.js +34 -10
- package/src/components/table/table.css +43 -0
- package/src/components/table/table.js +523 -45
- package/src/components/tooltip/tooltip.js +5 -2
- package/src/components/value-list/value-list.js +6 -3
- package/src/core/dom.js +302 -0
- package/src/core/drag-axis.js +203 -0
- package/src/core/position.js +68 -22
- package/src/index.js +19 -2
- package/styles/tokens/dark.css +15 -13
- package/styles/tokens/global.css +17 -5
- package/styles/tokens/semantic.css +14 -8
- package/styles/tokens/themes.css +72 -0
- package/styles/zx.css +11 -1
- package/dist/types/components/permission/permission.d.ts +0 -93
- package/src/components/permission/permission.css +0 -47
- package/src/components/permission/permission.js +0 -198
|
@@ -52,6 +52,7 @@ export class Modal extends Component {
|
|
|
52
52
|
if (!this.options.closable || cancelEvent.defaultPrevented) event.preventDefault();
|
|
53
53
|
});
|
|
54
54
|
this.listen(this.el, 'close', () => {
|
|
55
|
+
if (!this._isRealClose()) return;
|
|
55
56
|
this.el.dataset.state = 'closed';
|
|
56
57
|
delete this.el.dataset.zxOverlayOrder;
|
|
57
58
|
const result = this.#hasPendingResult ? this.#pendingResult : (this.el.returnValue || undefined);
|
|
@@ -66,6 +67,40 @@ export class Modal extends Component {
|
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Element the owned dialog is appended to. Overridden by subclasses that live inside a host
|
|
72
|
+
* rather than at the document level.
|
|
73
|
+
*
|
|
74
|
+
* Called from `render()`, which the base constructor runs before any subclass field
|
|
75
|
+
* initializers, so an override may read `this.options` but must not touch instance state.
|
|
76
|
+
* @returns {Element}
|
|
77
|
+
*/
|
|
78
|
+
mountTarget() {
|
|
79
|
+
return document.body;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Puts the dialog on screen. Overridden by subclasses that open non-modally.
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
_show() {
|
|
87
|
+
this.el.showModal();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether an incoming `close` event is a real dismissal.
|
|
92
|
+
*
|
|
93
|
+
* A dialog cannot move between the top layer and the page flow while it is open, so a subclass
|
|
94
|
+
* that re-hosts itself has to close and immediately reopen. `close` is dispatched in a queued
|
|
95
|
+
* task, not synchronously, so that stale event arrives once the dialog is already open again —
|
|
96
|
+
* late enough to clobber `data-state`, fire a `close` nobody asked for, and trip
|
|
97
|
+
* `destroyOnClose`. Returning false from an override drops it at the source instead.
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
_isRealClose() {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
/**
|
|
70
105
|
* Creates the owned native dialog.
|
|
71
106
|
* @returns {HTMLDialogElement}
|
|
@@ -75,7 +110,7 @@ export class Modal extends Component {
|
|
|
75
110
|
class: 'zx-modal'
|
|
76
111
|
}, h('div', { class: 'zx-modal__content', ref: 'content' })));
|
|
77
112
|
dialog.dataset.state = 'closed';
|
|
78
|
-
|
|
113
|
+
this.mountTarget().append(dialog);
|
|
79
114
|
return dialog;
|
|
80
115
|
}
|
|
81
116
|
|
|
@@ -89,7 +124,7 @@ export class Modal extends Component {
|
|
|
89
124
|
this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
|
|
90
125
|
this.el.returnValue = '';
|
|
91
126
|
try {
|
|
92
|
-
this.
|
|
127
|
+
this._show();
|
|
93
128
|
} catch (error) {
|
|
94
129
|
delete this.el.dataset.zxOverlayOrder;
|
|
95
130
|
throw error;
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
/* Deliberately not a size container. Inline-size containment removes an element's own intrinsic
|
|
2
|
+
width, so every host that sizes to its contents — a flex row, an inline-block, a grid `auto`
|
|
3
|
+
track, a table cell — measured this editor as nothing and the rows spilled out of a 26px sliver.
|
|
4
|
+
`inline-size: 100%` does not rescue it either: the percentage resolves against a containing
|
|
5
|
+
block that was itself measured from the contribution containment had already zeroed. The row
|
|
6
|
+
below reflows on its own instead, which reads the width that actually matters — the row's.
|
|
7
|
+
|
|
8
|
+
Without containment `inline-size: 100%` does work: a percentage that has no definite containing
|
|
9
|
+
block to resolve against counts as `auto` while the host measures its contents, so the editor
|
|
10
|
+
contributes its real width and then fills whatever the host settled on. */
|
|
1
11
|
.zx-multi-value-editor {
|
|
2
12
|
display: grid;
|
|
3
13
|
gap: var(--zx-space-3);
|
|
4
14
|
min-inline-size: 0;
|
|
15
|
+
inline-size: 100%;
|
|
5
16
|
padding: var(--zx-space-3);
|
|
6
17
|
border: 1px solid var(--zx-color-border);
|
|
7
18
|
border-radius: var(--zx-radius-lg);
|
|
8
19
|
background: var(--zx-color-bg-surface);
|
|
9
|
-
container-type: inline-size;
|
|
10
20
|
}
|
|
11
21
|
|
|
12
22
|
.zx-multi-value-editor__rows {
|
|
@@ -15,19 +25,32 @@
|
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
.zx-multi-value-editor__row {
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
position: relative;
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-wrap: wrap;
|
|
31
|
+
/* A grid item refuses to go under its min-content width unless told to, which in a pane narrower
|
|
32
|
+
than the control cluster pushed the whole row back out through the editor's side. */
|
|
33
|
+
min-inline-size: 0;
|
|
20
34
|
gap: var(--zx-space-2);
|
|
21
35
|
align-items: center;
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
/* Below roughly this basis the controls wrap onto a line of their own, which is what the container
|
|
39
|
+
query used to do — except a row measures the space the row actually has. */
|
|
24
40
|
.zx-multi-value-editor__value {
|
|
25
|
-
|
|
41
|
+
flex: 1 1 11rem;
|
|
42
|
+
min-inline-size: 0;
|
|
26
43
|
}
|
|
27
44
|
|
|
45
|
+
/* Wide enough and this keeps its natural size at the end of the row; narrower than the buttons it
|
|
46
|
+
holds, and they wrap among themselves rather than out through the side. */
|
|
28
47
|
.zx-multi-value-editor__controls {
|
|
29
48
|
display: inline-flex;
|
|
49
|
+
flex: 0 1 auto;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
min-inline-size: 0;
|
|
30
52
|
gap: var(--zx-space-1);
|
|
53
|
+
margin-inline-start: auto;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
.zx-multi-value-editor__controls .zx-btn {
|
|
@@ -35,22 +58,83 @@
|
|
|
35
58
|
padding-inline: var(--zx-space-2);
|
|
36
59
|
}
|
|
37
60
|
|
|
38
|
-
.zx-multi-value-editor__controls .zx-btn
|
|
61
|
+
.zx-multi-value-editor__controls .zx-btn[data-action="remove"] {
|
|
39
62
|
color: var(--zx-color-danger);
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
/* A row at either end keeps its move button focusable and merely announces that it cannot go
|
|
66
|
+
further, so pressing it repeatedly never drops focus to the document. */
|
|
67
|
+
.zx-multi-value-editor__controls .zx-btn[aria-disabled="true"] {
|
|
68
|
+
opacity: 0.55;
|
|
69
|
+
cursor: not-allowed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.zx-multi-value-editor__controls .zx-btn[aria-disabled="true"]:hover {
|
|
73
|
+
background: var(--zx-color-bg-control);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.zx-multi-value-editor__handle {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
flex: none;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
min-block-size: var(--zx-control-height);
|
|
82
|
+
min-inline-size: var(--zx-space-6);
|
|
83
|
+
padding: 0;
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: var(--zx-control-radius);
|
|
86
|
+
background: none;
|
|
87
|
+
color: var(--zx-color-text-muted);
|
|
88
|
+
cursor: grab;
|
|
89
|
+
touch-action: none;
|
|
90
|
+
transition: color var(--zx-dur-fast) var(--zx-ease),
|
|
91
|
+
background-color var(--zx-dur-fast) var(--zx-ease);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.zx-multi-value-editor__handle:hover:not(:disabled) {
|
|
95
|
+
background: var(--zx-color-bg-hover);
|
|
96
|
+
color: var(--zx-color-text);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.zx-multi-value-editor__handle:focus-visible {
|
|
100
|
+
outline: none;
|
|
101
|
+
box-shadow: var(--zx-focus-ring);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.zx-multi-value-editor__handle:disabled {
|
|
105
|
+
opacity: 0.55;
|
|
106
|
+
cursor: default;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.zx-multi-value-editor[data-state="dragging"] .zx-multi-value-editor__handle {
|
|
110
|
+
cursor: grabbing;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.zx-multi-value-editor__row[data-dragging="true"] {
|
|
114
|
+
opacity: 0.45;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The line sits in the gap between two rows, so it reads as "it lands here", not "it replaces
|
|
118
|
+
this row". */
|
|
119
|
+
.zx-multi-value-editor__row[data-drop]::after {
|
|
120
|
+
content: '';
|
|
121
|
+
position: absolute;
|
|
122
|
+
inset-inline: 0;
|
|
123
|
+
block-size: 2px;
|
|
124
|
+
border-radius: var(--zx-radius-full);
|
|
125
|
+
background: var(--zx-color-accent);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.zx-multi-value-editor__row[data-drop="before"]::after {
|
|
129
|
+
inset-block-start: calc(var(--zx-space-2) / -2 - 1px);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.zx-multi-value-editor__row[data-drop="after"]::after {
|
|
133
|
+
inset-block-end: calc(var(--zx-space-2) / -2 - 1px);
|
|
134
|
+
}
|
|
135
|
+
|
|
42
136
|
.zx-multi-value-editor__add {
|
|
43
137
|
justify-self: start;
|
|
44
138
|
border-color: var(--zx-color-accent);
|
|
45
139
|
color: var(--zx-color-accent);
|
|
46
140
|
}
|
|
47
|
-
|
|
48
|
-
@container (max-width: 24rem) {
|
|
49
|
-
.zx-multi-value-editor__row {
|
|
50
|
-
grid-template-columns: minmax(0, 1fr);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.zx-multi-value-editor__controls {
|
|
54
|
-
justify-self: end;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
2
|
import { h } from '../../core/dom.js';
|
|
3
|
+
import { icon } from '../../core/icons.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {Object} MultiValueEditorOptions
|
|
6
7
|
* @property {unknown[]} [values=[]] Initial ordered values.
|
|
7
8
|
* @property {unknown[]|Record<string, string>|null} [options=null] Allowed values; null uses text inputs.
|
|
8
9
|
* @property {string} [addLabel='Add value'] Add-row button text.
|
|
10
|
+
* @property {'both'|'buttons'|'drag'|'none'} [reorder='both'] Reordering affordances each row gets:
|
|
11
|
+
* a drag handle, the up/down buttons, both, or neither.
|
|
12
|
+
* @property {(event: CustomEvent<{values: string[]}>) => void} [onchange] Values change listener.
|
|
9
13
|
*/
|
|
10
14
|
|
|
15
|
+
/** Affordances each `reorder` setting turns on. */
|
|
16
|
+
const REORDER = Object.freeze({
|
|
17
|
+
both: { drag: true, buttons: true },
|
|
18
|
+
buttons: { drag: false, buttons: true },
|
|
19
|
+
drag: { drag: true, buttons: false },
|
|
20
|
+
none: { drag: false, buttons: false }
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Focusable parts of a row, so a re-render can put focus back where the reader left it. */
|
|
24
|
+
const PARTS = Object.freeze({
|
|
25
|
+
value: '.zx-multi-value-editor__value',
|
|
26
|
+
handle: '.zx-multi-value-editor__handle',
|
|
27
|
+
up: '[data-action="up"]',
|
|
28
|
+
down: '[data-action="down"]'
|
|
29
|
+
});
|
|
30
|
+
|
|
11
31
|
/**
|
|
12
32
|
* Ordered value editor with explicit editable rows and controls.
|
|
33
|
+
*
|
|
34
|
+
* Rows reorder by dragging their handle or by pressing the up/down buttons — `reorder` picks which
|
|
35
|
+
* of the two a row offers. The handle is also a keyboard control (arrows, Home, End), so drag-only
|
|
36
|
+
* mode still reorders without a pointer.
|
|
13
37
|
* @fires MultiValueEditor#change
|
|
14
38
|
* @extends {Component<MultiValueEditorOptions>}
|
|
15
39
|
*/
|
|
@@ -17,13 +41,18 @@ export class MultiValueEditor extends Component {
|
|
|
17
41
|
static cssName = 'multi-value-editor';
|
|
18
42
|
|
|
19
43
|
/** @type {MultiValueEditorOptions} */
|
|
20
|
-
static defaults = { values: [], options: null, addLabel: 'Add value' };
|
|
44
|
+
static defaults = { values: [], options: null, addLabel: 'Add value', reorder: 'both' };
|
|
21
45
|
|
|
22
46
|
/** @returns {HTMLElement} */
|
|
23
47
|
render() {
|
|
24
48
|
const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
|
|
49
|
+
this.el = root;
|
|
25
50
|
this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
|
|
26
51
|
this._ownedNodes = [];
|
|
52
|
+
this._initialState = root.getAttribute('data-state');
|
|
53
|
+
this._dragIndex = -1;
|
|
54
|
+
/** @type {HTMLElement|null} Row made draggable by a pointer press on its handle. */
|
|
55
|
+
this._armed = null;
|
|
27
56
|
this._rows = h('div', { class: 'zx-multi-value-editor__rows' });
|
|
28
57
|
this._add = h('button', {
|
|
29
58
|
class: 'zx-btn zx-multi-value-editor__add',
|
|
@@ -34,6 +63,19 @@ export class MultiValueEditor extends Component {
|
|
|
34
63
|
root.append(this._rows, this._add);
|
|
35
64
|
this.listen(root, 'click', (event) => this._handleClick(event));
|
|
36
65
|
this.listen(this._rows, 'change', (event) => this._handleChange(event));
|
|
66
|
+
this.listen(this._rows, 'keydown', (event) => this._handleKeydown(event));
|
|
67
|
+
if (this._reorder().drag) {
|
|
68
|
+
this.listen(this._rows, 'pointerdown', (event) => this._handlePointerDown(event));
|
|
69
|
+
this.listen(this._rows, 'dragstart', (event) => this._handleDragStart(event));
|
|
70
|
+
this.listen(this._rows, 'dragover', (event) => this._handleDragOver(event));
|
|
71
|
+
this.listen(this._rows, 'dragleave', (event) => this._handleDragLeave(event));
|
|
72
|
+
this.listen(this._rows, 'drop', (event) => this._handleDrop(event));
|
|
73
|
+
this.listen(this._rows, 'dragend', () => this._finishDrag());
|
|
74
|
+
// A press that never became a drag still has to disarm the row, and its pointerup can land
|
|
75
|
+
// anywhere — including outside the component.
|
|
76
|
+
this.listen(document, 'pointerup', () => this._disarm());
|
|
77
|
+
this.listen(document, 'pointercancel', () => this._disarm());
|
|
78
|
+
}
|
|
37
79
|
this._renderRows();
|
|
38
80
|
return root;
|
|
39
81
|
}
|
|
@@ -59,16 +101,32 @@ export class MultiValueEditor extends Component {
|
|
|
59
101
|
return this;
|
|
60
102
|
}
|
|
61
103
|
|
|
62
|
-
/** @
|
|
63
|
-
|
|
104
|
+
/** @returns {{drag: boolean, buttons: boolean}} */
|
|
105
|
+
_reorder() {
|
|
106
|
+
return REORDER[this.options.reorder] ?? REORDER.both;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
|
|
111
|
+
* @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
|
|
112
|
+
* @returns {void}
|
|
113
|
+
*/
|
|
114
|
+
_renderRows(focusIndex = -1, focusPart = 'value') {
|
|
64
115
|
const choices = normalizeOptions(this.options.options);
|
|
65
116
|
const useSelect = this.options.options !== null;
|
|
117
|
+
const { drag, buttons } = this._reorder();
|
|
118
|
+
const last = this._values.length - 1;
|
|
66
119
|
const rows = this._values.map((value, index) => {
|
|
120
|
+
// An option list that has lost a stored value still has to round-trip it, or `getValues()`
|
|
121
|
+
// would quietly return something the select never showed.
|
|
122
|
+
const entries = !useSelect || choices.some(([option]) => option === value)
|
|
123
|
+
? choices
|
|
124
|
+
: [[value, value], ...choices];
|
|
67
125
|
const control = useSelect ? h('select', {
|
|
68
126
|
class: 'zx-multi-value-editor__value',
|
|
69
127
|
ariaLabel: `Value ${index + 1}`,
|
|
70
128
|
dataset: { index }
|
|
71
|
-
},
|
|
129
|
+
}, entries.map(([optionValue, label]) => h('option', {
|
|
72
130
|
value: optionValue,
|
|
73
131
|
selected: optionValue === value
|
|
74
132
|
}, label))) : h('input', {
|
|
@@ -79,39 +137,52 @@ export class MultiValueEditor extends Component {
|
|
|
79
137
|
dataset: { index }
|
|
80
138
|
});
|
|
81
139
|
return h('div', { class: 'zx-multi-value-editor__row', dataset: { row: index } },
|
|
140
|
+
drag ? h('button', {
|
|
141
|
+
class: 'zx-multi-value-editor__handle',
|
|
142
|
+
type: 'button',
|
|
143
|
+
ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
|
|
144
|
+
dataset: { action: 'grab', index }
|
|
145
|
+
}, icon('drag', { size: 12 })) : null,
|
|
82
146
|
control,
|
|
83
147
|
h('div', { class: 'zx-multi-value-editor__controls' },
|
|
84
|
-
h('button', {
|
|
148
|
+
buttons ? h('button', {
|
|
85
149
|
class: 'zx-btn',
|
|
86
150
|
type: 'button',
|
|
87
|
-
disabled
|
|
151
|
+
// `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
|
|
152
|
+
// control that disables itself under the reader's finger drops focus to the document.
|
|
153
|
+
ariaDisabled: String(index === 0),
|
|
88
154
|
ariaLabel: `Move value ${index + 1} up`,
|
|
89
155
|
dataset: { action: 'up', index }
|
|
90
|
-
}, '
|
|
91
|
-
h('button', {
|
|
156
|
+
}, icon('chevron-up', { size: 12 })) : null,
|
|
157
|
+
buttons ? h('button', {
|
|
92
158
|
class: 'zx-btn',
|
|
93
159
|
type: 'button',
|
|
94
|
-
|
|
160
|
+
ariaDisabled: String(index === last),
|
|
95
161
|
ariaLabel: `Move value ${index + 1} down`,
|
|
96
162
|
dataset: { action: 'down', index }
|
|
97
|
-
}, '
|
|
163
|
+
}, icon('chevron-down', { size: 12 })) : null,
|
|
98
164
|
h('button', {
|
|
99
165
|
class: 'zx-btn',
|
|
100
166
|
type: 'button',
|
|
101
167
|
ariaLabel: `Remove value ${index + 1}`,
|
|
102
168
|
dataset: { action: 'remove', index }
|
|
103
|
-
}, '
|
|
169
|
+
}, icon('x', { size: 12 }))
|
|
104
170
|
)
|
|
105
171
|
);
|
|
106
172
|
});
|
|
107
173
|
this._rows.replaceChildren(...rows);
|
|
108
|
-
if (focusIndex >= 0)
|
|
174
|
+
if (focusIndex >= 0) {
|
|
175
|
+
const row = rows[Math.min(focusIndex, rows.length - 1)];
|
|
176
|
+
/** @type {HTMLElement|null|undefined} */
|
|
177
|
+
(row?.querySelector(PARTS[focusPart] ?? PARTS.value))?.focus();
|
|
178
|
+
}
|
|
109
179
|
}
|
|
110
180
|
|
|
111
181
|
/** @param {MouseEvent} event @returns {void} */
|
|
112
182
|
_handleClick(event) {
|
|
113
183
|
const button = event.target instanceof Element ? event.target.closest('[data-action]') : null;
|
|
114
184
|
if (!button || !this.el.contains(button)) return;
|
|
185
|
+
if (button.getAttribute('aria-disabled') === 'true') return;
|
|
115
186
|
const action = button.dataset.action;
|
|
116
187
|
const index = Number(button.dataset.index);
|
|
117
188
|
if (action === 'add') {
|
|
@@ -124,24 +195,169 @@ export class MultiValueEditor extends Component {
|
|
|
124
195
|
this._renderRows(Math.min(index, this._values.length - 1));
|
|
125
196
|
this._emitChange();
|
|
126
197
|
} else if ((action === 'up' || action === 'down') && Number.isInteger(index)) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
[this._values[index], this._values[target]] = [this._values[target], this._values[index]];
|
|
130
|
-
this._renderRows(target);
|
|
131
|
-
this._emitChange();
|
|
198
|
+
// Focus stays on the button that moved the row, so the reader can press it again.
|
|
199
|
+
this._move(index, index + (action === 'up' ? -1 : 1), action);
|
|
132
200
|
}
|
|
133
201
|
}
|
|
134
202
|
|
|
135
203
|
/** @param {Event} event @returns {void} */
|
|
136
204
|
_handleChange(event) {
|
|
137
205
|
const control = event.target instanceof Element ? event.target.closest('[data-index]') : null;
|
|
138
|
-
if (!control) return;
|
|
206
|
+
if (!control || !('value' in control)) return;
|
|
139
207
|
const index = Number(control.dataset.index);
|
|
140
208
|
if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
|
|
141
209
|
this._values[index] = control.value;
|
|
142
210
|
this._emitChange();
|
|
143
211
|
}
|
|
144
212
|
|
|
213
|
+
/**
|
|
214
|
+
* The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
|
|
215
|
+
* without a pointer.
|
|
216
|
+
* @param {KeyboardEvent} event
|
|
217
|
+
* @returns {void}
|
|
218
|
+
*/
|
|
219
|
+
_handleKeydown(event) {
|
|
220
|
+
const handle = event.target instanceof Element
|
|
221
|
+
? event.target.closest('.zx-multi-value-editor__handle')
|
|
222
|
+
: null;
|
|
223
|
+
if (!handle) return;
|
|
224
|
+
const index = Number(handle.dataset.index);
|
|
225
|
+
const target = event.key === 'ArrowUp' ? index - 1
|
|
226
|
+
: event.key === 'ArrowDown' ? index + 1
|
|
227
|
+
: event.key === 'Home' ? 0
|
|
228
|
+
: event.key === 'End' ? this._values.length - 1
|
|
229
|
+
: -1;
|
|
230
|
+
if (target < 0 || target >= this._values.length) return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
this._move(index, target, 'handle');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Arms the row for dragging. Only the handle does this, so text selection inside a row's input
|
|
237
|
+
* keeps working and a stray drag cannot reorder anything.
|
|
238
|
+
* @param {PointerEvent} event
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
241
|
+
_handlePointerDown(event) {
|
|
242
|
+
const handle = event.target instanceof Element
|
|
243
|
+
? event.target.closest('.zx-multi-value-editor__handle')
|
|
244
|
+
: null;
|
|
245
|
+
this._disarm();
|
|
246
|
+
if (!handle || handle.disabled) return;
|
|
247
|
+
const row = /** @type {HTMLElement|null} */ (handle.closest('.zx-multi-value-editor__row'));
|
|
248
|
+
if (!row) return;
|
|
249
|
+
row.draggable = true;
|
|
250
|
+
this._armed = row;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** @returns {void} */
|
|
254
|
+
_disarm() {
|
|
255
|
+
if (this._armed) this._armed.draggable = false;
|
|
256
|
+
this._armed = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @param {DragEvent} event @returns {void} */
|
|
260
|
+
_handleDragStart(event) {
|
|
261
|
+
const row = event.target instanceof Element
|
|
262
|
+
? event.target.closest('.zx-multi-value-editor__row')
|
|
263
|
+
: null;
|
|
264
|
+
if (!row || row !== this._armed) {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this._dragIndex = Number(row.dataset.row);
|
|
269
|
+
row.dataset.dragging = 'true';
|
|
270
|
+
this.el.setAttribute('data-state', 'dragging');
|
|
271
|
+
event.dataTransfer?.setData('text/plain', String(this._dragIndex));
|
|
272
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @param {DragEvent} event @returns {void} */
|
|
276
|
+
_handleDragOver(event) {
|
|
277
|
+
if (this._dragIndex < 0) return;
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
|
280
|
+
this._showDrop(this._dropTarget(event.clientY));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** @param {DragEvent} event @returns {void} */
|
|
284
|
+
_handleDragLeave(event) {
|
|
285
|
+
const to = event.relatedTarget;
|
|
286
|
+
if (to instanceof Node && this._rows.contains(to)) return;
|
|
287
|
+
this._clearDrop();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** @param {DragEvent} event @returns {void} */
|
|
291
|
+
_handleDrop(event) {
|
|
292
|
+
if (this._dragIndex < 0) return;
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
const from = this._dragIndex;
|
|
295
|
+
const slot = this._dropTarget(event.clientY);
|
|
296
|
+
this._finishDrag();
|
|
297
|
+
// The slot is an insertion point in the list as it stands; lifting the row out shifts every
|
|
298
|
+
// slot after it down by one.
|
|
299
|
+
this._move(from, slot > from ? slot - 1 : slot, 'handle');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @returns {void} */
|
|
303
|
+
_finishDrag() {
|
|
304
|
+
this._dragIndex = -1;
|
|
305
|
+
this._disarm();
|
|
306
|
+
this._clearDrop();
|
|
307
|
+
for (const row of this._rows.children) delete (/** @type {HTMLElement} */ (row)).dataset.dragging;
|
|
308
|
+
if (this._initialState === null) this.el.removeAttribute('data-state');
|
|
309
|
+
else this.el.setAttribute('data-state', this._initialState);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Insertion slot (0…length) for a pointer at `clientY`.
|
|
314
|
+
* @param {number} clientY Pointer position.
|
|
315
|
+
* @returns {number}
|
|
316
|
+
*/
|
|
317
|
+
_dropTarget(clientY) {
|
|
318
|
+
const rows = [...this._rows.children];
|
|
319
|
+
for (const [index, row] of rows.entries()) {
|
|
320
|
+
const box = row.getBoundingClientRect();
|
|
321
|
+
if (clientY < box.top + box.height / 2) return index;
|
|
322
|
+
}
|
|
323
|
+
return rows.length;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @param {number} slot Insertion slot to mark. @returns {void} */
|
|
327
|
+
_showDrop(slot) {
|
|
328
|
+
const rows = [...this._rows.children];
|
|
329
|
+
// Dropping either side of the row being dragged puts it back where it was; showing a line
|
|
330
|
+
// there would promise a move that does not happen.
|
|
331
|
+
const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
|
|
332
|
+
for (const [index, row] of rows.entries()) {
|
|
333
|
+
const cell = /** @type {HTMLElement} */ (row);
|
|
334
|
+
if (!inert && index === slot) cell.dataset.drop = 'before';
|
|
335
|
+
else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = 'after';
|
|
336
|
+
else delete cell.dataset.drop;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** @returns {void} */
|
|
341
|
+
_clearDrop() {
|
|
342
|
+
for (const row of this._rows.children) delete (/** @type {HTMLElement} */ (row)).dataset.drop;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {number} from Index to move.
|
|
347
|
+
* @param {number} to Destination index.
|
|
348
|
+
* @param {keyof PARTS} part Control of the moved row that takes focus.
|
|
349
|
+
* @returns {boolean} Whether anything moved.
|
|
350
|
+
*/
|
|
351
|
+
_move(from, to, part) {
|
|
352
|
+
const size = this._values.length;
|
|
353
|
+
if (from === to || from < 0 || to < 0 || from >= size || to >= size) return false;
|
|
354
|
+
const [value] = this._values.splice(from, 1);
|
|
355
|
+
this._values.splice(to, 0, value);
|
|
356
|
+
this._renderRows(to, part);
|
|
357
|
+
this._emitChange();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
145
361
|
/** @returns {void} */
|
|
146
362
|
_emitChange() {
|
|
147
363
|
this.emit('change', { values: this.getValues() });
|
|
@@ -149,6 +365,8 @@ export class MultiValueEditor extends Component {
|
|
|
149
365
|
|
|
150
366
|
/** @returns {void} */
|
|
151
367
|
destroy() {
|
|
368
|
+
if (this._initialState === null) this.el?.removeAttribute('data-state');
|
|
369
|
+
else this.el?.setAttribute('data-state', this._initialState);
|
|
152
370
|
for (const node of this._ownedNodes ?? []) node.remove();
|
|
153
371
|
super.destroy();
|
|
154
372
|
}
|
|
@@ -166,5 +384,5 @@ function normalizeOptions(options) {
|
|
|
166
384
|
/**
|
|
167
385
|
* Ordered values changed event.
|
|
168
386
|
* @event MultiValueEditor#change
|
|
169
|
-
* @type {CustomEvent<{values: string[]}>
|
|
387
|
+
* @type {CustomEvent<{values: string[]}>}
|
|
170
388
|
*/
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
.zx-navigation-bar {
|
|
2
2
|
min-inline-size: 0;
|
|
3
3
|
container-type: inline-size;
|
|
4
|
+
/* A size container takes no width from its own contents, so in a shrink-to-fit host — a flex
|
|
5
|
+
row, an inline-block, a grid `auto` track — it would collapse to nothing and spill. Only the
|
|
6
|
+
host can size it, so it always fills the host. That covers every host with a width of its
|
|
7
|
+
own; a host that shrink-wraps *its* contents still collapses, because 100% then resolves
|
|
8
|
+
against a containing block this element's own containment zeroed. Dropping containment is the
|
|
9
|
+
only fix for that — see multi-value-editor.css. */
|
|
10
|
+
inline-size: 100%;
|
|
4
11
|
}
|
|
5
12
|
|
|
6
13
|
.zx-navigation-bar__bar {
|