@fusion.dev/helix-headless 0.154.0 → 0.155.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/custom-elements.json +1427 -88
- package/dist/accordion/item/accordion-item.d.ts +15 -0
- package/dist/accordion/item/accordion-item.js +12 -1
- package/dist/accordion/root/accordion-root.d.ts +2 -1
- package/dist/accordion/root/accordion-root.js +1 -0
- package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
- package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
- package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
- package/dist/button-group/root/button-group-root.d.ts +2 -0
- package/dist/button-group/root/button-group-root.js +1 -0
- package/dist/color-input/root/color-input-root.d.ts +9 -3
- package/dist/color-input/root/color-input-root.js +3 -1
- package/dist/color-picker/channels.d.ts +5 -3
- package/dist/color-picker/channels.js +79 -21
- package/dist/color-picker/root/color-picker-root.d.ts +58 -5
- package/dist/color-picker/root/color-picker-root.js +75 -13
- package/dist/date-input/root/date-input-root.d.ts +1 -0
- package/dist/date-input/root/date-input-root.js +2 -1
- package/dist/date-picker/root/date-picker-root.d.ts +43 -13
- package/dist/date-picker/root/date-picker-root.js +54 -31
- package/dist/element-events.generated.js +2 -1
- package/dist/navigation/root/navigation-root.d.ts +26 -0
- package/dist/navigation/root/navigation-root.js +58 -1
- package/dist/select/root/select-root.d.ts +29 -3
- package/dist/select/root/select-root.js +34 -9
- package/dist/slider/root/slider-root.d.ts +71 -9
- package/dist/slider/root/slider-root.js +129 -42
- package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
- package/dist/speed-dial/item/speed-dial-item.js +48 -7
- package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
- package/dist/speed-dial/root/speed-dial-root.js +50 -5
- package/dist/splitter/root/splitter-root.d.ts +22 -0
- package/dist/splitter/root/splitter-root.js +36 -7
- package/dist/steps/reach.d.ts +5 -1
- package/dist/steps/reach.js +2 -2
- package/dist/steps/root/steps-root.d.ts +37 -2
- package/dist/steps/root/steps-root.js +49 -7
- package/dist/steps/step/step.d.ts +6 -0
- package/dist/steps/step/step.js +4 -0
- package/dist/tabs/root/tabs-root.d.ts +37 -0
- package/dist/tabs/root/tabs-root.js +92 -2
- package/dist/tabs/tab/tab.d.ts +38 -2
- package/dist/tabs/tab/tab.js +45 -4
- package/dist/tag/group/tag-group.d.ts +29 -3
- package/dist/tag/group/tag-group.js +42 -11
- package/dist/toggle/root/toggle-root.d.ts +2 -0
- package/dist/toggle/root/toggle-root.js +1 -1
- package/dist/tree/item/tree-item.d.ts +10 -0
- package/dist/tree/item/tree-item.js +12 -0
- package/dist/tree/root/tree-root.d.ts +40 -1
- package/dist/tree/root/tree-root.js +120 -4
- package/dist/tree/selection.d.ts +6 -0
- package/dist/tree/selection.js +11 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +984 -151
- package/types/vue.d.ts +221 -49
- package/types/words.d.ts +36 -1
|
@@ -12,7 +12,11 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
12
12
|
constructor() {
|
|
13
13
|
super(...arguments);
|
|
14
14
|
this.disabled = false;
|
|
15
|
+
this.download = false;
|
|
16
|
+
this.href = '';
|
|
17
|
+
this.rel = '';
|
|
15
18
|
this.shortcut = '';
|
|
19
|
+
this.target = '';
|
|
16
20
|
this.value = '';
|
|
17
21
|
this.isSpeedDialItem = true;
|
|
18
22
|
this.hasShortcut = false;
|
|
@@ -22,6 +26,16 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
22
26
|
this.handleClick = () => {
|
|
23
27
|
this.choose();
|
|
24
28
|
};
|
|
29
|
+
this.handleKeydown = (event) => {
|
|
30
|
+
if (event.key !== ' ') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const link = this.renderRoot.querySelector('a[part="item"]');
|
|
34
|
+
if (link) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
link.click();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
25
39
|
}
|
|
26
40
|
focus(options) {
|
|
27
41
|
this.partNamed('item')?.focus(options);
|
|
@@ -55,6 +69,27 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
55
69
|
}
|
|
56
70
|
render() {
|
|
57
71
|
const keys = this.shortcut === '' ? nothing : ariaChord(this.shortcut, !this.hydrating && onApple());
|
|
72
|
+
const inside = html `<span part="icon"><slot name="icon"></slot></span>
|
|
73
|
+
<span part="label"><slot></slot><span
|
|
74
|
+
part="shortcut"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
?hidden=${!this.hasShortcut && this.shortcut === ''}
|
|
77
|
+
>${this.renderShortcut()}</span
|
|
78
|
+
></span>`;
|
|
79
|
+
if (this.href !== '' && !this.disabled) {
|
|
80
|
+
return html `<a
|
|
81
|
+
part="item"
|
|
82
|
+
href=${this.href}
|
|
83
|
+
target=${this.target || nothing}
|
|
84
|
+
rel=${this.rel || nothing}
|
|
85
|
+
?download=${this.download}
|
|
86
|
+
role="menuitem"
|
|
87
|
+
aria-keyshortcuts=${keys}
|
|
88
|
+
tabindex="-1"
|
|
89
|
+
@click=${this.handleClick}
|
|
90
|
+
@keydown=${this.handleKeydown}
|
|
91
|
+
>${inside}</a>`;
|
|
92
|
+
}
|
|
58
93
|
return html `<button
|
|
59
94
|
part="item"
|
|
60
95
|
role="menuitem"
|
|
@@ -65,13 +100,7 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
65
100
|
tabindex="-1"
|
|
66
101
|
@click=${this.handleClick}
|
|
67
102
|
>
|
|
68
|
-
|
|
69
|
-
<span part="label"><slot></slot><span
|
|
70
|
-
part="shortcut"
|
|
71
|
-
aria-hidden="true"
|
|
72
|
-
?hidden=${!this.hasShortcut && this.shortcut === ''}
|
|
73
|
-
>${this.renderShortcut()}</span
|
|
74
|
-
></span>
|
|
103
|
+
${inside}
|
|
75
104
|
</button>`;
|
|
76
105
|
}
|
|
77
106
|
renderShortcut() {
|
|
@@ -89,9 +118,21 @@ let HelixHeadlessSpeedDialItem = class HelixHeadlessSpeedDialItem extends HelixE
|
|
|
89
118
|
__decorate([
|
|
90
119
|
property({ reflect: true, type: Boolean })
|
|
91
120
|
], HelixHeadlessSpeedDialItem.prototype, "disabled", void 0);
|
|
121
|
+
__decorate([
|
|
122
|
+
property({ type: Boolean })
|
|
123
|
+
], HelixHeadlessSpeedDialItem.prototype, "download", void 0);
|
|
124
|
+
__decorate([
|
|
125
|
+
property()
|
|
126
|
+
], HelixHeadlessSpeedDialItem.prototype, "href", void 0);
|
|
127
|
+
__decorate([
|
|
128
|
+
property()
|
|
129
|
+
], HelixHeadlessSpeedDialItem.prototype, "rel", void 0);
|
|
92
130
|
__decorate([
|
|
93
131
|
property()
|
|
94
132
|
], HelixHeadlessSpeedDialItem.prototype, "shortcut", void 0);
|
|
133
|
+
__decorate([
|
|
134
|
+
property()
|
|
135
|
+
], HelixHeadlessSpeedDialItem.prototype, "target", void 0);
|
|
95
136
|
__decorate([
|
|
96
137
|
property()
|
|
97
138
|
], HelixHeadlessSpeedDialItem.prototype, "value", void 0);
|
|
@@ -25,8 +25,9 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
|
|
|
25
25
|
* action whose name starts with it. The actions are one tab stop, and Tab
|
|
26
26
|
* closes the dial and moves on.
|
|
27
27
|
*
|
|
28
|
-
* **Keyboard.** On the trigger, Enter, Space or a press opens the dial
|
|
29
|
-
* first action
|
|
28
|
+
* **Keyboard.** On the trigger, Enter, Space, Down or a press opens the dial
|
|
29
|
+
* on its first action, and Up opens it on its last, as an APG menu button
|
|
30
|
+
* does. With the dial open:
|
|
30
31
|
*
|
|
31
32
|
* | Key | What it does |
|
|
32
33
|
* |---|---|
|
|
@@ -50,6 +51,13 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
|
|
|
50
51
|
* and a slotted one alike. A press that has opened the dial does not also do
|
|
51
52
|
* the trigger's action.
|
|
52
53
|
*
|
|
54
|
+
* **Open on hover.** With `open-on="hover"`, a mouse resting on the trigger
|
|
55
|
+
* opens the dial, as Material's speed dial does, and the dial closes a moment
|
|
56
|
+
* after the mouse leaves both the trigger and the actions. A press, a touch
|
|
57
|
+
* and the keyboard open it as under `click`, and a press on the trigger of a
|
|
58
|
+
* dial already open moves focus to its first action, so the press a mouse
|
|
59
|
+
* makes after hovering keeps the dial open.
|
|
60
|
+
*
|
|
53
61
|
* @since 0.1.0
|
|
54
62
|
* @status stable
|
|
55
63
|
* @category actions
|
|
@@ -57,6 +65,7 @@ declare const HelixHeadlessSpeedDial_base: (new (...args: any[]) => import("@fus
|
|
|
57
65
|
* @documentation https://fusion.dev/helix/components/speed-dial
|
|
58
66
|
*
|
|
59
67
|
* @slot - The actions.
|
|
68
|
+
* @slot icon - Replaces the plus on the button the dial draws when no trigger is slotted, such as with a pencil for a dial of ways to write.
|
|
60
69
|
* @slot trigger - The control that opens the actions. Without one, a button named by `label` is drawn.
|
|
61
70
|
*
|
|
62
71
|
* @event {HelixBeforeCloseEvent} helix-before-close - The actions are about to close. Cancelable: cancel it to keep them open.
|
|
@@ -94,11 +103,13 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
|
|
|
94
103
|
*/
|
|
95
104
|
offset: number;
|
|
96
105
|
/**
|
|
97
|
-
* What opens the dial. `click` opens it on a press of the trigger.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
106
|
+
* What opens the dial. `click` opens it on a press of the trigger. `hover`
|
|
107
|
+
* also opens it while a mouse rests on the trigger, and closes it once the
|
|
108
|
+
* mouse has left. Under `long-press`, a press is the trigger's own action,
|
|
109
|
+
* and the dial opens on a press held for half a second, or on Alt+Down or
|
|
110
|
+
* Alt+Up.
|
|
100
111
|
*/
|
|
101
|
-
openOn: 'click' | 'long-press';
|
|
112
|
+
openOn: 'click' | 'hover' | 'long-press';
|
|
102
113
|
/**
|
|
103
114
|
* The direction the actions open in. They move to another side if that side
|
|
104
115
|
* does not fit.
|
|
@@ -117,6 +128,10 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
|
|
|
117
128
|
* wherever the reader is typing.
|
|
118
129
|
*/
|
|
119
130
|
private focusOnOpen;
|
|
131
|
+
/** Whether the open under way puts focus on the last action, from Up. */
|
|
132
|
+
private focusLast;
|
|
133
|
+
/** The close a mouse leaving a hover dial has asked for, a moment from now. */
|
|
134
|
+
private leaving;
|
|
120
135
|
protected readonly panel: PopoverController;
|
|
121
136
|
/** Whatever the consumer slotted, or the button rendered in its place. */
|
|
122
137
|
private get trigger();
|
|
@@ -128,6 +143,17 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
|
|
|
128
143
|
/** Watches the trigger for a held press, under `open-on="long-press"`. */
|
|
129
144
|
private readonly longPress;
|
|
130
145
|
private get pressHeld();
|
|
146
|
+
/** Opens on a mouse coming over the trigger, under `open-on="hover"`. */
|
|
147
|
+
private readonly handleTriggerPointerOver;
|
|
148
|
+
/** Keeps a hover dial open while the mouse is back over it. */
|
|
149
|
+
private readonly handlePointerEnter;
|
|
150
|
+
/**
|
|
151
|
+
* Closes a hover dial a moment after the mouse leaves it, so a mouse
|
|
152
|
+
* crossing the gap between the trigger and the actions keeps it open.
|
|
153
|
+
* Focus inside the dial keeps it open too, as a reader is using it.
|
|
154
|
+
*/
|
|
155
|
+
private readonly handlePointerLeave;
|
|
156
|
+
private stay;
|
|
131
157
|
/**
|
|
132
158
|
* Puts focus on the trigger, the slotted one or the button drawn in its
|
|
133
159
|
* place, as a `focus()` on a native control would. The host itself takes no
|
|
@@ -31,6 +31,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
31
31
|
typeahead: typeaheadByWords(),
|
|
32
32
|
});
|
|
33
33
|
this.focusOnOpen = false;
|
|
34
|
+
this.focusLast = false;
|
|
34
35
|
this.panel = new PopoverController(this, {
|
|
35
36
|
anchor: () => this.trigger,
|
|
36
37
|
boundary: () => this,
|
|
@@ -47,7 +48,35 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
});
|
|
51
|
+
this.handleTriggerPointerOver = (event) => {
|
|
52
|
+
if (this.openOn !== 'hover' || event.pointerType !== 'mouse') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.stay();
|
|
56
|
+
if (!this.open) {
|
|
57
|
+
this.requestOpen(true);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
this.handlePointerEnter = () => {
|
|
61
|
+
this.stay();
|
|
62
|
+
};
|
|
63
|
+
this.handlePointerLeave = (event) => {
|
|
64
|
+
if (this.openOn !== 'hover' || event.pointerType !== 'mouse' || !this.open) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.stay();
|
|
68
|
+
this.leaving = setTimeout(() => {
|
|
69
|
+
this.leaving = undefined;
|
|
70
|
+
if (this.open && !this.matches(':focus-within')) {
|
|
71
|
+
this.requestOpen(false);
|
|
72
|
+
}
|
|
73
|
+
}, 300);
|
|
74
|
+
};
|
|
50
75
|
this.handleTriggerClick = () => {
|
|
76
|
+
if (this.open && this.openOn === 'hover') {
|
|
77
|
+
this.items.find((one) => !one.disabled)?.focusItem();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
51
80
|
if (this.open) {
|
|
52
81
|
this.requestOpen(false);
|
|
53
82
|
return;
|
|
@@ -91,11 +120,11 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
91
120
|
};
|
|
92
121
|
this.handleKeydown = (event) => {
|
|
93
122
|
if (!this.open) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
(event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
123
|
+
const arrow = event.key === 'ArrowDown' || event.key === 'ArrowUp';
|
|
124
|
+
if (arrow && (this.pressHeld ? event.altKey : !event.altKey)) {
|
|
97
125
|
event.preventDefault();
|
|
98
126
|
this.focusOnOpen = this.requestOpen(true);
|
|
127
|
+
this.focusLast = this.focusOnOpen && event.key === 'ArrowUp';
|
|
99
128
|
}
|
|
100
129
|
return;
|
|
101
130
|
}
|
|
@@ -132,6 +161,10 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
132
161
|
get pressHeld() {
|
|
133
162
|
return this.openOn === 'long-press';
|
|
134
163
|
}
|
|
164
|
+
stay() {
|
|
165
|
+
clearTimeout(this.leaving);
|
|
166
|
+
this.leaving = undefined;
|
|
167
|
+
}
|
|
135
168
|
focus(options) {
|
|
136
169
|
const trigger = this.trigger;
|
|
137
170
|
if (trigger === this) {
|
|
@@ -151,6 +184,8 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
151
184
|
this.addEventListener('helix-select', this.handleSelect);
|
|
152
185
|
this.addEventListener('keydown', this.handleKeydown);
|
|
153
186
|
this.addEventListener('click', this.handleHeldClick, true);
|
|
187
|
+
this.addEventListener('pointerenter', this.handlePointerEnter);
|
|
188
|
+
this.addEventListener('pointerleave', this.handlePointerLeave);
|
|
154
189
|
}
|
|
155
190
|
disconnectedCallback() {
|
|
156
191
|
super.disconnectedCallback();
|
|
@@ -162,6 +197,9 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
162
197
|
this.removeEventListener('helix-select', this.handleSelect);
|
|
163
198
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
164
199
|
this.removeEventListener('click', this.handleHeldClick, true);
|
|
200
|
+
this.removeEventListener('pointerenter', this.handlePointerEnter);
|
|
201
|
+
this.removeEventListener('pointerleave', this.handlePointerLeave);
|
|
202
|
+
this.stay();
|
|
165
203
|
}
|
|
166
204
|
announce() {
|
|
167
205
|
const trigger = this.trigger;
|
|
@@ -191,11 +229,17 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
191
229
|
super.updated?.(changed);
|
|
192
230
|
this.announce();
|
|
193
231
|
this.describeTrigger();
|
|
232
|
+
if (!this.open) {
|
|
233
|
+
this.stay();
|
|
234
|
+
}
|
|
194
235
|
if (this.open && this.focusOnOpen) {
|
|
195
236
|
this.focusOnOpen = false;
|
|
237
|
+
const last = this.focusLast;
|
|
238
|
+
this.focusLast = false;
|
|
196
239
|
this.updateComplete.then(() => {
|
|
197
240
|
if (this.open) {
|
|
198
|
-
this.items.
|
|
241
|
+
const usable = this.items.filter((one) => !one.disabled);
|
|
242
|
+
(last ? usable.at(-1) : usable[0])?.focusItem();
|
|
199
243
|
}
|
|
200
244
|
});
|
|
201
245
|
}
|
|
@@ -209,6 +253,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
209
253
|
@focusin=${this.handleTriggerFocusIn}
|
|
210
254
|
@pointercancel=${this.handleTriggerPointerCancel}
|
|
211
255
|
@pointerdown=${this.handleTriggerPointerDown}
|
|
256
|
+
@pointerover=${this.handleTriggerPointerOver}
|
|
212
257
|
@pointermove=${this.handleTriggerPointerMove}
|
|
213
258
|
@pointerup=${this.handleTriggerPointerUp}
|
|
214
259
|
@slotchange=${() => this.describeTrigger()}
|
|
@@ -222,7 +267,7 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
|
|
|
222
267
|
aria-label=${this.label}
|
|
223
268
|
>
|
|
224
269
|
${''}
|
|
225
|
-
|
|
270
|
+
<slot name="icon">${drawIcon(plus)}</slot>
|
|
226
271
|
</button>
|
|
227
272
|
${held ? html `<span id="long-press" hidden>${this.longPressLabel}</span>` : nothing}
|
|
228
273
|
</slot>`;
|
|
@@ -35,6 +35,13 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
35
35
|
* takes all of the change, as Web Awesome's split panel does. Without it,
|
|
36
36
|
* both panes keep their share.
|
|
37
37
|
*
|
|
38
|
+
* **Three or more panes along one axis are splitters nested on that axis**, a
|
|
39
|
+
* splitter in the `end` pane of another for three columns. The nested one
|
|
40
|
+
* keeps the pane on its far side the size it was while the outer divider
|
|
41
|
+
* moves, so a divider resizes only the two panes beside it, as Zag's splitter
|
|
42
|
+
* does with any number of panels. Where the whole row is resized, every pane
|
|
43
|
+
* keeps its share, unless `primary` says otherwise.
|
|
44
|
+
*
|
|
38
45
|
* **Keyboard.** The divider is one tab stop.
|
|
39
46
|
*
|
|
40
47
|
* | Key | What it does |
|
|
@@ -213,6 +220,21 @@ export declare class HelixHeadlessSplitter extends HelixElement {
|
|
|
213
220
|
primary?: 'end' | 'start';
|
|
214
221
|
/** Watches this element's length along its axis while `primary` is set. */
|
|
215
222
|
private lengths;
|
|
223
|
+
/**
|
|
224
|
+
* The splitter this one is nested in on the same axis, and the pane of it
|
|
225
|
+
* this one is in, for a row of three or more panes.
|
|
226
|
+
*/
|
|
227
|
+
private chain;
|
|
228
|
+
/** The length of the splitter this one is chained to, last seen. */
|
|
229
|
+
private chainLength;
|
|
230
|
+
/**
|
|
231
|
+
* The pane that keeps its pixels as this splitter's length changes:
|
|
232
|
+
* `primary`, or the far pane of one chained into another, whose divider
|
|
233
|
+
* would otherwise drag this one's along with it.
|
|
234
|
+
*/
|
|
235
|
+
private get held();
|
|
236
|
+
/** The chained splitter's length along the shared axis, in pixels. */
|
|
237
|
+
private measureChain;
|
|
216
238
|
/** The length along the axis last seen, in pixels. */
|
|
217
239
|
private length;
|
|
218
240
|
/**
|
|
@@ -70,8 +70,15 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
70
70
|
this.reached = undefined;
|
|
71
71
|
this.requestUpdate('position', previous);
|
|
72
72
|
}
|
|
73
|
+
get held() {
|
|
74
|
+
return this.primary ?? this.chain?.side;
|
|
75
|
+
}
|
|
76
|
+
measureChain() {
|
|
77
|
+
const box = this.chain?.by.getBoundingClientRect();
|
|
78
|
+
return box ? (this.vertical ? box.height : box.width) : 0;
|
|
79
|
+
}
|
|
73
80
|
watchLength() {
|
|
74
|
-
if (!this.
|
|
81
|
+
if (!this.held || typeof ResizeObserver === 'undefined') {
|
|
75
82
|
this.lengths?.disconnect();
|
|
76
83
|
this.lengths = undefined;
|
|
77
84
|
this.length = 0;
|
|
@@ -92,17 +99,26 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
92
99
|
holdPrimary(length) {
|
|
93
100
|
const before = this.length;
|
|
94
101
|
this.length = length;
|
|
95
|
-
|
|
102
|
+
const held = this.held;
|
|
103
|
+
if (!this.primary && this.chain) {
|
|
104
|
+
const outer = this.measureChain();
|
|
105
|
+
const outerBefore = this.chainLength;
|
|
106
|
+
this.chainLength = outer;
|
|
107
|
+
if (outer !== outerBefore) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!held || before <= 0 || length <= 0 || length === before) {
|
|
96
112
|
return;
|
|
97
113
|
}
|
|
98
114
|
if (this.collapsed || this.dragging) {
|
|
99
115
|
return;
|
|
100
116
|
}
|
|
101
117
|
const position = this.position;
|
|
102
|
-
const kept = ((
|
|
118
|
+
const kept = ((held === 'start' ? position : 100 - position) / 100) * before;
|
|
103
119
|
const share = (kept / length) * 100;
|
|
104
120
|
const previous = position;
|
|
105
|
-
this.positionState = this.clamp(
|
|
121
|
+
this.positionState = this.clamp(held === 'start' ? share : 100 - share);
|
|
106
122
|
this.requestUpdate('position', previous);
|
|
107
123
|
}
|
|
108
124
|
get showing() {
|
|
@@ -261,6 +277,8 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
261
277
|
});
|
|
262
278
|
this.max = 100;
|
|
263
279
|
this.min = 0;
|
|
280
|
+
this.chain = null;
|
|
281
|
+
this.chainLength = 0;
|
|
264
282
|
this.length = 0;
|
|
265
283
|
this.handlePaneKeys = (event) => {
|
|
266
284
|
if (event.key !== 'F6' ||
|
|
@@ -484,7 +502,17 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
484
502
|
}
|
|
485
503
|
findOuter() {
|
|
486
504
|
const outer = closestInstance(this.parentElement, HelixHeadlessSplitter_1);
|
|
487
|
-
|
|
505
|
+
this.chain = null;
|
|
506
|
+
if (outer && outer !== this && outer.vertical === this.vertical) {
|
|
507
|
+
let node = this;
|
|
508
|
+
while (node.parentElement && node.parentElement !== outer) {
|
|
509
|
+
node = node.parentElement;
|
|
510
|
+
}
|
|
511
|
+
this.chain = { by: outer, side: node.slot === 'start' ? 'start' : 'end' };
|
|
512
|
+
this.chainLength = this.measureChain();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (!outer || outer === this) {
|
|
488
516
|
return;
|
|
489
517
|
}
|
|
490
518
|
this.outer = outer;
|
|
@@ -499,8 +527,8 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
499
527
|
super.connectedCallback();
|
|
500
528
|
if (this.hasUpdated) {
|
|
501
529
|
this.watchWidth();
|
|
502
|
-
this.watchLength();
|
|
503
530
|
this.findOuter();
|
|
531
|
+
this.watchLength();
|
|
504
532
|
}
|
|
505
533
|
this.addEventListener('keydown', this.handlePaneKeys);
|
|
506
534
|
}
|
|
@@ -512,6 +540,7 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
512
540
|
this.lengths?.disconnect();
|
|
513
541
|
this.lengths = undefined;
|
|
514
542
|
this.length = 0;
|
|
543
|
+
this.chain = null;
|
|
515
544
|
if (this.outer) {
|
|
516
545
|
channel.get(this.outer)?.uncross(this);
|
|
517
546
|
}
|
|
@@ -521,8 +550,8 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
|
|
|
521
550
|
firstUpdated(changed) {
|
|
522
551
|
super.firstUpdated(changed);
|
|
523
552
|
this.watchWidth();
|
|
524
|
-
this.watchLength();
|
|
525
553
|
this.findOuter();
|
|
554
|
+
this.watchLength();
|
|
526
555
|
this.finishHydration();
|
|
527
556
|
}
|
|
528
557
|
updated(changed) {
|
package/dist/steps/reach.d.ts
CHANGED
|
@@ -11,8 +11,12 @@ export interface ReachableStep {
|
|
|
11
11
|
* Back is always allowed, as Ark UI's linear steps allow it. Forward is allowed
|
|
12
12
|
* only where every stage from the one the reader is on up to the target is
|
|
13
13
|
* complete or optional, so no stage is passed unfinished.
|
|
14
|
+
*
|
|
15
|
+
* `passing` is a sequence whose stages are complete once a reader moves past
|
|
16
|
+
* them, so the stage the reader is on counts as finished and the next one is
|
|
17
|
+
* in reach.
|
|
14
18
|
*/
|
|
15
|
-
export declare const canReach: (steps: readonly ReachableStep[], from: number, to: number) => boolean;
|
|
19
|
+
export declare const canReach: (steps: readonly ReachableStep[], from: number, to: number, passing?: boolean) => boolean;
|
|
16
20
|
/**
|
|
17
21
|
* How far through a sequence is, from 0 to 100: the share of its stages that
|
|
18
22
|
* are complete, rounded to a whole number. An empty sequence is at 0.
|
package/dist/steps/reach.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const canReach = (steps, from, to) => {
|
|
1
|
+
export const canReach = (steps, from, to, passing = false) => {
|
|
2
2
|
if (to <= from) {
|
|
3
3
|
return true;
|
|
4
4
|
}
|
|
5
|
-
for (let at = Math.max(from, 0); at < to; at += 1) {
|
|
5
|
+
for (let at = Math.max(passing ? from + 1 : from, 0); at < to; at += 1) {
|
|
6
6
|
const step = steps[at];
|
|
7
7
|
if (step && !step.complete && !step.optional) {
|
|
8
8
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HelixElement } from '@fusion.dev/helix-core';
|
|
1
|
+
import { HelixElement, KeyboardNavigationController } from '@fusion.dev/helix-core';
|
|
2
2
|
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
3
|
import type { HelixHeadlessStep } from '../step/step.js';
|
|
4
4
|
declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & typeof HelixElement;
|
|
@@ -18,7 +18,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
|
|
|
18
18
|
* UI's linear steps do: a stage ahead is out of reach, and its button disabled,
|
|
19
19
|
* until every stage before it is `complete` or `optional`. Going back is always
|
|
20
20
|
* allowed. `next()` and `previous()` move one stage either way, under the
|
|
21
|
-
* same rules, for your own buttons.
|
|
21
|
+
* same rules, for your own buttons. `reset()` goes back to the first stage and
|
|
22
|
+
* clears every stage's `complete`, as Ark UI's `resetStep` does.
|
|
23
|
+
*
|
|
24
|
+
* `completion="passed"` marks a stage `complete` once a reader moves past it,
|
|
25
|
+
* as Ark UI counts a stage before the current one as completed. Going back
|
|
26
|
+
* leaves it complete. In a `linear` sequence the next stage is then in reach
|
|
27
|
+
* from the current one, and `validate` decides whether the reader may go.
|
|
22
28
|
*
|
|
23
29
|
* To check a stage before a reader moves on from it, set `validate` to a
|
|
24
30
|
* function that answers whether the stage is valid, as Ark UI's `isStepValid`
|
|
@@ -39,6 +45,13 @@ declare const HelixHeadlessSteps_base: (new (...args: any[]) => import("@fusion.
|
|
|
39
45
|
* current stage's button says `aria-current="step"`, and a finished stage's
|
|
40
46
|
* button is described by its `complete-label`.
|
|
41
47
|
*
|
|
48
|
+
* **Keyboard.** Each stage's name is a button and its own tab stop, and Enter
|
|
49
|
+
* or Space goes to the stage. Set `arrows` and the arrow keys move focus
|
|
50
|
+
* between the names as well, as Ark UI's steps do: Left and Right along a row,
|
|
51
|
+
* Up and Down a column, Home and End to the first and the last. The arrows
|
|
52
|
+
* pass over a disabled stage and one a linear sequence holds out of reach,
|
|
53
|
+
* and they move focus without going to the stage.
|
|
54
|
+
*
|
|
42
55
|
* @since 0.1.0
|
|
43
56
|
* @status stable
|
|
44
57
|
* @category navigation
|
|
@@ -63,6 +76,20 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
|
|
|
63
76
|
* `registerWords('de', { 'helix-steps': { progressLabel: '{share, number, percent} erledigt' } })`.
|
|
64
77
|
*/
|
|
65
78
|
static words: string;
|
|
79
|
+
/**
|
|
80
|
+
* Whether the arrow keys move focus between the stages' names, along the
|
|
81
|
+
* way the steps run. Off by default, so the arrows keep scrolling the page.
|
|
82
|
+
*/
|
|
83
|
+
arrows: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* What marks a stage `complete`.
|
|
86
|
+
*
|
|
87
|
+
* Absent, you do: each step's `complete` is yours, because a reader who
|
|
88
|
+
* steps back has finished more stages than the one they are on. `passed`
|
|
89
|
+
* marks every stage before the one a reader moves to, and leaves the marks
|
|
90
|
+
* when they go back.
|
|
91
|
+
*/
|
|
92
|
+
completion?: 'passed';
|
|
66
93
|
/**
|
|
67
94
|
* The sequence's name, read by screen readers.
|
|
68
95
|
*
|
|
@@ -120,6 +147,7 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
|
|
|
120
147
|
* a reader is told to come back to by number.
|
|
121
148
|
*/
|
|
122
149
|
variant?: 'numeric';
|
|
150
|
+
protected readonly keys: KeyboardNavigationController;
|
|
123
151
|
/** The steps, in the order the consumer wrote them. */
|
|
124
152
|
private get steps();
|
|
125
153
|
/** The index of the stage the reader is on, or `-1`. */
|
|
@@ -132,6 +160,13 @@ export declare class HelixHeadlessSteps extends HelixHeadlessSteps_base {
|
|
|
132
160
|
next(): boolean;
|
|
133
161
|
/** Moves to the stage before, as Ark UI's `goToPrevStep` does. Returns whether it moved. */
|
|
134
162
|
previous(): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Goes back to the first stage that can be gone to, and clears `complete`
|
|
165
|
+
* and the invalid mark from every stage, as Ark UI's `resetStep` does.
|
|
166
|
+
* Returns whether it moved: a listener can refuse the move with
|
|
167
|
+
* `helix-before-activate`, and the stages then keep their marks.
|
|
168
|
+
*/
|
|
169
|
+
reset(): boolean;
|
|
135
170
|
/** Moves `delta` stages, skipping disabled ones. */
|
|
136
171
|
private step;
|
|
137
172
|
/**
|
|
@@ -4,16 +4,35 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { customElement, formatWords, HelixElement, hostAria, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { customElement, focusItemOf, formatWords, HelixElement, hostAria, KeyboardNavigationController, MixinControllerActive, whenHydrated, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { canReach, progressOf } from '../reach.js';
|
|
11
|
+
const navigable = (step) => {
|
|
12
|
+
const label = () => step.shadowRoot?.querySelector('[part="label"]');
|
|
13
|
+
return focusItemOf(step, {
|
|
14
|
+
disabled: step.disabled || step.locked,
|
|
15
|
+
focus: () => step.focus(),
|
|
16
|
+
owns: (node) => {
|
|
17
|
+
const control = label();
|
|
18
|
+
return Boolean(control && node && (node === control || control.contains(node)));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
};
|
|
11
22
|
let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(HelixElement) {
|
|
12
23
|
constructor() {
|
|
13
24
|
super(...arguments);
|
|
25
|
+
this.arrows = false;
|
|
14
26
|
this.label = 'Steps';
|
|
15
27
|
this.linear = false;
|
|
16
28
|
this.progressLabel = '{share, number, percent} complete';
|
|
29
|
+
this.keys = new KeyboardNavigationController(this, {
|
|
30
|
+
items: () => (this.arrows ? this.steps.map(navigable) : []),
|
|
31
|
+
origin: 'focus',
|
|
32
|
+
reserved: () => this.orientation === 'vertical' ? ['ArrowLeft', 'ArrowRight'] : ['ArrowDown', 'ArrowUp'],
|
|
33
|
+
selects: () => false,
|
|
34
|
+
wrap: false,
|
|
35
|
+
});
|
|
17
36
|
this.handleSelection = (event) => {
|
|
18
37
|
if (event.defaultPrevented || !this.steps.includes(event.target)) {
|
|
19
38
|
return;
|
|
@@ -45,6 +64,18 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
|
|
|
45
64
|
previous() {
|
|
46
65
|
return this.step(-1);
|
|
47
66
|
}
|
|
67
|
+
reset() {
|
|
68
|
+
const steps = this.steps;
|
|
69
|
+
const first = steps.find((one) => !one.disabled);
|
|
70
|
+
if (!first || !this.requestActive(first.value)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
for (const one of steps) {
|
|
74
|
+
one.complete = false;
|
|
75
|
+
one.invalid = false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
48
79
|
step(delta) {
|
|
49
80
|
const steps = this.steps;
|
|
50
81
|
let index = this.at + delta;
|
|
@@ -67,7 +98,7 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
|
|
|
67
98
|
return valid;
|
|
68
99
|
}
|
|
69
100
|
reachable(index) {
|
|
70
|
-
return !this.linear || canReach(this.steps, this.at, index);
|
|
101
|
+
return !this.linear || canReach(this.steps, this.at, index, this.completion === 'passed');
|
|
71
102
|
}
|
|
72
103
|
lock() {
|
|
73
104
|
const steps = this.steps;
|
|
@@ -84,12 +115,17 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
|
|
|
84
115
|
}
|
|
85
116
|
const steps = this.steps;
|
|
86
117
|
const value = this.resolveActive(steps)?.value ?? '';
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
const reached = steps.findIndex((step) => step.value === value);
|
|
119
|
+
steps.forEach((step, index) => {
|
|
120
|
+
const current = index === reached;
|
|
121
|
+
const passed = this.completion === 'passed' && index < reached;
|
|
89
122
|
whenHydrated(step, () => {
|
|
90
123
|
step.current = current;
|
|
124
|
+
if (passed) {
|
|
125
|
+
step.complete = true;
|
|
126
|
+
}
|
|
91
127
|
});
|
|
92
|
-
}
|
|
128
|
+
});
|
|
93
129
|
}
|
|
94
130
|
connectedCallback() {
|
|
95
131
|
super.connectedCallback();
|
|
@@ -125,10 +161,10 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
|
|
|
125
161
|
}
|
|
126
162
|
updated(changed) {
|
|
127
163
|
super.updated(changed);
|
|
128
|
-
if (changed.has('active')) {
|
|
164
|
+
if (changed.has('active') || changed.has('completion')) {
|
|
129
165
|
this.sync();
|
|
130
166
|
}
|
|
131
|
-
if (changed.has('linear') || changed.has('active')) {
|
|
167
|
+
if (changed.has('linear') || changed.has('active') || changed.has('completion')) {
|
|
132
168
|
this.lock();
|
|
133
169
|
}
|
|
134
170
|
}
|
|
@@ -148,6 +184,12 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
|
|
|
148
184
|
></div>`;
|
|
149
185
|
}
|
|
150
186
|
};
|
|
187
|
+
__decorate([
|
|
188
|
+
property({ type: Boolean })
|
|
189
|
+
], HelixHeadlessSteps.prototype, "arrows", void 0);
|
|
190
|
+
__decorate([
|
|
191
|
+
property({ reflect: true })
|
|
192
|
+
], HelixHeadlessSteps.prototype, "completion", void 0);
|
|
151
193
|
__decorate([
|
|
152
194
|
words,
|
|
153
195
|
property()
|
|
@@ -53,6 +53,7 @@ import '../../button/index.js';
|
|
|
53
53
|
* @csspart state - The words that describe a finished stage's button. Hidden.
|
|
54
54
|
*
|
|
55
55
|
* @cssstate invalid - The sequence's `validate` last answered that this stage is not valid.
|
|
56
|
+
* @cssstate locked - A `linear` sequence holds this stage out of reach until the stages before it are finished.
|
|
56
57
|
*/
|
|
57
58
|
export declare class HelixHeadlessStep extends HelixElement {
|
|
58
59
|
/**
|
|
@@ -106,6 +107,11 @@ export declare class HelixHeadlessStep extends HelixElement {
|
|
|
106
107
|
value: string;
|
|
107
108
|
/** Says a stage was finished, from the first time one is. */
|
|
108
109
|
private readonly status;
|
|
110
|
+
/**
|
|
111
|
+
* Puts focus on the step's name, the control a reader presses, as a
|
|
112
|
+
* `focus()` on a native control would.
|
|
113
|
+
*/
|
|
114
|
+
focus(options?: FocusOptions): void;
|
|
109
115
|
/**
|
|
110
116
|
* Fires `helix-select` for the step, unless the step is `disabled` or a
|
|
111
117
|
* linear sequence has it out of reach.
|