@fusion.dev/helix-headless 0.50.0 → 0.52.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 +111 -16
- package/dist/carousel/root/carousel-root.d.ts +150 -7
- package/dist/carousel/root/carousel-root.js +82 -9
- package/dist/splitter/root/splitter-root.d.ts +56 -19
- package/dist/splitter/root/splitter-root.js +5 -6
- package/package.json +5 -5
- package/types/jsx.d.ts +175 -21
- package/types/vue.d.ts +52 -4
|
@@ -9,6 +9,15 @@ import { caretLeft, caretRight, play, stop } from '@fusion.dev/helix-icons';
|
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property, state } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
+
const COUNT_OR_AUTO = {
|
|
13
|
+
fromAttribute: (value) => {
|
|
14
|
+
if (value === null) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return value === 'auto' ? 'auto' : Number(value);
|
|
18
|
+
},
|
|
19
|
+
toAttribute: (value) => value === undefined ? null : String(value),
|
|
20
|
+
};
|
|
12
21
|
let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
13
22
|
constructor() {
|
|
14
23
|
super(...arguments);
|
|
@@ -20,6 +29,16 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
20
29
|
this.label = 'Carousel';
|
|
21
30
|
this.loop = false;
|
|
22
31
|
this.rotating = false;
|
|
32
|
+
this.readFit = () => {
|
|
33
|
+
const fitted = this.measure();
|
|
34
|
+
if (fitted === undefined || fitted === this.fitted) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.fitted = fitted;
|
|
38
|
+
if (this.index > this.positions - 1) {
|
|
39
|
+
this.goTo(this.positions - 1);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
23
42
|
this.handleIndicatorKeydown = (event) => {
|
|
24
43
|
const step = inlineStep(this, event.key);
|
|
25
44
|
if (step === 0) {
|
|
@@ -95,12 +114,55 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
95
114
|
this.indexState = next;
|
|
96
115
|
this.requestUpdate('index', previous);
|
|
97
116
|
}
|
|
98
|
-
get
|
|
117
|
+
get clamped() {
|
|
99
118
|
const asked = this.perView ?? 1;
|
|
100
|
-
return Number.isFinite(asked) ? Math.max(1, Math.floor(asked)) : 1;
|
|
119
|
+
return typeof asked === 'number' && Number.isFinite(asked) ? Math.max(1, Math.floor(asked)) : 1;
|
|
120
|
+
}
|
|
121
|
+
get fits() {
|
|
122
|
+
return this.perView === 'auto' && this.orientation !== 'vertical';
|
|
123
|
+
}
|
|
124
|
+
get across() {
|
|
125
|
+
return this.fits ? this.fitted : this.clamped;
|
|
101
126
|
}
|
|
102
127
|
get positions() {
|
|
103
|
-
return Math.max(1, this.slides.length - this.across + 1);
|
|
128
|
+
return Math.max(1, this.slides.length - (this.across ?? 1) + 1);
|
|
129
|
+
}
|
|
130
|
+
measure() {
|
|
131
|
+
const track = this.partNamed('track');
|
|
132
|
+
const pane = this.panes[0]?.getBoundingClientRect().width;
|
|
133
|
+
if (!track || pane === undefined) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const asked = Number.parseFloat(getComputedStyle(track).columnGap);
|
|
137
|
+
const gap = Number.isNaN(asked) ? 0 : asked;
|
|
138
|
+
const room = track.clientWidth - 2 * this.bleed(track);
|
|
139
|
+
if (pane <= 0 || room <= 0) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return Math.max(1, Math.round((room + gap) / (pane + gap)));
|
|
143
|
+
}
|
|
144
|
+
handed() {
|
|
145
|
+
return this.fits ? nothing : `--carousel-per-view: ${this.clamped}`;
|
|
146
|
+
}
|
|
147
|
+
watchPane() {
|
|
148
|
+
if (!this.fits || typeof ResizeObserver === 'undefined') {
|
|
149
|
+
this.sizes?.disconnect();
|
|
150
|
+
this.sizes = undefined;
|
|
151
|
+
this.watched = undefined;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.sizes ??= new ResizeObserver(this.readFit);
|
|
155
|
+
const pane = this.panes[0];
|
|
156
|
+
if (pane === this.watched) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (this.watched) {
|
|
160
|
+
this.sizes.unobserve(this.watched);
|
|
161
|
+
}
|
|
162
|
+
this.watched = pane;
|
|
163
|
+
if (pane) {
|
|
164
|
+
this.sizes.observe(pane);
|
|
165
|
+
}
|
|
104
166
|
}
|
|
105
167
|
get panes() {
|
|
106
168
|
return [...(this.renderRoot?.querySelectorAll?.('[part="slide"]') ?? [])];
|
|
@@ -196,6 +258,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
196
258
|
this.addEventListener('pointerenter', this.pause);
|
|
197
259
|
this.watcher = new MutationObserver(() => this.requestUpdate());
|
|
198
260
|
this.watcher.observe(this, { childList: true });
|
|
261
|
+
this.watchPane();
|
|
199
262
|
}
|
|
200
263
|
disconnectedCallback() {
|
|
201
264
|
super.disconnectedCallback();
|
|
@@ -207,6 +270,9 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
207
270
|
this.removeEventListener('pointerenter', this.pause);
|
|
208
271
|
this.watcher?.disconnect();
|
|
209
272
|
this.watcher = undefined;
|
|
273
|
+
this.sizes?.disconnect();
|
|
274
|
+
this.sizes = undefined;
|
|
275
|
+
this.watched = undefined;
|
|
210
276
|
clearInterval(this.timer);
|
|
211
277
|
this.timer = undefined;
|
|
212
278
|
}
|
|
@@ -218,25 +284,29 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
218
284
|
if (changed.has('index')) {
|
|
219
285
|
this.showing = undefined;
|
|
220
286
|
}
|
|
287
|
+
if (changed.has('perView')) {
|
|
288
|
+
this.fitted = undefined;
|
|
289
|
+
}
|
|
221
290
|
hostAria(this.internals, { label: this.label, role: 'group' });
|
|
222
291
|
this.internals.ariaRoleDescription = 'carousel';
|
|
223
292
|
}
|
|
224
293
|
firstUpdated(changed) {
|
|
225
294
|
super.firstUpdated(changed);
|
|
295
|
+
this.watchPane();
|
|
226
296
|
this.finishHydration();
|
|
227
297
|
}
|
|
228
298
|
updated(changed) {
|
|
229
299
|
super.updated(changed);
|
|
230
|
-
const count = this.slides.length;
|
|
231
300
|
const drawn = this.renderRoot.querySelectorAll('[part="slide"]').length;
|
|
232
301
|
if (changed.has('index') || (this.scrolledFor === 0 && drawn > 0)) {
|
|
233
302
|
this.scrollToIndex();
|
|
234
303
|
}
|
|
235
304
|
this.scrolledFor = drawn;
|
|
236
305
|
this.runTimer();
|
|
237
|
-
if (
|
|
238
|
-
this.goTo(
|
|
306
|
+
if (this.slides.length > 0 && this.index > this.positions - 1) {
|
|
307
|
+
this.goTo(this.positions - 1);
|
|
239
308
|
}
|
|
309
|
+
this.watchPane();
|
|
240
310
|
this.assign();
|
|
241
311
|
}
|
|
242
312
|
assign() {
|
|
@@ -308,7 +378,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
308
378
|
<div
|
|
309
379
|
data-scroller
|
|
310
380
|
part="track"
|
|
311
|
-
style=${
|
|
381
|
+
style=${this.handed()}
|
|
312
382
|
@scroll=${this.handleTrackScroll}
|
|
313
383
|
@scrollend=${this.handleScroll}
|
|
314
384
|
@pointerdown=${this.handleTake}
|
|
@@ -328,7 +398,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
|
|
|
328
398
|
></slot>
|
|
329
399
|
</div>`)}
|
|
330
400
|
</div>
|
|
331
|
-
${this.slides.length > 1
|
|
401
|
+
${this.slides.length > 1 && this.across !== undefined
|
|
332
402
|
? html `<div part="indicators" role="group" aria-label=${`${this.label} slides`}>
|
|
333
403
|
${''}
|
|
334
404
|
${Array.from({ length: this.positions }).map((_place, at) => {
|
|
@@ -376,8 +446,11 @@ __decorate([
|
|
|
376
446
|
state()
|
|
377
447
|
], HelixHeadlessCarousel.prototype, "rotating", void 0);
|
|
378
448
|
__decorate([
|
|
379
|
-
property({ attribute: 'per-view',
|
|
449
|
+
property({ attribute: 'per-view', converter: COUNT_OR_AUTO, reflect: true })
|
|
380
450
|
], HelixHeadlessCarousel.prototype, "perView", void 0);
|
|
451
|
+
__decorate([
|
|
452
|
+
state()
|
|
453
|
+
], HelixHeadlessCarousel.prototype, "fitted", void 0);
|
|
381
454
|
__decorate([
|
|
382
455
|
state()
|
|
383
456
|
], HelixHeadlessCarousel.prototype, "showing", void 0);
|
|
@@ -27,13 +27,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
27
27
|
* **A pane can be shut, and that is not the same as a divider at the edge.**
|
|
28
28
|
* The geometry is identical and the knowledge is not: only a shut pane knows
|
|
29
29
|
* where to open back to. So `collapsed` is a second state rather than a reading
|
|
30
|
-
* of the position, and the position it opens to is the
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* of the position, and the position it opens to is where the gesture that shut
|
|
31
|
+
* it began, whichever gesture that was: a drag takes its reading on the press,
|
|
32
|
+
* and every other door takes it in `settle`, nothing having been applied there
|
|
33
|
+
* yet. `collapsed-size` keeps a sliver showing where a pane has a tab strip
|
|
34
|
+
* worth leaving, and collapse overrides `min` and `max`, which is what those
|
|
35
|
+
* are for.
|
|
34
36
|
*
|
|
35
37
|
* @since 0.1.0
|
|
36
|
-
* @status
|
|
38
|
+
* @status stable
|
|
37
39
|
* @category layout
|
|
38
40
|
* @aliases SplitPane, Resizable, SplitView
|
|
39
41
|
* @documentation https://fusion.dev/helix/components/splitter
|
|
@@ -41,8 +43,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
41
43
|
* @slot end - The pane after it.
|
|
42
44
|
* @slot start - The pane before the divider.
|
|
43
45
|
*
|
|
44
|
-
* @event {HelixBeforeCollapseEvent} helix-before-collapse - Fired before a pane shuts or reopens,
|
|
45
|
-
* @event {HelixBeforeResizeEvent} helix-before-resize - Fired before the divider settles, and only where the gesture moved it: a press that moved nothing, a drag that came back, an arrow with nowhere left to go and a corner that moved the other divider are all silent on this one. Cancelable: refusing it takes the
|
|
46
|
+
* @event {HelixBeforeCollapseEvent} helix-before-collapse - Fired before a pane shuts or reopens, whether or not the resize that reached it was allowed, and carrying its own position: a shut pane sits at `collapsed-size` rather than at the number the resize named. Cancelable: refusing it leaves the pane as it was and the divider where the gesture put it.
|
|
47
|
+
* @event {HelixBeforeResizeEvent} helix-before-resize - Fired before the divider settles, and only where the gesture moved it: a press that moved nothing, a drag that came back, an arrow with nowhere left to go and a corner that moved the other divider are all silent on this one. Cancelable: refusing it takes the position and leaves the rest of the gesture, so a collapse the gesture reached is still announced and still yours to refuse. A drag has already drawn itself by then and holds where it was dropped until the consumer assigns their own.
|
|
46
48
|
*
|
|
47
49
|
* @csspart corner - The handle where two dividers cross, added on the client.
|
|
48
50
|
* @csspart end - The box holding the second.
|
|
@@ -181,6 +183,14 @@ export declare class HelixHeadlessSplitter extends HelixElement {
|
|
|
181
183
|
* the edge the number it started from is gone. Taken once, on the press, and
|
|
182
184
|
* written back if the gesture ends shut, so the pane opens to where it was
|
|
183
185
|
* rather than to the edge it was dragged against.
|
|
186
|
+
*
|
|
187
|
+
* **A pointer gesture only, because it is the only one that loses it**, and
|
|
188
|
+
* `settle` answers for the rest from `began`: a key and a double click apply
|
|
189
|
+
* nothing before they settle, so where the divider is at the top of that
|
|
190
|
+
* method is where it was. Read once and cleared there, exactly as `began` is,
|
|
191
|
+
* and for the second reason as well as the first. Left standing it measured
|
|
192
|
+
* the next gesture against the last one's press, so a collapse pressed from
|
|
193
|
+
* 20 reopened at the 60 a drag had started from an hour earlier.
|
|
184
194
|
*/
|
|
185
195
|
private opened;
|
|
186
196
|
/**
|
|
@@ -331,21 +341,48 @@ export declare class HelixHeadlessSplitter extends HelixElement {
|
|
|
331
341
|
* Through the audited path, so a consumer who owns `position` is told what was
|
|
332
342
|
* asked for and decides, and one who does not has already had it applied.
|
|
333
343
|
*
|
|
334
|
-
* **
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
+
* **A refused resize ends the resize and not the gesture.** `shutAt` reads the
|
|
345
|
+
* position the gesture ended on, so the collapse is computed from a number the
|
|
346
|
+
* consumer may have declined, and the reading this replaces was that the two
|
|
347
|
+
* are therefore one decision: refuse the first and the second cannot stand.
|
|
348
|
+
* That is a reasonable thing to believe about a splitter nobody else owns, and
|
|
349
|
+
* it takes "no" to mean the gesture is over. What it misses is that a consumer
|
|
350
|
+
* who owns the position refuses in order to *take* the decision, not to end
|
|
351
|
+
* it. The sandbox two folders away refuses every `helix-before-resize` from
|
|
352
|
+
* the splitter inside it, re-announces the same move in its own vocabulary and
|
|
353
|
+
* writes the answer back through `position`, which is what makes it the single
|
|
354
|
+
* decision point; returning on its refusal left `panel-collapsed-size` inert
|
|
355
|
+
* there, the shipped `with-a-panel` example measuring a 0px panel against the
|
|
356
|
+
* 36 it asks for. So the collapse is reached either way and is refused, or
|
|
357
|
+
* not, on its own.
|
|
358
|
+
*
|
|
359
|
+
* **Which is honest because the collapse carries its own position.** A shut
|
|
360
|
+
* pane sits at `collapsed-size`, which is where `position` reads it from once
|
|
361
|
+
* `collapsed` is set, and never at the number the resize named: refuse a drag
|
|
362
|
+
* to 100 on a splitter with `collapsed-size="10"`, allow the collapse, and the
|
|
363
|
+
* divider is at 90. Nothing writes the refused number either, `requestChange`
|
|
364
|
+
* applying nothing when it is refused and the collapse below writing the open
|
|
365
|
+
* position back over whatever a drag had already drawn, so a consumer who says
|
|
366
|
+
* no to the position and yes to the collapse gets the pane shut and keeps the
|
|
367
|
+
* number they had to reopen to. The two coincide only where `collapsed-size`
|
|
368
|
+
* is zero, and the states differ even there: a shut pane knows where to open
|
|
369
|
+
* back to and a divider parked at the edge does not.
|
|
344
370
|
*
|
|
345
371
|
* Refusing the collapse alone still leaves the resize, which is the other way
|
|
346
372
|
* round and is coherent: the divider goes where the drag put it and the pane
|
|
347
|
-
* is not declared shut.
|
|
348
|
-
*
|
|
373
|
+
* is not declared shut.
|
|
374
|
+
*
|
|
375
|
+
* **The open position is taken here for every door but the pointer's, which
|
|
376
|
+
* is the same split `began` makes and for the same reason.** A drag has
|
|
377
|
+
* overwritten the position on the way to the edge, so it reads it on the
|
|
378
|
+
* press; a key, a double click and `moveTo` apply nothing until the request
|
|
379
|
+
* below, so where the divider is at the top of this method is where it was.
|
|
380
|
+
* Captured on the press alone, the class docstring's promise that a shut pane
|
|
381
|
+
* knows where to open back to held for a drag and for nothing else: measured
|
|
382
|
+
* 17 Sep 2026, `Home` on a splitter resting at 60 shut the start pane and
|
|
383
|
+
* reopened it at 0, the resize above having written the edge and the collapse
|
|
384
|
+
* having nothing to put back. Stale, it was worse than absent, `opened` never
|
|
385
|
+
* being cleared: a press at 60, then a key collapse from 20, reopened at 60.
|
|
349
386
|
*
|
|
350
387
|
* **A gesture that moved nothing announces nothing, and this is the one place
|
|
351
388
|
* that can say so.** A change carrying the position the divider already had
|
|
@@ -302,10 +302,12 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
|
|
|
302
302
|
const shut = this.shutAt(wanted);
|
|
303
303
|
const began = this.began ?? this.position;
|
|
304
304
|
this.began = undefined;
|
|
305
|
+
const opened = this.opened ?? (this.collapsed ? undefined : began);
|
|
306
|
+
this.opened = undefined;
|
|
305
307
|
if (wanted === began && shut === this.collapsed) {
|
|
306
308
|
return;
|
|
307
309
|
}
|
|
308
|
-
|
|
310
|
+
requestChange({
|
|
309
311
|
apply: (value) => {
|
|
310
312
|
const previous = this.position;
|
|
311
313
|
this.positionState = value;
|
|
@@ -315,15 +317,12 @@ let HelixHeadlessSplitter = HelixHeadlessSplitter_1 = class HelixHeadlessSplitte
|
|
|
315
317
|
host: this,
|
|
316
318
|
next: wanted,
|
|
317
319
|
});
|
|
318
|
-
if (!moved) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
320
|
if (shut !== this.collapsed) {
|
|
322
321
|
requestChange({
|
|
323
322
|
apply: (value) => {
|
|
324
323
|
const previous = this.collapsed;
|
|
325
|
-
if (value !== '' &&
|
|
326
|
-
this.positionState =
|
|
324
|
+
if (value !== '' && opened !== undefined) {
|
|
325
|
+
this.positionState = opened;
|
|
327
326
|
}
|
|
328
327
|
this.collapsedState = value === '' ? 'none' : value;
|
|
329
328
|
this.requestUpdate('collapsed', previous);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-headless",
|
|
3
3
|
"description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.52.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
},
|
|
47
47
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@fusion.dev/helix-core": "0.
|
|
50
|
-
"@fusion.dev/helix-icons": "0.
|
|
49
|
+
"@fusion.dev/helix-core": "0.52.0",
|
|
50
|
+
"@fusion.dev/helix-icons": "0.52.0",
|
|
51
51
|
"uqr": "^0.1.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
55
|
-
"@fusion.dev/helix-cem": "0.
|
|
56
|
-
"@fusion.dev/helix-testing": "0.
|
|
55
|
+
"@fusion.dev/helix-cem": "0.52.0",
|
|
56
|
+
"@fusion.dev/helix-testing": "0.52.0",
|
|
57
57
|
"@fusion.dev/scripts": "0.0.0-dev",
|
|
58
58
|
"@happy-dom/global-registrator": "^20.0.0",
|
|
59
59
|
"@lit-labs/ssr": "^4.1.0",
|