@lmfaole/basics 0.1.0 → 0.2.1

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.
@@ -0,0 +1,272 @@
1
+ const ElementBase = globalThis.Element ?? class {};
2
+ const HTMLElementBase = globalThis.HTMLElement ?? class {};
3
+ const HTMLButtonElementBase = globalThis.HTMLButtonElement ?? class {};
4
+ const HTMLDialogElementBase = globalThis.HTMLDialogElement ?? class {};
5
+
6
+ export const DIALOG_TAG_NAME = "basic-dialog";
7
+
8
+ const DEFAULT_LABEL = "Dialog";
9
+ const PANEL_SELECTOR = "[data-dialog-panel]";
10
+ const TITLE_SELECTOR = "[data-dialog-title]";
11
+ const OPEN_SELECTOR = "[data-dialog-open]";
12
+ const CLOSE_SELECTOR = "[data-dialog-close]";
13
+ const MANAGED_LABEL_ATTRIBUTE = "data-basic-dialog-managed-label";
14
+ const MANAGED_LABELLEDBY_ATTRIBUTE = "data-basic-dialog-managed-labelledby";
15
+
16
+ let nextDialogInstanceId = 1;
17
+
18
+ function collectOwnedElements(root, scope, selector) {
19
+ return Array.from(scope.querySelectorAll(selector)).filter(
20
+ (element) => element instanceof HTMLElementBase && element.closest(DIALOG_TAG_NAME) === root,
21
+ );
22
+ }
23
+
24
+ function normalizeDialogCloseValue(value) {
25
+ return value?.trim() ?? "";
26
+ }
27
+
28
+ export function normalizeDialogBackdropClose(value) {
29
+ if (value == null) {
30
+ return false;
31
+ }
32
+
33
+ const normalized = value.trim().toLowerCase();
34
+ return normalized === "" || normalized === "true";
35
+ }
36
+
37
+ export function normalizeDialogLabel(value) {
38
+ return value?.trim() || DEFAULT_LABEL;
39
+ }
40
+
41
+ export class DialogElement extends HTMLElementBase {
42
+ static observedAttributes = ["data-backdrop-close", "data-label"];
43
+
44
+ #instanceId = `${DIALOG_TAG_NAME}-${nextDialogInstanceId++}`;
45
+ #panel = null;
46
+ #panelWithEvents = null;
47
+ #title = null;
48
+ #openButtons = [];
49
+ #closeButtons = [];
50
+ #restoreFocusTo = null;
51
+ #eventsBound = false;
52
+
53
+ connectedCallback() {
54
+ if (!this.#eventsBound) {
55
+ this.addEventListener("click", this.#handleClick);
56
+ this.#eventsBound = true;
57
+ }
58
+
59
+ this.#sync();
60
+ }
61
+
62
+ disconnectedCallback() {
63
+ if (this.#eventsBound) {
64
+ this.removeEventListener("click", this.#handleClick);
65
+ this.#eventsBound = false;
66
+ }
67
+
68
+ this.#syncPanelEvents(null);
69
+ }
70
+
71
+ attributeChangedCallback() {
72
+ this.#sync();
73
+ }
74
+
75
+ showModal(opener = null) {
76
+ this.#sync();
77
+
78
+ if (
79
+ !(this.#panel instanceof HTMLDialogElementBase)
80
+ || typeof this.#panel.showModal !== "function"
81
+ ) {
82
+ return false;
83
+ }
84
+
85
+ if (this.#panel.open) {
86
+ this.#applyState();
87
+ return true;
88
+ }
89
+
90
+ const fallbackOpener = opener instanceof HTMLElementBase
91
+ ? opener
92
+ : this.ownerDocument?.activeElement instanceof HTMLElementBase
93
+ ? this.ownerDocument.activeElement
94
+ : null;
95
+
96
+ this.#restoreFocusTo = fallbackOpener;
97
+ this.#panel.showModal();
98
+ this.#applyState();
99
+ return true;
100
+ }
101
+
102
+ close(returnValue = "") {
103
+ if (
104
+ !(this.#panel instanceof HTMLDialogElementBase)
105
+ || typeof this.#panel.close !== "function"
106
+ || !this.#panel.open
107
+ ) {
108
+ return false;
109
+ }
110
+
111
+ this.#panel.close(returnValue);
112
+ return true;
113
+ }
114
+
115
+ #handleClick = (event) => {
116
+ if (!(event.target instanceof ElementBase)) {
117
+ return;
118
+ }
119
+
120
+ const openButton = event.target.closest(OPEN_SELECTOR);
121
+
122
+ if (
123
+ openButton instanceof HTMLElementBase
124
+ && openButton.closest(DIALOG_TAG_NAME) === this
125
+ ) {
126
+ event.preventDefault();
127
+ this.showModal(openButton);
128
+ return;
129
+ }
130
+
131
+ const closeButton = event.target.closest(CLOSE_SELECTOR);
132
+
133
+ if (
134
+ closeButton instanceof HTMLElementBase
135
+ && closeButton.closest(DIALOG_TAG_NAME) === this
136
+ ) {
137
+ event.preventDefault();
138
+ this.close(normalizeDialogCloseValue(
139
+ closeButton.getAttribute("data-dialog-close-value"),
140
+ ));
141
+ return;
142
+ }
143
+
144
+ if (
145
+ event.target === this.#panel
146
+ && normalizeDialogBackdropClose(this.getAttribute("data-backdrop-close"))
147
+ ) {
148
+ this.close();
149
+ }
150
+ };
151
+
152
+ #handleDialogClose = () => {
153
+ this.#applyState();
154
+
155
+ if (
156
+ this.#restoreFocusTo instanceof HTMLElementBase
157
+ && this.#restoreFocusTo.isConnected
158
+ ) {
159
+ this.#restoreFocusTo.focus();
160
+ }
161
+
162
+ this.#restoreFocusTo = null;
163
+ };
164
+
165
+ #handleDialogCancel = () => {
166
+ this.#applyState();
167
+ };
168
+
169
+ #sync() {
170
+ const nextPanel = collectOwnedElements(this, this, PANEL_SELECTOR)[0] ?? null;
171
+ const nextTitle = collectOwnedElements(this, this, TITLE_SELECTOR)[0] ?? null;
172
+
173
+ this.#syncPanelEvents(nextPanel instanceof HTMLDialogElementBase ? nextPanel : null);
174
+ this.#panel = nextPanel instanceof HTMLDialogElementBase ? nextPanel : null;
175
+ this.#title = nextTitle instanceof HTMLElementBase ? nextTitle : null;
176
+ this.#openButtons = collectOwnedElements(this, this, OPEN_SELECTOR);
177
+ this.#closeButtons = collectOwnedElements(this, this, CLOSE_SELECTOR);
178
+ this.#applyState();
179
+ }
180
+
181
+ #syncPanelEvents(nextPanel) {
182
+ if (this.#panelWithEvents === nextPanel) {
183
+ return;
184
+ }
185
+
186
+ if (this.#panelWithEvents instanceof HTMLDialogElementBase) {
187
+ this.#panelWithEvents.removeEventListener("close", this.#handleDialogClose);
188
+ this.#panelWithEvents.removeEventListener("cancel", this.#handleDialogCancel);
189
+ }
190
+
191
+ if (nextPanel instanceof HTMLDialogElementBase) {
192
+ nextPanel.addEventListener("close", this.#handleDialogClose);
193
+ nextPanel.addEventListener("cancel", this.#handleDialogCancel);
194
+ }
195
+
196
+ this.#panelWithEvents = nextPanel;
197
+ }
198
+
199
+ #applyState() {
200
+ for (const button of [...this.#openButtons, ...this.#closeButtons]) {
201
+ if (button instanceof HTMLButtonElementBase && !button.hasAttribute("type")) {
202
+ button.type = "button";
203
+ }
204
+ }
205
+
206
+ if (!(this.#panel instanceof HTMLDialogElementBase)) {
207
+ this.toggleAttribute("data-open", false);
208
+ return;
209
+ }
210
+
211
+ const baseId = this.id || this.#instanceId;
212
+
213
+ if (this.#title instanceof HTMLElementBase && !this.#title.id) {
214
+ this.#title.id = `${baseId}-title`;
215
+ }
216
+
217
+ this.#panel.setAttribute("aria-modal", "true");
218
+ this.#syncAccessibleLabel();
219
+ this.toggleAttribute("data-open", this.#panel.open);
220
+ }
221
+
222
+ #syncAccessibleLabel() {
223
+ if (!(this.#panel instanceof HTMLDialogElementBase)) {
224
+ return;
225
+ }
226
+
227
+ if (this.#title?.id) {
228
+ if (this.#panel.hasAttribute(MANAGED_LABEL_ATTRIBUTE)) {
229
+ this.#panel.removeAttribute("aria-label");
230
+ this.#panel.removeAttribute(MANAGED_LABEL_ATTRIBUTE);
231
+ }
232
+
233
+ if (
234
+ !this.#panel.hasAttribute("aria-labelledby")
235
+ || this.#panel.hasAttribute(MANAGED_LABELLEDBY_ATTRIBUTE)
236
+ ) {
237
+ this.#panel.setAttribute("aria-labelledby", this.#title.id);
238
+ this.#panel.setAttribute(MANAGED_LABELLEDBY_ATTRIBUTE, "");
239
+ }
240
+
241
+ return;
242
+ }
243
+
244
+ if (this.#panel.hasAttribute(MANAGED_LABELLEDBY_ATTRIBUTE)) {
245
+ this.#panel.removeAttribute("aria-labelledby");
246
+ this.#panel.removeAttribute(MANAGED_LABELLEDBY_ATTRIBUTE);
247
+ }
248
+
249
+ if (
250
+ !this.#panel.hasAttribute("aria-label")
251
+ || this.#panel.hasAttribute(MANAGED_LABEL_ATTRIBUTE)
252
+ ) {
253
+ this.#panel.setAttribute(
254
+ "aria-label",
255
+ normalizeDialogLabel(this.getAttribute("data-label")),
256
+ );
257
+ this.#panel.setAttribute(MANAGED_LABEL_ATTRIBUTE, "");
258
+ }
259
+ }
260
+ }
261
+
262
+ export function defineDialog(registry = globalThis.customElements) {
263
+ if (!registry?.get || !registry?.define) {
264
+ return DialogElement;
265
+ }
266
+
267
+ if (!registry.get(DIALOG_TAG_NAME)) {
268
+ registry.define(DIALOG_TAG_NAME, DialogElement);
269
+ }
270
+
271
+ return DialogElement;
272
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { defineDialog } from "./index.js";
2
+
3
+ defineDialog();
@@ -0,0 +1,70 @@
1
+ export const POPOVER_TAG_NAME: "basic-popover";
2
+
3
+ /**
4
+ * Normalizes unsupported or empty labels back to the default `"Popover"`.
5
+ */
6
+ export function normalizePopoverLabel(
7
+ value?: string | null,
8
+ ): string;
9
+
10
+ /**
11
+ * Normalizes the root `data-anchor-trigger` attribute into a boolean flag.
12
+ */
13
+ export function normalizePopoverAnchorTrigger(
14
+ value?: string | null,
15
+ ): boolean;
16
+
17
+ /**
18
+ * Normalizes unsupported or empty position-area values back to `"bottom"`.
19
+ */
20
+ export function normalizePopoverPositionArea(
21
+ value?: string | null,
22
+ ): string;
23
+
24
+ /**
25
+ * Returns the built-in fallback sequence used for a given default anchored placement.
26
+ */
27
+ export function getDefaultPopoverPositionTryFallbacks(
28
+ positionArea?: string | null,
29
+ ): string;
30
+
31
+ /**
32
+ * Normalizes custom fallback values and otherwise returns the built-in fallback
33
+ * sequence derived from the default position area.
34
+ */
35
+ export function normalizePopoverPositionTryFallbacks(
36
+ value?: string | null,
37
+ positionArea?: string | null,
38
+ ): string;
39
+
40
+ /**
41
+ * Returns whether the managed popover panel is currently open.
42
+ */
43
+ export function isPopoverOpen(
44
+ panel: HTMLElement | null | undefined,
45
+ ): boolean;
46
+
47
+ /**
48
+ * Custom element that upgrades popover trigger-and-panel markup into a
49
+ * non-modal overlay flow.
50
+ *
51
+ * Attributes:
52
+ * - `data-anchor-trigger`: establishes the opener as the popover's implicit anchor
53
+ * - `data-label`: fallback accessible name when the popover has no title
54
+ * - `data-position-area`: CSS anchor-positioning area to use when anchoring is enabled
55
+ * - `data-position-try-fallbacks`: optional CSS fallback list to try when the
56
+ * default anchored position would overflow
57
+ */
58
+ export class PopoverElement extends HTMLElement {
59
+ static observedAttributes: string[];
60
+ show(opener?: HTMLElement | null): boolean;
61
+ hide(): boolean;
62
+ toggle(opener?: HTMLElement | null): boolean;
63
+ }
64
+
65
+ /**
66
+ * Registers the `basic-popover` custom element if it is not already defined.
67
+ */
68
+ export function definePopover(
69
+ registry?: CustomElementRegistry,
70
+ ): typeof PopoverElement;