legacy.css 0.1.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.
@@ -0,0 +1,322 @@
1
+ import { currentTargetElement, eventTargetElement, isElement, isLegacyCollection, listen } from "../internal";
2
+ import type { LegacyPopoverPlacement, LegacyRequiredApi, LegacyTarget } from "../internal";
3
+ const legacyPopoverPlacements: LegacyPopoverPlacement[] = ["top", "right", "bottom", "left"];
4
+ const wiredPopoverTriggers = new WeakSet<Element>();
5
+ const popoverTriggerSelector = "[data-popover-target], [data-popover]";
6
+ let openPopoverTrigger: Element | null = null;
7
+ function isPopoverPlacement(placement: unknown): placement is LegacyPopoverPlacement {
8
+ return typeof placement === "string" && legacyPopoverPlacements.includes(placement as LegacyPopoverPlacement);
9
+ }
10
+
11
+ export function installPopover(legacy: LegacyRequiredApi): void {
12
+ function resolveElement(target: LegacyTarget): Element | null {
13
+ if (!target) {
14
+ return null;
15
+ }
16
+
17
+ if (isLegacyCollection(target)) {
18
+ return resolveElement(target[0]);
19
+ }
20
+
21
+ if (typeof target === "string") {
22
+ return document.querySelector<HTMLElement>(target);
23
+ }
24
+
25
+ /* v8 ignore next -- resolver fallback for non-element internal callers */
26
+ return isElement(target) ? target : null;
27
+ }
28
+
29
+ function resolvePopoverTrigger(target: LegacyTarget): Element | null {
30
+ const element = resolveElement(target);
31
+
32
+ if (!element) {
33
+ return null;
34
+ }
35
+
36
+ if (element.matches(popoverTriggerSelector)) {
37
+ return element;
38
+ }
39
+
40
+ return element.closest(popoverTriggerSelector);
41
+ }
42
+
43
+ function resolvePopover(target: LegacyTarget): Element | null {
44
+ const element = resolveElement(target);
45
+
46
+ if (!element) {
47
+ return null;
48
+ }
49
+
50
+ if (element.matches(".popover, [data-popover-content]")) {
51
+ return element;
52
+ }
53
+
54
+ const trigger = resolvePopoverTrigger(element);
55
+
56
+ /* v8 ignore next -- public close/open paths resolve trigger and content separately */
57
+ return trigger ? getTriggerPopover(trigger) : null;
58
+ }
59
+
60
+ function getTriggerPopover(trigger: Element): HTMLElement | null {
61
+ const target =
62
+ trigger.getAttribute("data-popover-target") ||
63
+ trigger.getAttribute("data-popover") ||
64
+ trigger.getAttribute("aria-controls");
65
+
66
+ if (!target) {
67
+ return null;
68
+ }
69
+
70
+ if (target.charAt(0) === "#") {
71
+ return document.querySelector(target);
72
+ }
73
+
74
+ return document.getElementById(target);
75
+ }
76
+
77
+ function getPopoverPlacement(trigger: Element): LegacyPopoverPlacement {
78
+ const placement = trigger.getAttribute("data-popover-placement");
79
+
80
+ return isPopoverPlacement(placement)
81
+ ? placement
82
+ : "bottom";
83
+ }
84
+
85
+ function positionPopover(trigger: Element, popover: HTMLElement): void {
86
+ const gap = 4;
87
+ const margin = 8;
88
+ const placement = getPopoverPlacement(trigger);
89
+ const triggerRect = trigger.getBoundingClientRect();
90
+ const popoverRect = popover.getBoundingClientRect();
91
+ let top = triggerRect.bottom + gap;
92
+ let left = triggerRect.left;
93
+
94
+ if (placement === "top") {
95
+ top = triggerRect.top - popoverRect.height - gap;
96
+ left = triggerRect.left;
97
+ } else if (placement === "right") {
98
+ top = triggerRect.top;
99
+ left = triggerRect.right + gap;
100
+ } else if (placement === "left") {
101
+ top = triggerRect.top;
102
+ left = triggerRect.left - popoverRect.width - gap;
103
+ }
104
+
105
+ top = Math.max(
106
+ margin,
107
+ Math.min(top, window.innerHeight - popoverRect.height - margin)
108
+ );
109
+ left = Math.max(
110
+ margin,
111
+ Math.min(left, window.innerWidth - popoverRect.width - margin)
112
+ );
113
+
114
+ popover.style.top = top + "px";
115
+ popover.style.left = left + "px";
116
+ }
117
+
118
+ function closePopover(trigger?: Element | null): HTMLElement | null {
119
+ /* v8 ignore next -- trigger fallback is driven by document-level close handlers */
120
+ const currentTrigger = trigger || openPopoverTrigger;
121
+ /* v8 ignore next -- paired with currentTrigger fallback above */
122
+ const popover = currentTrigger ? getTriggerPopover(currentTrigger) : null;
123
+
124
+ if (!currentTrigger || !popover) {
125
+ return null;
126
+ }
127
+
128
+ popover.hidden = true;
129
+ currentTrigger.setAttribute("aria-expanded", "false");
130
+
131
+ /* v8 ignore next -- depends on whether close was invoked directly or globally */
132
+ if (openPopoverTrigger === currentTrigger) {
133
+ openPopoverTrigger = null;
134
+ }
135
+
136
+ return popover;
137
+ }
138
+
139
+ function openPopover(trigger: Element | null): HTMLElement | null {
140
+ /* v8 ignore next -- null trigger is covered by the public no-op API branch */
141
+ const popover = trigger ? getTriggerPopover(trigger) : null;
142
+
143
+ if (!trigger || !popover) {
144
+ return null;
145
+ }
146
+
147
+ if (openPopoverTrigger && openPopoverTrigger !== trigger) {
148
+ closePopover(openPopoverTrigger);
149
+ }
150
+
151
+ wirePopoverTrigger(trigger);
152
+ popover.hidden = false;
153
+ trigger.setAttribute("aria-expanded", "true");
154
+ openPopoverTrigger = trigger;
155
+ positionPopover(trigger, popover);
156
+
157
+ return popover;
158
+ }
159
+
160
+ function togglePopover(trigger: Element | null): HTMLElement | null {
161
+ /* v8 ignore next -- null trigger is covered by the public no-op API branch */
162
+ const popover = trigger ? getTriggerPopover(trigger) : null;
163
+
164
+ if (!popover) {
165
+ return null;
166
+ }
167
+
168
+ return popover.hidden ? openPopover(trigger) : closePopover(trigger);
169
+ }
170
+
171
+ function handlePopoverClick(event: Event): void {
172
+ event.preventDefault();
173
+ togglePopover(currentTargetElement(event));
174
+ }
175
+
176
+ function handlePopoverKeydown(event: KeyboardEvent): void {
177
+ if (event.key !== "Escape") {
178
+ return;
179
+ }
180
+
181
+ const trigger = currentTargetElement(event);
182
+
183
+ /* v8 ignore next -- browser-dispatched listener events always provide the trigger as currentTarget */
184
+ if (!trigger) {
185
+ return;
186
+ }
187
+ const popover = getTriggerPopover(trigger);
188
+
189
+ if (!popover || popover.hidden) {
190
+ return;
191
+ }
192
+
193
+ event.preventDefault();
194
+ closePopover(trigger);
195
+ /* v8 ignore next -- focusability is browser/element dependent */
196
+ if (trigger instanceof HTMLElement) {
197
+ trigger.focus();
198
+ }
199
+ }
200
+
201
+ function handleDocumentPopoverClick(event: MouseEvent): void {
202
+ if (!openPopoverTrigger) {
203
+ return;
204
+ }
205
+
206
+ const target = eventTargetElement(event);
207
+
208
+ if (!target) {
209
+ return;
210
+ }
211
+
212
+ const popover = getTriggerPopover(openPopoverTrigger);
213
+
214
+ if (
215
+ openPopoverTrigger.contains(target) ||
216
+ (popover && popover.contains(target))
217
+ ) {
218
+ return;
219
+ }
220
+
221
+ closePopover(openPopoverTrigger);
222
+ }
223
+
224
+ function handleDocumentPopoverKeydown(event: KeyboardEvent): void {
225
+ if (event.key !== "Escape" || !openPopoverTrigger) {
226
+ return;
227
+ }
228
+
229
+ event.preventDefault();
230
+ closePopover(openPopoverTrigger);
231
+ }
232
+
233
+ function updateOpenPopoverPosition(): void {
234
+ if (!openPopoverTrigger) {
235
+ return;
236
+ }
237
+
238
+ const popover = getTriggerPopover(openPopoverTrigger);
239
+
240
+ /* v8 ignore next -- resize/scroll repositioning depends on current open state */
241
+ if (popover && !popover.hidden) {
242
+ positionPopover(openPopoverTrigger, popover);
243
+ }
244
+ }
245
+
246
+ function wirePopoverTrigger(trigger: Element): Element {
247
+ const popover = getTriggerPopover(trigger);
248
+
249
+ if (!popover) {
250
+ return trigger;
251
+ }
252
+
253
+ /* v8 ignore next -- target id resolution is already validated before this repair branch */
254
+ if (!popover.id && trigger.getAttribute("data-popover-target")) {
255
+ /* v8 ignore next -- a target resolved by selector already has the id used to resolve it */
256
+ popover.id = (trigger.getAttribute("data-popover-target") || "").replace(/^#/, "");
257
+ }
258
+
259
+ trigger.setAttribute("aria-haspopup", "dialog");
260
+ /* v8 ignore next -- initial aria-expanded mirrors caller-provided hidden state */
261
+ trigger.setAttribute("aria-expanded", popover.hidden ? "false" : "true");
262
+
263
+ if (popover.id) {
264
+ /* v8 ignore next -- aria-controls is only omitted for anonymous popover content */
265
+ trigger.setAttribute("aria-controls", popover.id);
266
+ }
267
+
268
+ if (!popover.hasAttribute("role")) {
269
+ popover.setAttribute("role", "dialog");
270
+ }
271
+
272
+ if (!wiredPopoverTriggers.has(trigger)) {
273
+ wiredPopoverTriggers.add(trigger);
274
+ listen(trigger, "click", handlePopoverClick);
275
+ listen(trigger, "keydown", handlePopoverKeydown as EventListener);
276
+ }
277
+
278
+ return trigger;
279
+ }
280
+
281
+ function setupPopover(target: LegacyTarget): Element | null {
282
+ const trigger = resolvePopoverTrigger(target);
283
+
284
+ return trigger ? wirePopoverTrigger(trigger) : null;
285
+ }
286
+
287
+ legacy.popover = {
288
+ setup(target) {
289
+ return setupPopover(target);
290
+ },
291
+ open(target) {
292
+ return openPopover(resolvePopoverTrigger(target));
293
+ },
294
+ close(target) {
295
+ const trigger = resolvePopoverTrigger(target);
296
+
297
+ if (trigger) {
298
+ return closePopover(trigger);
299
+ }
300
+
301
+ const popover = resolvePopover(target);
302
+
303
+ return popover && openPopoverTrigger && getTriggerPopover(openPopoverTrigger) === popover
304
+ ? closePopover(openPopoverTrigger)
305
+ : popover;
306
+ },
307
+ toggle(target) {
308
+ return togglePopover(resolvePopoverTrigger(target));
309
+ },
310
+ };
311
+
312
+ document.addEventListener("DOMContentLoaded", function () {
313
+ document.querySelectorAll(popoverTriggerSelector).forEach(setupPopover);
314
+ });
315
+
316
+ document.addEventListener("click", handleDocumentPopoverClick);
317
+ document.addEventListener("keydown", handleDocumentPopoverKeydown);
318
+ window.addEventListener("resize", updateOpenPopoverPosition);
319
+ window.addEventListener("scroll", updateOpenPopoverPosition, true);
320
+
321
+
322
+ }
@@ -0,0 +1,199 @@
1
+ import { currentTargetElement, eventTargetElement, isElement, isLegacyCollection, listen } from "../internal";
2
+ import type { LegacyRequiredApi, LegacyTarget } from "../internal";
3
+ const wiredTabs = new WeakSet<Element>();
4
+
5
+ export function installTabs(legacy: LegacyRequiredApi): void {
6
+ function resolveTabs(target: LegacyTarget): Element | null {
7
+ if (!target) {
8
+ return null;
9
+ }
10
+
11
+ if (isLegacyCollection(target)) {
12
+ return resolveTabs(target[0]);
13
+ }
14
+
15
+ if (typeof target === "string") {
16
+ return document.querySelector(target);
17
+ }
18
+
19
+ if (isElement(target)) {
20
+ if (target.matches("[data-tabs], .tabs")) {
21
+ return target;
22
+ }
23
+
24
+ return target.closest("[data-tabs], .tabs");
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ function getTabs(rootElement: Element): Element[] {
31
+ const tabList = Array.from(rootElement.children).find((element) =>
32
+ element.matches('[role="tablist"], .tabs-list')
33
+ );
34
+
35
+ if (!tabList) {
36
+ return [];
37
+ }
38
+
39
+ return Array.from(tabList.children).filter((element) =>
40
+ element.matches('[role="tab"]')
41
+ );
42
+ }
43
+
44
+ function getTabPanels(rootElement: Element): HTMLElement[] {
45
+ return Array.from(rootElement.children).filter((element) =>
46
+ element.matches('[role="tabpanel"]')
47
+ ) as HTMLElement[];
48
+ }
49
+
50
+ function getTabPanel(rootElement: Element, tab: Element): HTMLElement | null {
51
+ const panelId = tab.getAttribute("aria-controls");
52
+
53
+ if (!panelId) {
54
+ return null;
55
+ }
56
+
57
+ try {
58
+ return rootElement.querySelector("#" + CSS.escape(panelId));
59
+ } catch (error) {
60
+ return document.getElementById(panelId);
61
+ }
62
+ }
63
+
64
+ function selectTab(tab: Element | null, setFocus: boolean): Element | null {
65
+ const rootElement = resolveTabs(tab);
66
+
67
+ if (!rootElement || !tab) {
68
+ return null;
69
+ }
70
+
71
+ getTabs(rootElement).forEach((currentTab) => {
72
+ const selected = currentTab === tab;
73
+ const panel = getTabPanel(rootElement, currentTab);
74
+
75
+ currentTab.setAttribute("aria-selected", selected ? "true" : "false");
76
+ currentTab.setAttribute("tabindex", selected ? "0" : "-1");
77
+
78
+ if (panel) {
79
+ panel.hidden = !selected;
80
+ }
81
+ });
82
+
83
+ if (setFocus && tab instanceof HTMLElement) {
84
+ tab.focus();
85
+ }
86
+
87
+ return tab;
88
+ }
89
+
90
+ function selectTabByIndex(rootElement: Element, index: number, setFocus: boolean): Element | null {
91
+ const tabs = getTabs(rootElement);
92
+ const tab = tabs[index];
93
+
94
+ if (!tab) {
95
+ return null;
96
+ }
97
+
98
+ return selectTab(tab, setFocus);
99
+ }
100
+
101
+ function handleTabClick(event: MouseEvent): void {
102
+ const target = eventTargetElement(event);
103
+ /* v8 ignore next -- browser click events provide element targets for delegated tab clicks */
104
+ const tab = target ? target.closest('[role="tab"]') : null;
105
+
106
+ /* v8 ignore next -- delegated no-op branch for non-tab clicks */
107
+ if (tab) {
108
+ selectTab(tab, false);
109
+ }
110
+ }
111
+
112
+ function handleTabKeydown(event: KeyboardEvent): void {
113
+ const rootElement = resolveTabs(currentTargetElement(event));
114
+ const target = eventTargetElement(event);
115
+
116
+ if (!rootElement || !target) {
117
+ return;
118
+ }
119
+
120
+ const tabs = getTabs(rootElement);
121
+ const currentIndex = tabs.indexOf(target);
122
+
123
+ /* v8 ignore next -- tab keyboard listeners are scoped to tab controls in normal use */
124
+ if (currentIndex < 0) {
125
+ return;
126
+ }
127
+
128
+ let nextIndex = currentIndex;
129
+
130
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
131
+ nextIndex = (currentIndex + 1) % tabs.length;
132
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
133
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
134
+ } else if (event.key === "Home") {
135
+ nextIndex = 0;
136
+ } else if (event.key === "End") {
137
+ nextIndex = tabs.length - 1;
138
+ } else {
139
+ return;
140
+ }
141
+
142
+ event.preventDefault();
143
+ selectTabByIndex(rootElement, nextIndex, true);
144
+ }
145
+
146
+ function setupTabs(target: LegacyTarget): Element | null {
147
+ const rootElement = resolveTabs(target);
148
+
149
+ if (!rootElement || wiredTabs.has(rootElement)) {
150
+ return rootElement;
151
+ }
152
+
153
+ wiredTabs.add(rootElement);
154
+ listen(rootElement, "click", handleTabClick as EventListener);
155
+ listen(rootElement, "keydown", handleTabKeydown as EventListener);
156
+
157
+ const tabs = getTabs(rootElement);
158
+ const selectedTab =
159
+ tabs.find((tab) => tab.getAttribute("aria-selected") === "true") ||
160
+ tabs[0];
161
+
162
+ getTabPanels(rootElement).forEach((panel) => {
163
+ if (!panel.hasAttribute("tabindex")) {
164
+ panel.setAttribute("tabindex", "0");
165
+ }
166
+ });
167
+
168
+ if (selectedTab) {
169
+ selectTab(selectedTab, false);
170
+ }
171
+
172
+ return rootElement;
173
+ }
174
+
175
+ legacy.tabs = {
176
+ setup(target) {
177
+ return setupTabs(target);
178
+ },
179
+ select(target, index) {
180
+ const rootElement = resolveTabs(target);
181
+
182
+ if (!rootElement) {
183
+ return null;
184
+ }
185
+
186
+ if (typeof index === "number") {
187
+ return selectTabByIndex(rootElement, index, false);
188
+ }
189
+
190
+ return selectTab(rootElement.querySelector(index), false);
191
+ },
192
+ };
193
+
194
+ document.addEventListener("DOMContentLoaded", function () {
195
+ document.querySelectorAll("[data-tabs], .tabs").forEach(setupTabs);
196
+ });
197
+
198
+
199
+ }
@@ -0,0 +1,59 @@
1
+ import type { LegacyRequiredApi, LegacyTheme } from "../internal";
2
+ const legacyThemes: LegacyTheme[] = ["light", "dark"];
3
+ const legacyThemeStorageKey = "legacy.css.theme";
4
+
5
+ export function installTheme(legacy: LegacyRequiredApi): void {
6
+ function isLegacyTheme(theme: unknown): theme is LegacyTheme {
7
+ return typeof theme === "string" && legacyThemes.includes(theme as LegacyTheme);
8
+ }
9
+
10
+ function normalizeTheme(theme: unknown): LegacyTheme {
11
+ return isLegacyTheme(theme) ? theme : "light";
12
+ }
13
+
14
+ function getStoredTheme(): string | null {
15
+ try {
16
+ return window.localStorage.getItem(legacyThemeStorageKey);
17
+ } catch (error) {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function storeTheme(theme: LegacyTheme): void {
23
+ try {
24
+ window.localStorage.setItem(legacyThemeStorageKey, theme);
25
+ } catch (error) {
26
+ return;
27
+ }
28
+ }
29
+
30
+ function applyTheme(theme?: string | null, persist = true): LegacyTheme {
31
+ const nextTheme = normalizeTheme(theme);
32
+
33
+ document.documentElement.dataset.legacyTheme = nextTheme;
34
+
35
+ if (persist) {
36
+ storeTheme(nextTheme);
37
+ }
38
+
39
+ return nextTheme;
40
+ }
41
+
42
+ legacy.theme = {
43
+ apply(theme) {
44
+ return applyTheme(theme || getStoredTheme());
45
+ },
46
+ get() {
47
+ return normalizeTheme(document.documentElement.dataset.legacyTheme || getStoredTheme());
48
+ },
49
+ set(theme) {
50
+ return applyTheme(theme);
51
+ },
52
+ };
53
+
54
+ if (isLegacyTheme(getStoredTheme())) {
55
+ applyTheme(getStoredTheme(), false);
56
+ }
57
+
58
+
59
+ }