@akonwi/mica 0.9.0 → 0.11.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/README.md +27 -2
- package/breadcrumbs.d.ts +5 -0
- package/breadcrumbs.js +74 -0
- package/header.d.ts +7 -0
- package/header.js +172 -0
- package/mica.css +872 -12
- package/package.json +34 -3
- package/sidebar.d.ts +11 -0
- package/sidebar.js +135 -0
- package/table.d.ts +5 -0
- package/table.js +59 -0
- package/tabs.d.ts +2 -1
- package/tabs.js +139 -17
- package/types/react.d.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akonwi/mica",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Custom elements. Native behavior. Nearly no JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -55,6 +55,22 @@
|
|
|
55
55
|
},
|
|
56
56
|
"./types/react": {
|
|
57
57
|
"types": "./types/react.d.ts"
|
|
58
|
+
},
|
|
59
|
+
"./header.js": {
|
|
60
|
+
"types": "./header.d.ts",
|
|
61
|
+
"default": "./header.js"
|
|
62
|
+
},
|
|
63
|
+
"./sidebar.js": {
|
|
64
|
+
"types": "./sidebar.d.ts",
|
|
65
|
+
"default": "./sidebar.js"
|
|
66
|
+
},
|
|
67
|
+
"./breadcrumbs.js": {
|
|
68
|
+
"types": "./breadcrumbs.d.ts",
|
|
69
|
+
"default": "./breadcrumbs.js"
|
|
70
|
+
},
|
|
71
|
+
"./table.js": {
|
|
72
|
+
"types": "./table.d.ts",
|
|
73
|
+
"default": "./table.js"
|
|
58
74
|
}
|
|
59
75
|
},
|
|
60
76
|
"files": [
|
|
@@ -76,7 +92,15 @@
|
|
|
76
92
|
"combobox.js",
|
|
77
93
|
"combobox.d.ts",
|
|
78
94
|
"types/react.d.ts",
|
|
79
|
-
"THIRD_PARTY_LICENSES.md"
|
|
95
|
+
"THIRD_PARTY_LICENSES.md",
|
|
96
|
+
"header.js",
|
|
97
|
+
"header.d.ts",
|
|
98
|
+
"sidebar.js",
|
|
99
|
+
"sidebar.d.ts",
|
|
100
|
+
"breadcrumbs.js",
|
|
101
|
+
"breadcrumbs.d.ts",
|
|
102
|
+
"table.js",
|
|
103
|
+
"table.d.ts"
|
|
80
104
|
],
|
|
81
105
|
"sideEffects": true,
|
|
82
106
|
"devDependencies": {
|
|
@@ -93,6 +117,13 @@
|
|
|
93
117
|
"docs:serve": "bun run docs:build && bun tools/serve.ts",
|
|
94
118
|
"snapshot": "bun tools/snapshot.ts",
|
|
95
119
|
"snapshot:check": "bun tools/snapshot.ts --check",
|
|
96
|
-
"snapshot:setup": "playwright install chromium webkit"
|
|
120
|
+
"snapshot:setup": "playwright install chromium webkit",
|
|
121
|
+
"header:check": "bun tools/header-check.ts",
|
|
122
|
+
"sidebar:check": "bun tools/sidebar-check.ts",
|
|
123
|
+
"navigation-menu:check": "bun tools/navigation-menu-check.ts",
|
|
124
|
+
"bottom-navigation:check": "bun tools/bottom-navigation-check.ts",
|
|
125
|
+
"breadcrumbs:check": "bun tools/breadcrumbs-check.ts",
|
|
126
|
+
"data-table:check": "bun tools/data-table-check.ts",
|
|
127
|
+
"callout:check": "bun tools/callout-check.ts"
|
|
97
128
|
}
|
|
98
129
|
}
|
package/sidebar.d.ts
ADDED
package/sidebar.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* Optional sidebar shell enhancement. The panel, dialog, controls, and all
|
|
2
|
+
* icons are authored HTML. Only the same panel is moved into the native
|
|
3
|
+
* dialog on narrow containers; no UI or navigation is generated. */
|
|
4
|
+
class MSidebarLayout extends HTMLElement {
|
|
5
|
+
#cleanup;
|
|
6
|
+
#actions;
|
|
7
|
+
#refresh;
|
|
8
|
+
static observedAttributes = ['collapse'];
|
|
9
|
+
attributeChangedCallback() { this.#refresh?.(); }
|
|
10
|
+
open() { this.#actions?.open(); }
|
|
11
|
+
close() { this.#actions?.close(); }
|
|
12
|
+
toggle() { this.#actions?.toggle(); }
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
queueMicrotask(() => {
|
|
16
|
+
if (!this.isConnected || this.#cleanup) return;
|
|
17
|
+
const sidebar = this.querySelector(':scope > m-sidebar');
|
|
18
|
+
const main = this.querySelector(':scope > [data-sidebar-main]');
|
|
19
|
+
const dialog = this.querySelector(':scope > dialog[data-sidebar-dialog]');
|
|
20
|
+
if (!sidebar?.id || !main || !dialog || !('showModal' in dialog) || dialog.open || dialog.children.length) return;
|
|
21
|
+
const triggers = [...main.querySelectorAll('button[data-sidebar-toggle]')].filter(b => b.getAttribute('aria-controls') === sidebar.id);
|
|
22
|
+
const closers = [...sidebar.querySelectorAll('button[data-sidebar-close]')];
|
|
23
|
+
if (!triggers.length || !closers.length) return;
|
|
24
|
+
const originalExpanded = triggers.map(b => b.getAttribute('aria-expanded'));
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const { signal } = controller;
|
|
27
|
+
let mobile = false;
|
|
28
|
+
let rail = false;
|
|
29
|
+
let canRail = false;
|
|
30
|
+
let opener = triggers[0];
|
|
31
|
+
const focusable = 'a[href],button:not(:disabled),summary';
|
|
32
|
+
const closePopovers = () => sidebar.querySelectorAll('[popover]:popover-open').forEach(p => p.hidePopover());
|
|
33
|
+
const update = () => {
|
|
34
|
+
this.toggleAttribute('data-m-sidebar-mobile', mobile);
|
|
35
|
+
this.toggleAttribute('data-m-sidebar-rail', rail && canRail && !mobile);
|
|
36
|
+
this.toggleAttribute('data-m-sidebar-can-rail', canRail && !mobile);
|
|
37
|
+
for (const button of triggers) button.setAttribute('aria-expanded', String(mobile ? dialog.open : !(rail && canRail)));
|
|
38
|
+
};
|
|
39
|
+
const setRail = value => {
|
|
40
|
+
closePopovers();
|
|
41
|
+
const active = document.activeElement;
|
|
42
|
+
rail = value && canRail;
|
|
43
|
+
update();
|
|
44
|
+
if (sidebar.contains(active) && active instanceof HTMLElement && !active.getClientRects().length) triggers[0].focus();
|
|
45
|
+
};
|
|
46
|
+
const open = () => {
|
|
47
|
+
if (mobile) {
|
|
48
|
+
if (!dialog.open) dialog.showModal();
|
|
49
|
+
update();
|
|
50
|
+
} else setRail(false);
|
|
51
|
+
};
|
|
52
|
+
const close = () => {
|
|
53
|
+
if (mobile) {
|
|
54
|
+
closePopovers();
|
|
55
|
+
if (dialog.open) dialog.close();
|
|
56
|
+
update();
|
|
57
|
+
} else setRail(true);
|
|
58
|
+
};
|
|
59
|
+
const toggle = () => mobile ? (dialog.open ? close() : open()) : setRail(!rail);
|
|
60
|
+
const measure = () => {
|
|
61
|
+
const active = document.activeElement;
|
|
62
|
+
// Icon collapse is explicitly requested and requires named, authored
|
|
63
|
+
// icons. Invalid icon anatomy falls back to the expanded desktop panel.
|
|
64
|
+
const items = [...sidebar.querySelectorAll('[data-sidebar-item]')].filter(e => !e.closest('[data-sidebar-subnav], [popover]'));
|
|
65
|
+
canRail = this.getAttribute('collapse') === 'icon' && items.length > 0 && items.every(e => e.querySelector('[data-sidebar-icon]') && (e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby')));
|
|
66
|
+
const nextMobile = this.getBoundingClientRect().width < 44 * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
67
|
+
if (nextMobile !== mobile || sidebar.parentElement !== (nextMobile ? dialog : this)) {
|
|
68
|
+
const hadSidebarFocus = sidebar.contains(active);
|
|
69
|
+
closePopovers();
|
|
70
|
+
if (dialog.open) dialog.close();
|
|
71
|
+
mobile = nextMobile;
|
|
72
|
+
if (mobile) dialog.append(sidebar);
|
|
73
|
+
else this.insertBefore(sidebar, main);
|
|
74
|
+
update();
|
|
75
|
+
if (hadSidebarFocus && active instanceof HTMLElement) {
|
|
76
|
+
if (mobile) opener.focus();
|
|
77
|
+
else {
|
|
78
|
+
// Preserve a focused nested link when returning to desktop.
|
|
79
|
+
if (active.closest('[data-sidebar-subnav]')) rail = false;
|
|
80
|
+
update();
|
|
81
|
+
if (active.matches('[data-sidebar-close]')) sidebar.querySelector(focusable)?.focus();
|
|
82
|
+
else active.focus();
|
|
83
|
+
}
|
|
84
|
+
} else if (!mobile && triggers.includes(active) && !canRail) sidebar.querySelector(focusable)?.focus();
|
|
85
|
+
} else update();
|
|
86
|
+
};
|
|
87
|
+
for (const button of triggers) button.addEventListener('click', () => { opener = button; toggle(); }, { signal });
|
|
88
|
+
for (const button of closers) button.addEventListener('click', close, { signal });
|
|
89
|
+
dialog.addEventListener('close', () => {
|
|
90
|
+
update();
|
|
91
|
+
// WebKit can leave focus on body after closing a modal that contained
|
|
92
|
+
// a popover. Preserve router-assigned focus elsewhere in the page.
|
|
93
|
+
if (mobile && (document.activeElement === document.body || dialog.contains(document.activeElement))) opener.focus();
|
|
94
|
+
}, { signal });
|
|
95
|
+
dialog.addEventListener('click', event => {
|
|
96
|
+
if (event.target !== dialog) return;
|
|
97
|
+
const r = dialog.getBoundingClientRect();
|
|
98
|
+
if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) close();
|
|
99
|
+
}, { signal });
|
|
100
|
+
sidebar.addEventListener('click', event => {
|
|
101
|
+
const summary = event.target.closest('summary');
|
|
102
|
+
if (summary && rail && !mobile) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
setRail(false);
|
|
105
|
+
summary.parentElement.open = true;
|
|
106
|
+
}
|
|
107
|
+
const link = event.target.closest('a[href]');
|
|
108
|
+
if (link && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) queueMicrotask(() => { if (!event.defaultPrevented && mobile) close(); });
|
|
109
|
+
}, { signal });
|
|
110
|
+
const observer = new ResizeObserver(measure);
|
|
111
|
+
observer.observe(this);
|
|
112
|
+
window.addEventListener('resize', measure, { signal, passive: true });
|
|
113
|
+
this.#actions = { open, close, toggle };
|
|
114
|
+
this.#refresh = measure;
|
|
115
|
+
this.#cleanup = () => {
|
|
116
|
+
controller.abort();
|
|
117
|
+
observer.disconnect();
|
|
118
|
+
closePopovers();
|
|
119
|
+
if (dialog.open) dialog.close();
|
|
120
|
+
this.insertBefore(sidebar, main);
|
|
121
|
+
for (const name of ['ready', 'mobile', 'rail', 'can-rail']) this.removeAttribute(`data-m-sidebar-${name}`);
|
|
122
|
+
triggers.forEach((b, i) => originalExpanded[i] === null ? b.removeAttribute('aria-expanded') : b.setAttribute('aria-expanded', originalExpanded[i]));
|
|
123
|
+
};
|
|
124
|
+
this.setAttribute('data-m-sidebar-ready', '');
|
|
125
|
+
measure();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
disconnectedCallback() {
|
|
130
|
+
this.#cleanup?.();
|
|
131
|
+
this.#cleanup = this.#actions = this.#refresh = undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!customElements.get('m-sidebar-layout')) customElements.define('m-sidebar-layout', MSidebarLayout);
|
package/table.d.ts
ADDED
package/table.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Optional overflow indicators. Authored rows, selection, sorting, and paging
|
|
2
|
+
* remain application-owned. The native scroll region works without this module. */
|
|
3
|
+
class MicaTableScroll extends HTMLElement {
|
|
4
|
+
connectedCallback() {
|
|
5
|
+
this._abort?.abort();
|
|
6
|
+
this._resize?.disconnect();
|
|
7
|
+
this._mutation?.disconnect();
|
|
8
|
+
this._abort = new AbortController();
|
|
9
|
+
this._update = () => {
|
|
10
|
+
cancelAnimationFrame(this._frame);
|
|
11
|
+
this._frame = requestAnimationFrame(() => this._measure());
|
|
12
|
+
};
|
|
13
|
+
this.addEventListener('scroll', this._update, { capture: true, passive: true, signal: this._abort.signal });
|
|
14
|
+
window.addEventListener('resize', this._update, { signal: this._abort.signal });
|
|
15
|
+
this._resize = new ResizeObserver(this._update);
|
|
16
|
+
this._observe = () => {
|
|
17
|
+
this._resize.disconnect();
|
|
18
|
+
this._resize.observe(this);
|
|
19
|
+
const region = this.querySelector(':scope > [data-table-region]');
|
|
20
|
+
if (region) this._resize.observe(region);
|
|
21
|
+
const table = region?.querySelector('table');
|
|
22
|
+
if (table) this._resize.observe(table);
|
|
23
|
+
this._update();
|
|
24
|
+
};
|
|
25
|
+
this._mutation = new MutationObserver(this._observe);
|
|
26
|
+
this._mutation.observe(this, { subtree: true, childList: true, attributes: true, attributeFilter: ['hidden', 'data-state', 'dir'] });
|
|
27
|
+
this._observe();
|
|
28
|
+
}
|
|
29
|
+
_measure() {
|
|
30
|
+
const region = this.querySelector(':scope > [data-table-region]');
|
|
31
|
+
const table = region?.querySelector('table');
|
|
32
|
+
if (!region || !table) return;
|
|
33
|
+
const r = region.getBoundingClientRect(), t = table.getBoundingClientRect();
|
|
34
|
+
const overflow = !region.hasAttribute('data-state') && region.scrollWidth > region.clientWidth + 1;
|
|
35
|
+
this.toggleAttribute('data-m-overflow-left', overflow && t.left < r.left - 1);
|
|
36
|
+
this.toggleAttribute('data-m-overflow-right', overflow && t.right > r.right + 1);
|
|
37
|
+
let left = 0, right = 0;
|
|
38
|
+
const rtl = getComputedStyle(region).direction === 'rtl';
|
|
39
|
+
for (const cell of table.querySelectorAll('thead [data-table-sticky]')) {
|
|
40
|
+
if (getComputedStyle(cell).position !== 'sticky' || !cell.getClientRects().length) continue;
|
|
41
|
+
const c = cell.getBoundingClientRect();
|
|
42
|
+
if (rtl) right = Math.max(right, r.right - c.left);
|
|
43
|
+
else left = Math.max(left, c.right - r.left);
|
|
44
|
+
}
|
|
45
|
+
this.style.setProperty('--m-table-edge-left', `${left}px`);
|
|
46
|
+
this.style.setProperty('--m-table-edge-right', `${right}px`);
|
|
47
|
+
}
|
|
48
|
+
disconnectedCallback() {
|
|
49
|
+
this._abort?.abort();
|
|
50
|
+
this._resize?.disconnect();
|
|
51
|
+
this._mutation?.disconnect();
|
|
52
|
+
cancelAnimationFrame(this._frame);
|
|
53
|
+
this.removeAttribute('data-m-overflow-left');
|
|
54
|
+
this.removeAttribute('data-m-overflow-right');
|
|
55
|
+
this.style.removeProperty('--m-table-edge-left');
|
|
56
|
+
this.style.removeProperty('--m-table-edge-right');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!customElements.get('m-table-scroll')) customElements.define('m-table-scroll', MicaTableScroll);
|
package/tabs.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Type declarations for mica/tabs.js — side-effect module.
|
|
2
|
-
* Registers <m-tabs>: roving tabindex, arrow keys, aria-selected wiring
|
|
2
|
+
* Registers <m-tabs>: roving tabindex, arrow keys, aria-selected wiring,
|
|
3
|
+
* selected-tab reveal, and directional overflow markers.
|
|
3
4
|
*/
|
|
4
5
|
declare global {
|
|
5
6
|
interface HTMLElementTagNameMap {
|
package/tabs.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Enhances working light-DOM markup; never renders it. Without this
|
|
4
4
|
* module, the nav buttons are inert and every panel is visible in
|
|
5
5
|
* order — complete, readable content. With it: real tablist semantics,
|
|
6
|
-
* roving tabindex, arrow-key navigation, automatic activation
|
|
6
|
+
* roving tabindex, arrow-key navigation, automatic activation, and
|
|
7
|
+
* selected-tab reveal with position-aware overflow markers.
|
|
7
8
|
*
|
|
8
9
|
* <m-tabs>
|
|
9
10
|
* <nav>
|
|
@@ -23,10 +24,43 @@ let uid = 0;
|
|
|
23
24
|
class MTabs extends HTMLElement {
|
|
24
25
|
#tabs = [];
|
|
25
26
|
#panels = [];
|
|
27
|
+
#nav = null;
|
|
28
|
+
#overflowing = false;
|
|
29
|
+
#resizeObserver = null;
|
|
30
|
+
#revealFrame = null;
|
|
31
|
+
#onScroll = () => this.#syncOverflow();
|
|
32
|
+
#cancelReveal = () => {
|
|
33
|
+
if (this.#revealFrame !== null) cancelAnimationFrame(this.#revealFrame);
|
|
34
|
+
this.#revealFrame = null;
|
|
35
|
+
};
|
|
36
|
+
#onClick = (event) => {
|
|
37
|
+
const tab = event.target instanceof Element ? event.target.closest("button") : null;
|
|
38
|
+
const index = this.#tabs.indexOf(tab);
|
|
39
|
+
if (index === -1) return;
|
|
40
|
+
this.select(index);
|
|
41
|
+
this.#reveal(index);
|
|
42
|
+
};
|
|
43
|
+
#onKeyDown = (event) => {
|
|
44
|
+
const n = this.#tabs.length;
|
|
45
|
+
const current = this.#tabs.findIndex(
|
|
46
|
+
(tab) => tab.getAttribute("aria-selected") === "true",
|
|
47
|
+
);
|
|
48
|
+
let next = null;
|
|
49
|
+
if (event.key === "ArrowRight") next = (current + 1) % n;
|
|
50
|
+
else if (event.key === "ArrowLeft") next = (current - 1 + n) % n;
|
|
51
|
+
else if (event.key === "Home") next = 0;
|
|
52
|
+
else if (event.key === "End") next = n - 1;
|
|
53
|
+
if (next === null) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
this.select(next); // automatic activation, radix-style
|
|
56
|
+
this.#tabs[next].focus({ preventScroll: true });
|
|
57
|
+
this.#reveal(next);
|
|
58
|
+
};
|
|
26
59
|
|
|
27
60
|
connectedCallback() {
|
|
28
61
|
const nav = this.querySelector(":scope > nav");
|
|
29
62
|
if (!nav) return;
|
|
63
|
+
this.#nav = nav;
|
|
30
64
|
this.#tabs = [...nav.querySelectorAll("button")];
|
|
31
65
|
const sections = [...this.querySelectorAll(":scope > section")];
|
|
32
66
|
this.#panels = this.#tabs.map((b, i) => {
|
|
@@ -44,29 +78,117 @@ class MTabs extends HTMLElement {
|
|
|
44
78
|
panel.setAttribute("aria-labelledby", tab.id);
|
|
45
79
|
panel.tabIndex = 0;
|
|
46
80
|
}
|
|
47
|
-
tab.addEventListener("click", () => this.select(i));
|
|
48
81
|
});
|
|
49
82
|
|
|
50
|
-
nav.addEventListener("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
let next = null;
|
|
56
|
-
if (e.key === "ArrowRight") next = (current + 1) % n;
|
|
57
|
-
else if (e.key === "ArrowLeft") next = (current - 1 + n) % n;
|
|
58
|
-
else if (e.key === "Home") next = 0;
|
|
59
|
-
else if (e.key === "End") next = n - 1;
|
|
60
|
-
if (next === null) return;
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
this.select(next); // automatic activation, radix-style
|
|
63
|
-
this.#tabs[next].focus();
|
|
64
|
-
});
|
|
83
|
+
nav.addEventListener("click", this.#onClick);
|
|
84
|
+
nav.addEventListener("keydown", this.#onKeyDown);
|
|
85
|
+
nav.addEventListener("pointerdown", this.#cancelReveal, { passive: true });
|
|
86
|
+
nav.addEventListener("wheel", this.#cancelReveal, { passive: true });
|
|
65
87
|
|
|
66
88
|
const initial = this.#tabs.findIndex(
|
|
67
89
|
(t) => t.hasAttribute("selected") || t.getAttribute("aria-selected") === "true",
|
|
68
90
|
);
|
|
69
91
|
this.select(initial === -1 ? 0 : initial);
|
|
92
|
+
|
|
93
|
+
nav.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
94
|
+
this.#resizeObserver = new ResizeObserver(() => this.#measureOverflow());
|
|
95
|
+
this.#resizeObserver.observe(nav);
|
|
96
|
+
this.#tabs.forEach((tab) => this.#resizeObserver.observe(tab));
|
|
97
|
+
this.#measureOverflow();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
disconnectedCallback() {
|
|
101
|
+
this.#nav?.removeEventListener("click", this.#onClick);
|
|
102
|
+
this.#nav?.removeEventListener("keydown", this.#onKeyDown);
|
|
103
|
+
this.#nav?.removeEventListener("pointerdown", this.#cancelReveal);
|
|
104
|
+
this.#nav?.removeEventListener("wheel", this.#cancelReveal);
|
|
105
|
+
this.#nav?.removeEventListener("scroll", this.#onScroll);
|
|
106
|
+
this.#resizeObserver?.disconnect();
|
|
107
|
+
this.#cancelReveal();
|
|
108
|
+
for (const property of [
|
|
109
|
+
"--m-tabs-overflow-start-gutter",
|
|
110
|
+
"--m-tabs-overflow-end-gutter",
|
|
111
|
+
"--m-tabs-overflow-start",
|
|
112
|
+
"--m-tabs-overflow-end",
|
|
113
|
+
]) this.#nav?.style.removeProperty(property);
|
|
114
|
+
this.#nav = null;
|
|
115
|
+
this.#resizeObserver = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#measureOverflow() {
|
|
119
|
+
const nav = this.#nav;
|
|
120
|
+
if (!nav) return;
|
|
121
|
+
const style = getComputedStyle(nav);
|
|
122
|
+
const gap = Number.parseFloat(style.columnGap) || 0;
|
|
123
|
+
const padding = (Number.parseFloat(style.paddingInlineStart) || 0) +
|
|
124
|
+
(Number.parseFloat(style.paddingInlineEnd) || 0);
|
|
125
|
+
const tabsWidth = this.#tabs.reduce(
|
|
126
|
+
(width, tab) => width + tab.getBoundingClientRect().width,
|
|
127
|
+
padding + gap * Math.max(0, this.#tabs.length - 1),
|
|
128
|
+
);
|
|
129
|
+
this.#overflowing = tabsWidth > nav.clientWidth + 1;
|
|
130
|
+
this.#syncOverflow();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#syncOverflow() {
|
|
134
|
+
const nav = this.#nav;
|
|
135
|
+
if (!nav) return;
|
|
136
|
+
const offset = Math.abs(nav.scrollLeft);
|
|
137
|
+
const max = nav.scrollWidth - nav.clientWidth;
|
|
138
|
+
const start = this.#overflowing && offset > 1;
|
|
139
|
+
const end = this.#overflowing && offset < max - 1;
|
|
140
|
+
nav.style.setProperty("--m-tabs-overflow-start", String(start ? 1 : 0));
|
|
141
|
+
nav.style.setProperty("--m-tabs-overflow-end", String(end ? 1 : 0));
|
|
142
|
+
nav.style.setProperty(
|
|
143
|
+
"--m-tabs-overflow-start-gutter",
|
|
144
|
+
start ? "var(--space-md)" : "0px",
|
|
145
|
+
);
|
|
146
|
+
nav.style.setProperty(
|
|
147
|
+
"--m-tabs-overflow-end-gutter",
|
|
148
|
+
end ? "var(--space-md)" : "0px",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#reveal(index) {
|
|
153
|
+
const nav = this.#nav;
|
|
154
|
+
if (!nav || !this.#tabs[index]) return;
|
|
155
|
+
this.#cancelReveal();
|
|
156
|
+
const durations = getComputedStyle(nav, "::before").transitionDuration.split(",");
|
|
157
|
+
const duration = durations.reduce((longest, value) => {
|
|
158
|
+
const time = Number.parseFloat(value) * (value.trim().endsWith("ms") ? 1 : 1000);
|
|
159
|
+
return Math.max(longest, time);
|
|
160
|
+
}, 0);
|
|
161
|
+
// Follow the moving gutters through their transition, plus two frames
|
|
162
|
+
// for the scroll event that activates the newly exposed marker.
|
|
163
|
+
const settleAt = performance.now() + duration + 34;
|
|
164
|
+
let firstFrame = true;
|
|
165
|
+
const reveal = (now) => {
|
|
166
|
+
const nav = this.#nav;
|
|
167
|
+
const tab = this.#tabs[index];
|
|
168
|
+
if (!nav || !tab) return;
|
|
169
|
+
const navRect = nav.getBoundingClientRect();
|
|
170
|
+
const tabRect = tab.getBoundingClientRect();
|
|
171
|
+
const beforeStyle = getComputedStyle(nav, "::before");
|
|
172
|
+
const afterStyle = getComputedStyle(nav, "::after");
|
|
173
|
+
const before = Math.max(0,
|
|
174
|
+
(Number.parseFloat(beforeStyle.inlineSize) || 0) +
|
|
175
|
+
(Number.parseFloat(beforeStyle.marginInlineEnd) || 0));
|
|
176
|
+
const after = Math.max(0,
|
|
177
|
+
(Number.parseFloat(afterStyle.inlineSize) || 0) +
|
|
178
|
+
(Number.parseFloat(afterStyle.marginInlineStart) || 0));
|
|
179
|
+
const rtl = getComputedStyle(nav).direction === "rtl";
|
|
180
|
+
const left = navRect.left + (rtl ? after : before);
|
|
181
|
+
const right = navRect.right - (rtl ? before : after);
|
|
182
|
+
if (tabRect.left < left) nav.scrollBy({ left: tabRect.left - left });
|
|
183
|
+
else if (tabRect.right > right) nav.scrollBy({ left: tabRect.right - right });
|
|
184
|
+
if (firstFrame) {
|
|
185
|
+
this.#syncOverflow();
|
|
186
|
+
firstFrame = false;
|
|
187
|
+
}
|
|
188
|
+
if (now < settleAt) this.#revealFrame = requestAnimationFrame(reveal);
|
|
189
|
+
else this.#revealFrame = null;
|
|
190
|
+
};
|
|
191
|
+
this.#revealFrame = requestAnimationFrame(reveal);
|
|
70
192
|
}
|
|
71
193
|
|
|
72
194
|
select(index) {
|
package/types/react.d.ts
CHANGED
|
@@ -51,7 +51,13 @@ declare module 'react' {
|
|
|
51
51
|
|
|
52
52
|
// components
|
|
53
53
|
'm-segmented': MicaElement
|
|
54
|
-
'm-
|
|
54
|
+
'm-sidebar': MicaElement
|
|
55
|
+
'm-sidebar-layout': MicaElement<{ variant?: 'docked' | 'inset'; collapse?: 'icon' }>
|
|
56
|
+
'm-table-scroll': MicaElement
|
|
57
|
+
'm-breadcrumb-overflow': MicaElement
|
|
58
|
+
'm-header': MicaElement<{ align?: 'start' | 'end' }>
|
|
59
|
+
'm-stepper': MicaElement
|
|
60
|
+
'm-tabs': MicaElement<{ variant?: 'underline' }>
|
|
55
61
|
'm-badge': MicaElement<{
|
|
56
62
|
variant?: 'primary' | 'success' | 'warning' | 'danger'
|
|
57
63
|
count?: boolean
|
|
@@ -60,6 +66,8 @@ declare module 'react' {
|
|
|
60
66
|
blobatar?: string
|
|
61
67
|
contained?: boolean
|
|
62
68
|
}>
|
|
69
|
+
'm-callout': MicaElement<{ variant?: 'neutral' | 'success' | 'warning' | 'danger' }>
|
|
70
|
+
'm-skeleton': MicaElement
|
|
63
71
|
'm-toast': MicaElement<{
|
|
64
72
|
popover?: 'manual'
|
|
65
73
|
variant?: 'success' | 'warning' | 'danger'
|