@fusion.dev/helix-headless 0.100.0 → 0.101.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 +1565 -1
- package/dist/dialog/root/dialog-root.d.ts +1 -1
- package/dist/element-events.generated.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lightbox/index.d.ts +2 -0
- package/dist/lightbox/index.js +2 -0
- package/dist/lightbox/item/lightbox-item.d.ts +60 -0
- package/dist/lightbox/item/lightbox-item.js +45 -0
- package/dist/lightbox/root/lightbox-root.d.ts +176 -0
- package/dist/lightbox/root/lightbox-root.js +212 -0
- package/package.json +5 -5
- package/types/jsx.d.ts +673 -0
- package/types/svelte.d.ts +2 -0
- package/types/vue.d.ts +129 -2
|
@@ -20,7 +20,7 @@ declare const HelixHeadlessDialog_base: (new (...args: any[]) => import("@fusion
|
|
|
20
20
|
* @since 0.1.0
|
|
21
21
|
* @status stable
|
|
22
22
|
* @category overlays
|
|
23
|
-
* @aliases Modal,
|
|
23
|
+
* @aliases Modal, Sheet
|
|
24
24
|
* @documentation https://fusion.dev/helix/components/dialog
|
|
25
25
|
*
|
|
26
26
|
* @slot - The dialog's content.
|
|
@@ -76,6 +76,14 @@ registerElementEvents({
|
|
|
76
76
|
'helix-headless-image': ['error', 'load'],
|
|
77
77
|
'helix-headless-input': ['change', 'helix-before-change', 'input'],
|
|
78
78
|
'helix-headless-kbd': ['helix-shortcut'],
|
|
79
|
+
'helix-headless-lightbox': [
|
|
80
|
+
'close',
|
|
81
|
+
'helix-activate',
|
|
82
|
+
'helix-before-activate',
|
|
83
|
+
'helix-before-close',
|
|
84
|
+
'helix-before-open',
|
|
85
|
+
'toggle',
|
|
86
|
+
],
|
|
79
87
|
'helix-headless-listbox': ['change', 'helix-before-change', 'helix-filter', 'input'],
|
|
80
88
|
'helix-headless-megamenu': ['helix-before-close', 'helix-before-open', 'toggle'],
|
|
81
89
|
'helix-headless-menu': ['helix-before-close', 'helix-before-open', 'helix-select', 'toggle'],
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export * from './image/index.js';
|
|
|
53
53
|
export * from './input/index.js';
|
|
54
54
|
export * from './kbd/index.js';
|
|
55
55
|
export * from './legend/index.js';
|
|
56
|
+
export * from './lightbox/index.js';
|
|
56
57
|
export * from './line-chart/index.js';
|
|
57
58
|
export * from './list/index.js';
|
|
58
59
|
export * from './listbox/index.js';
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ export * from './image/index.js';
|
|
|
44
44
|
export * from './input/index.js';
|
|
45
45
|
export * from './kbd/index.js';
|
|
46
46
|
export * from './legend/index.js';
|
|
47
|
+
export * from './lightbox/index.js';
|
|
47
48
|
export * from './line-chart/index.js';
|
|
48
49
|
export * from './list/index.js';
|
|
49
50
|
export * from './listbox/index.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { HelixElement } from '@fusion.dev/helix-core';
|
|
2
|
+
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
+
/**
|
|
4
|
+
* @summary One thing a lightbox can show.
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* A wrapper around whatever the consumer opened, which is the whole of what it
|
|
8
|
+
* is: a picture, a frame of video, a chart. The family shows one at a time and
|
|
9
|
+
* this is how it says which, so the content stays the consumer's and the
|
|
10
|
+
* arithmetic stays the root's.
|
|
11
|
+
*
|
|
12
|
+
* Hidden rather than unmounted while another item is current, for the reason
|
|
13
|
+
* `TabPanel` gives: what is in the markup is what a crawler reads and what a
|
|
14
|
+
* server sent, and an `<img>` that has already decoded does not decode again
|
|
15
|
+
* when the reader walks back to it.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.100.0
|
|
18
|
+
* @status stable
|
|
19
|
+
* @category media
|
|
20
|
+
* @aliases LightboxSlide, GalleryItem
|
|
21
|
+
* @documentation https://fusion.dev/helix/components/lightbox
|
|
22
|
+
*
|
|
23
|
+
* @slot - What is being shown.
|
|
24
|
+
* @slot description - A line under it saying what it is.
|
|
25
|
+
*
|
|
26
|
+
* @csspart item - The box the content is shown in.
|
|
27
|
+
*
|
|
28
|
+
* @cssstate current - While this is the item the lightbox is showing.
|
|
29
|
+
*/
|
|
30
|
+
export declare class HelixHeadlessLightboxItem extends HelixElement {
|
|
31
|
+
/**
|
|
32
|
+
* Whether this is the one being shown.
|
|
33
|
+
*
|
|
34
|
+
* Written by the root, which is the only thing that sees the set.
|
|
35
|
+
*/
|
|
36
|
+
current: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* What this item is called, for the reading that says where the reader is.
|
|
39
|
+
*/
|
|
40
|
+
label: string;
|
|
41
|
+
/**
|
|
42
|
+
* What names this item, and what `active` on the root is compared against.
|
|
43
|
+
*/
|
|
44
|
+
value: string;
|
|
45
|
+
/**
|
|
46
|
+
* Mirrors the shown state where a stylesheet can select on it.
|
|
47
|
+
*
|
|
48
|
+
* A custom state and never a reflected attribute, for the reason `TabPanel`
|
|
49
|
+
* writes out at length: the parent writes this, so reflecting it would put
|
|
50
|
+
* the element's own bookkeeping into markup a consumer never sent, which is a
|
|
51
|
+
* hydration mismatch in React and a choice nobody made in a serialized tree.
|
|
52
|
+
*/
|
|
53
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
54
|
+
protected render(): TemplateResult;
|
|
55
|
+
}
|
|
56
|
+
declare global {
|
|
57
|
+
interface HTMLElementTagNameMap {
|
|
58
|
+
'helix-headless-lightbox-item': HelixHeadlessLightboxItem;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { customElement, HelixElement } from '@fusion.dev/helix-core';
|
|
8
|
+
import { html, nothing } from 'lit';
|
|
9
|
+
import { property } from 'lit/decorators.js';
|
|
10
|
+
let HelixHeadlessLightboxItem = class HelixHeadlessLightboxItem extends HelixElement {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.current = false;
|
|
14
|
+
this.label = '';
|
|
15
|
+
this.value = '';
|
|
16
|
+
}
|
|
17
|
+
willUpdate(changed) {
|
|
18
|
+
super.willUpdate(changed);
|
|
19
|
+
this.toggleState('current', this.current);
|
|
20
|
+
}
|
|
21
|
+
render() {
|
|
22
|
+
return html `<div
|
|
23
|
+
part="item"
|
|
24
|
+
role="group"
|
|
25
|
+
aria-label=${this.label || nothing}
|
|
26
|
+
?hidden=${!this.current}
|
|
27
|
+
>
|
|
28
|
+
<slot></slot>
|
|
29
|
+
<slot name="description"></slot>
|
|
30
|
+
</div>`;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
__decorate([
|
|
34
|
+
property({ type: Boolean })
|
|
35
|
+
], HelixHeadlessLightboxItem.prototype, "current", void 0);
|
|
36
|
+
__decorate([
|
|
37
|
+
property()
|
|
38
|
+
], HelixHeadlessLightboxItem.prototype, "label", void 0);
|
|
39
|
+
__decorate([
|
|
40
|
+
property()
|
|
41
|
+
], HelixHeadlessLightboxItem.prototype, "value", void 0);
|
|
42
|
+
HelixHeadlessLightboxItem = __decorate([
|
|
43
|
+
customElement('helix-headless-lightbox-item')
|
|
44
|
+
], HelixHeadlessLightboxItem);
|
|
45
|
+
export { HelixHeadlessLightboxItem };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { CommandController, HelixElement } from '@fusion.dev/helix-core';
|
|
2
|
+
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
+
declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
|
|
4
|
+
/**
|
|
5
|
+
* @summary A viewer that opens one thing over the page.
|
|
6
|
+
*
|
|
7
|
+
* @description
|
|
8
|
+
* **A lightbox is chrome around a picture rather than a picture.** It draws
|
|
9
|
+
* nothing of its own: what it shows is whatever the consumer opened, so this
|
|
10
|
+
* earns its place on the frame, the keyboard and the reading that says where in
|
|
11
|
+
* the set the reader is.
|
|
12
|
+
*
|
|
13
|
+
* **The modal half is the platform's.** A real `<dialog>` opened with
|
|
14
|
+
* `showModal()` brings the top layer, a backdrop, Escape and `inert` behind it,
|
|
15
|
+
* and this repository has already removed one hand-written focus trap for being
|
|
16
|
+
* worse than that: `inert` on the rest of the document is something no
|
|
17
|
+
* JavaScript trap can achieve. Every way it can close comes back through
|
|
18
|
+
* `requestOpen`, which is what `Dialog` and `Drawer` both do and for the reason
|
|
19
|
+
* they both give.
|
|
20
|
+
*
|
|
21
|
+
* **The set is this component's own and not `Carousel`'s.** A carousel is a
|
|
22
|
+
* track: a scroller with snap points, per-view arithmetic and stops it can rest
|
|
23
|
+
* between. A lightbox shows one thing at a time and has no track, so what it
|
|
24
|
+
* needs is which item is current, which is `active` and the same pair `Tabs`
|
|
25
|
+
* uses. The arrow keys move the picture under a reader whose focus stays in the
|
|
26
|
+
* dialog, which is also why `KeyboardNavigationController` is not the right
|
|
27
|
+
* tool: that one moves focus between focusable items, and these are pictures.
|
|
28
|
+
*
|
|
29
|
+
* **Chrome hides while the reader is still**, on `IdleController`, and comes
|
|
30
|
+
* back on a pointer move or a focus. `fullscreen` is `FullscreenController`, the
|
|
31
|
+
* same pair `Video` holds and the only two it shares.
|
|
32
|
+
*
|
|
33
|
+
* @since 0.100.0
|
|
34
|
+
* @status stable
|
|
35
|
+
* @category media
|
|
36
|
+
* @aliases Photo viewer, Image viewer, Gallery overlay
|
|
37
|
+
* @documentation https://fusion.dev/helix/components/lightbox
|
|
38
|
+
*
|
|
39
|
+
* @slot - The items, which are `<helix-headless-lightbox-item>` elements.
|
|
40
|
+
* @slot footer - Chrome below the item, such as a counter or a thumbnail strip.
|
|
41
|
+
* @slot header - Chrome above the item, such as a title and a close button.
|
|
42
|
+
*
|
|
43
|
+
* @event {Event} close - Relayed, because the platform's own does not escape.
|
|
44
|
+
* @event {HelixActivateEvent} helix-activate - Fired once the shown item has moved, so the element already holds it.
|
|
45
|
+
* @event {HelixBeforeActivateEvent} helix-before-activate - Fired before the shown item changes, and cancelable. The only way to refuse one.
|
|
46
|
+
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes, and cancelable.
|
|
47
|
+
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before it opens, and cancelable.
|
|
48
|
+
* @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
|
|
49
|
+
*
|
|
50
|
+
* @csspart dialog - The native dialog element.
|
|
51
|
+
* @csspart live - The region that says which item is shown.
|
|
52
|
+
* @csspart viewport - The box the current item is shown in.
|
|
53
|
+
*
|
|
54
|
+
* @cssstate fullscreen - While the viewer fills the screen.
|
|
55
|
+
* @cssstate idle - While the reader has been still long enough to hide the chrome.
|
|
56
|
+
* @cssstate open - While the viewer is open.
|
|
57
|
+
*/
|
|
58
|
+
export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
|
|
59
|
+
/**
|
|
60
|
+
* How long the reader may be still before the chrome hides, in milliseconds.
|
|
61
|
+
*
|
|
62
|
+
* Zero keeps it up, which is what a viewer with a caption worth reading
|
|
63
|
+
* wants. The wait is only ever about idling: whether anything else should
|
|
64
|
+
* keep the chrome up is the consumer's to decide with this.
|
|
65
|
+
*/
|
|
66
|
+
idleDelay: number;
|
|
67
|
+
/**
|
|
68
|
+
* Accessible name for the viewer.
|
|
69
|
+
*/
|
|
70
|
+
label: string;
|
|
71
|
+
/**
|
|
72
|
+
* Whether walking past the end comes back to the beginning.
|
|
73
|
+
*/
|
|
74
|
+
loop: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The reading that says where in the set the reader is, with `{index}` and
|
|
77
|
+
* `{count}` filled in.
|
|
78
|
+
*
|
|
79
|
+
* Announced rather than only drawn, because the picture changing under a
|
|
80
|
+
* reader who cannot see it is otherwise silent. A consumer who has their own
|
|
81
|
+
* counter in the chrome can empty this and say it themselves.
|
|
82
|
+
*/
|
|
83
|
+
statusLabel: string;
|
|
84
|
+
/** Whether the reader has been still long enough to hide the chrome. */
|
|
85
|
+
private resting;
|
|
86
|
+
/** Whether the viewer is filling the screen. */
|
|
87
|
+
private filled;
|
|
88
|
+
/** What the live region is currently saying. */
|
|
89
|
+
private announcement;
|
|
90
|
+
private readonly fullscreen;
|
|
91
|
+
private readonly idle;
|
|
92
|
+
protected readonly commands: CommandController;
|
|
93
|
+
/** The items, read from the host so the server has them too. */
|
|
94
|
+
private get items();
|
|
95
|
+
/** The native dialog, or nothing where there is no DOM to hold one. */
|
|
96
|
+
private get dialog();
|
|
97
|
+
/** Opens the viewer, which makes the rest of the page inert. */
|
|
98
|
+
show(): void;
|
|
99
|
+
/** Asks to close, which a consumer may refuse. */
|
|
100
|
+
close(): void;
|
|
101
|
+
/** Shows the next item, or the first where it is past the end and looping. */
|
|
102
|
+
next(): void;
|
|
103
|
+
/** Shows the previous item, or the last where it is past the start and looping. */
|
|
104
|
+
previous(): void;
|
|
105
|
+
/** Fills the screen with the viewer, or comes back. */
|
|
106
|
+
toggleFullscreen(): void;
|
|
107
|
+
/**
|
|
108
|
+
* Moves the shown item, through `requestActive` so a consumer can refuse.
|
|
109
|
+
*
|
|
110
|
+
* Clamped rather than wrapped by default: a viewer that silently returns to
|
|
111
|
+
* the first picture tells a reader they have seen everything only if they
|
|
112
|
+
* were counting, and `loop` is how a consumer asks for the other reading.
|
|
113
|
+
*/
|
|
114
|
+
private step;
|
|
115
|
+
/**
|
|
116
|
+
* Escape, intercepted so it is a request rather than an act.
|
|
117
|
+
*
|
|
118
|
+
* The native `cancel` fires before the dialog closes, so preventing it leaves
|
|
119
|
+
* the viewer open and lets `requestOpen` decide. Without this a reader
|
|
120
|
+
* pressing Escape would close a viewer its consumer had said was open, and the
|
|
121
|
+
* two would disagree until something else moved.
|
|
122
|
+
*/
|
|
123
|
+
private readonly handleCancel;
|
|
124
|
+
/**
|
|
125
|
+
* A close that happened anyway, reflected back and relayed.
|
|
126
|
+
*
|
|
127
|
+
* `close` on a `<dialog>` neither bubbles nor is composed, so the one the
|
|
128
|
+
* browser fires reaches nobody outside this shadow root.
|
|
129
|
+
*/
|
|
130
|
+
private readonly handleClose;
|
|
131
|
+
/**
|
|
132
|
+
* The arrow keys, which move the picture rather than the focus.
|
|
133
|
+
*
|
|
134
|
+
* `inlineStep` rather than a comparison against `ArrowLeft`, so the reader on
|
|
135
|
+
* a right-to-left page gets the next picture from the key that points the way
|
|
136
|
+
* their set runs. Up and down are bound as well, the set having one axis and
|
|
137
|
+
* a reader having no way to know which one this component thinks it is.
|
|
138
|
+
*/
|
|
139
|
+
private readonly handleKeydown;
|
|
140
|
+
/** Home and End, which are an index rather than a step. */
|
|
141
|
+
private toEnd;
|
|
142
|
+
/**
|
|
143
|
+
* A press on the backdrop, which lands on the dialog element itself.
|
|
144
|
+
*
|
|
145
|
+
* The point is compared against the panel's box rather than trusting the
|
|
146
|
+
* target alone, for the reason `Dialog` gives: a modal `<dialog>` attributes
|
|
147
|
+
* backdrop presses to itself, so any padding the styled tier gives the panel
|
|
148
|
+
* is a strip where a press on the panel looks like a press outside it.
|
|
149
|
+
*/
|
|
150
|
+
private readonly handleBackdrop;
|
|
151
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
152
|
+
protected updated(changed: PropertyValues): void;
|
|
153
|
+
/**
|
|
154
|
+
* Tells each item whether it is the one being shown.
|
|
155
|
+
*
|
|
156
|
+
* `resolveActiveItem` rather than a comparison, so a set whose `active` names
|
|
157
|
+
* nothing still shows something: every set before its consumer has chosen, and
|
|
158
|
+
* every set whose current item has just been removed.
|
|
159
|
+
*/
|
|
160
|
+
private mark;
|
|
161
|
+
/**
|
|
162
|
+
* Says where the reader is, once the reader has moved.
|
|
163
|
+
*
|
|
164
|
+
* Empty until then, because a live region populated as it enters the document
|
|
165
|
+
* announces itself on arrival, which for a component that upgrades on load is
|
|
166
|
+
* every page load.
|
|
167
|
+
*/
|
|
168
|
+
private say;
|
|
169
|
+
protected render(): TemplateResult;
|
|
170
|
+
}
|
|
171
|
+
declare global {
|
|
172
|
+
interface HTMLElementTagNameMap {
|
|
173
|
+
'helix-headless-lightbox': HelixHeadlessLightbox;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { CommandController, customElement, FullscreenController, HelixElement, hostAria, IdleController, inlineStep, MixinControllerActive, MixinControllerOpen, resolveActiveItem, } from '@fusion.dev/helix-core';
|
|
8
|
+
import { html, nothing } from 'lit';
|
|
9
|
+
import { property, state } from 'lit/decorators.js';
|
|
10
|
+
import { HelixHeadlessLightboxItem } from '../item/lightbox-item.js';
|
|
11
|
+
const fill = (template, index, count) => template.replaceAll('{index}', String(index)).replaceAll('{count}', String(count));
|
|
12
|
+
let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerActive(MixinControllerOpen(HelixElement)) {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.idleDelay = 0;
|
|
16
|
+
this.label = '';
|
|
17
|
+
this.loop = false;
|
|
18
|
+
this.statusLabel = '{index} of {count}';
|
|
19
|
+
this.resting = false;
|
|
20
|
+
this.filled = false;
|
|
21
|
+
this.announcement = '';
|
|
22
|
+
this.fullscreen = new FullscreenController(this, (full) => {
|
|
23
|
+
this.filled = full;
|
|
24
|
+
});
|
|
25
|
+
this.idle = new IdleController(this, () => this.idleDelay, (resting) => {
|
|
26
|
+
this.resting = resting;
|
|
27
|
+
});
|
|
28
|
+
this.commands = new CommandController(this, {
|
|
29
|
+
commands: {
|
|
30
|
+
'--close': () => this.close(),
|
|
31
|
+
'--fullscreen': () => this.toggleFullscreen(),
|
|
32
|
+
'--next': () => this.next(),
|
|
33
|
+
'--previous': () => this.previous(),
|
|
34
|
+
'--show': () => this.show(),
|
|
35
|
+
'--toggle': () => this.toggleOpen(),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
this.handleCancel = (event) => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
this.requestOpen(false);
|
|
41
|
+
};
|
|
42
|
+
this.handleClose = (event) => {
|
|
43
|
+
if (this.open) {
|
|
44
|
+
this.requestOpen(false);
|
|
45
|
+
}
|
|
46
|
+
this.relayNativeEvent(event);
|
|
47
|
+
};
|
|
48
|
+
this.handleKeydown = (event) => {
|
|
49
|
+
const along = inlineStep(this, event.key);
|
|
50
|
+
if (along === 1 || event.key === 'ArrowDown') {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
this.next();
|
|
53
|
+
}
|
|
54
|
+
else if (along === -1 || event.key === 'ArrowUp') {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
this.previous();
|
|
57
|
+
}
|
|
58
|
+
else if (event.key === 'Home') {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
this.toEnd(0);
|
|
61
|
+
}
|
|
62
|
+
else if (event.key === 'End') {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
this.toEnd(this.items.length - 1);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
this.handleBackdrop = (event) => {
|
|
68
|
+
const panel = this.dialog;
|
|
69
|
+
if (!panel || event.target !== panel) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const box = panel.getBoundingClientRect();
|
|
73
|
+
if (event.clientX >= box.left &&
|
|
74
|
+
event.clientX <= box.right &&
|
|
75
|
+
event.clientY >= box.top &&
|
|
76
|
+
event.clientY <= box.bottom) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.requestOpen(false);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
get items() {
|
|
83
|
+
return [...this.children].filter((child) => child instanceof HelixHeadlessLightboxItem);
|
|
84
|
+
}
|
|
85
|
+
get dialog() {
|
|
86
|
+
return this.renderRoot?.querySelector('dialog') ?? null;
|
|
87
|
+
}
|
|
88
|
+
show() {
|
|
89
|
+
this.requestOpen(true);
|
|
90
|
+
}
|
|
91
|
+
close() {
|
|
92
|
+
this.requestOpen(false);
|
|
93
|
+
}
|
|
94
|
+
next() {
|
|
95
|
+
this.step(1);
|
|
96
|
+
}
|
|
97
|
+
previous() {
|
|
98
|
+
this.step(-1);
|
|
99
|
+
}
|
|
100
|
+
toggleFullscreen() {
|
|
101
|
+
void this.fullscreen.toggle();
|
|
102
|
+
}
|
|
103
|
+
step(by) {
|
|
104
|
+
const items = this.items;
|
|
105
|
+
if (items.length === 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const current = items.findIndex((item) => item.value === this.active);
|
|
109
|
+
const from = current === -1 ? 0 : current;
|
|
110
|
+
const last = items.length - 1;
|
|
111
|
+
const next = this.loop
|
|
112
|
+
? (from + by + items.length) % items.length
|
|
113
|
+
: Math.min(Math.max(from + by, 0), last);
|
|
114
|
+
const target = items[next];
|
|
115
|
+
if (target && target.value !== this.active) {
|
|
116
|
+
this.requestActive(target.value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
toEnd(at) {
|
|
120
|
+
const target = this.items[at];
|
|
121
|
+
if (target && target.value !== this.active) {
|
|
122
|
+
this.requestActive(target.value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
willUpdate(changed) {
|
|
126
|
+
super.willUpdate(changed);
|
|
127
|
+
hostAria(this.internals, { label: this.label });
|
|
128
|
+
this.toggleState('fullscreen', this.filled);
|
|
129
|
+
this.toggleState('idle', this.resting);
|
|
130
|
+
this.say(changed);
|
|
131
|
+
}
|
|
132
|
+
updated(changed) {
|
|
133
|
+
super.updated(changed);
|
|
134
|
+
this.mark();
|
|
135
|
+
const dialog = this.dialog;
|
|
136
|
+
if (!dialog) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this.open && !dialog.open) {
|
|
140
|
+
dialog.showModal();
|
|
141
|
+
this.idle.stir();
|
|
142
|
+
}
|
|
143
|
+
else if (!this.open && dialog.open) {
|
|
144
|
+
dialog.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
mark() {
|
|
148
|
+
const items = this.items;
|
|
149
|
+
const current = resolveActiveItem(this.active, items);
|
|
150
|
+
for (const item of items) {
|
|
151
|
+
item.current = item === current;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
say(changed) {
|
|
155
|
+
if (!changed.has('active') && !changed.has('open')) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!this.open || this.statusLabel === '') {
|
|
159
|
+
this.announcement = '';
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const items = this.items;
|
|
163
|
+
const current = resolveActiveItem(this.active, items);
|
|
164
|
+
const at = current ? items.indexOf(current) : -1;
|
|
165
|
+
this.announcement = at === -1 ? '' : fill(this.statusLabel, at + 1, items.length);
|
|
166
|
+
}
|
|
167
|
+
render() {
|
|
168
|
+
return html `<dialog
|
|
169
|
+
part="dialog"
|
|
170
|
+
aria-label=${this.label || 'Lightbox'}
|
|
171
|
+
@cancel=${this.handleCancel}
|
|
172
|
+
@click=${this.handleBackdrop}
|
|
173
|
+
@close=${this.handleClose}
|
|
174
|
+
@focusin=${this.idle.stir}
|
|
175
|
+
@keydown=${this.handleKeydown}
|
|
176
|
+
@pointerleave=${this.idle.leave}
|
|
177
|
+
@pointermove=${this.idle.stir}
|
|
178
|
+
>
|
|
179
|
+
<slot name="header"></slot>
|
|
180
|
+
<div part="viewport">
|
|
181
|
+
<slot></slot>
|
|
182
|
+
</div>
|
|
183
|
+
<slot name="footer"></slot>
|
|
184
|
+
<span part="live" role="status" aria-live="polite">${this.announcement || nothing}</span>
|
|
185
|
+
</dialog>`;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
__decorate([
|
|
189
|
+
property({ attribute: 'idle-delay', type: Number })
|
|
190
|
+
], HelixHeadlessLightbox.prototype, "idleDelay", void 0);
|
|
191
|
+
__decorate([
|
|
192
|
+
property()
|
|
193
|
+
], HelixHeadlessLightbox.prototype, "label", void 0);
|
|
194
|
+
__decorate([
|
|
195
|
+
property({ reflect: true, type: Boolean })
|
|
196
|
+
], HelixHeadlessLightbox.prototype, "loop", void 0);
|
|
197
|
+
__decorate([
|
|
198
|
+
property({ attribute: 'status-label' })
|
|
199
|
+
], HelixHeadlessLightbox.prototype, "statusLabel", void 0);
|
|
200
|
+
__decorate([
|
|
201
|
+
state()
|
|
202
|
+
], HelixHeadlessLightbox.prototype, "resting", void 0);
|
|
203
|
+
__decorate([
|
|
204
|
+
state()
|
|
205
|
+
], HelixHeadlessLightbox.prototype, "filled", void 0);
|
|
206
|
+
__decorate([
|
|
207
|
+
state()
|
|
208
|
+
], HelixHeadlessLightbox.prototype, "announcement", void 0);
|
|
209
|
+
HelixHeadlessLightbox = __decorate([
|
|
210
|
+
customElement('helix-headless-lightbox')
|
|
211
|
+
], HelixHeadlessLightbox);
|
|
212
|
+
export { HelixHeadlessLightbox };
|
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.101.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -46,13 +46,13 @@
|
|
|
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.101.0",
|
|
50
|
+
"@fusion.dev/helix-icons": "0.101.0",
|
|
51
51
|
"uqr": "^0.1.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@fusion.dev/helix-cem": "0.
|
|
55
|
-
"@fusion.dev/helix-testing": "0.
|
|
54
|
+
"@fusion.dev/helix-cem": "0.101.0",
|
|
55
|
+
"@fusion.dev/helix-testing": "0.101.0",
|
|
56
56
|
"@fusion.dev/scripts": "0.0.0-dev",
|
|
57
57
|
"@happy-dom/global-registrator": "^20.0.0",
|
|
58
58
|
"@lit-labs/ssr": "^4.1.0",
|