@extrachill/components 0.4.23 → 0.4.24
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/CHANGELOG.md +35 -0
- package/dist/ResponsiveTabs.d.ts +3 -1
- package/dist/ResponsiveTabs.d.ts.map +1 -1
- package/dist/ResponsiveTabs.js +37 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/initResponsiveTabsDom.d.ts +8 -0
- package/dist/initResponsiveTabsDom.d.ts.map +1 -0
- package/dist/initResponsiveTabsDom.js +139 -0
- package/package.json +1 -1
- package/src/ResponsiveTabs.tsx +58 -10
- package/src/index.tsx +1 -0
- package/src/initResponsiveTabsDom.ts +184 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.23] - 2026-03-25
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Refine responsive accordion spacing and interaction styling
|
|
7
|
+
|
|
8
|
+
## [0.4.22] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Preserve local accordion collapse state on mobile
|
|
12
|
+
|
|
13
|
+
## [0.4.21] - 2026-03-25
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Allow responsive accordion items to collapse on second tap
|
|
17
|
+
|
|
18
|
+
## [0.4.20] - 2026-03-25
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Keep responsive tabs root structural only
|
|
22
|
+
|
|
23
|
+
## [0.4.19] - 2026-03-25
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Limit responsive tabs accordion mode to phone breakpoint
|
|
27
|
+
|
|
28
|
+
## [0.4.18] - 2026-03-25
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Remove mobile color overrides from shared surfaces
|
|
32
|
+
|
|
33
|
+
## [0.4.17] - 2026-03-25
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Fix responsive tabs mobile interaction and shared hover styling
|
|
37
|
+
|
|
3
38
|
## [0.4.16] - 2026-03-25
|
|
4
39
|
|
|
5
40
|
### Changed
|
package/dist/ResponsiveTabs.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ResponsiveTabsProps {
|
|
|
12
12
|
mobileBreakpoint?: number;
|
|
13
13
|
accordionClassName?: string;
|
|
14
14
|
showDesktopTabs?: boolean;
|
|
15
|
+
syncWithHash?: boolean;
|
|
16
|
+
hashPrefix?: string;
|
|
15
17
|
}
|
|
16
|
-
export declare function ResponsiveTabs({ tabs, active, onChange, renderPanel, className, classPrefix, tabsClassName, tabsClassPrefix, mobileBreakpoint, accordionClassName, showDesktopTabs, }: ResponsiveTabsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
18
|
+
export declare function ResponsiveTabs({ tabs, active, onChange, renderPanel, className, classPrefix, tabsClassName, tabsClassPrefix, mobileBreakpoint, accordionClassName, showDesktopTabs, syncWithHash, hashPrefix, }: ResponsiveTabsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
17
19
|
//# sourceMappingURL=ResponsiveTabs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResponsiveTabs.d.ts","sourceRoot":"","sources":["../src/ResponsiveTabs.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAE,EAAE,EAAE,MAAM,KAAM,IAAI,CAAC;IACjC,WAAW,EAAE,CAAE,EAAE,EAAE,MAAM,KAAM,SAAS,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ResponsiveTabs.d.ts","sourceRoot":"","sources":["../src/ResponsiveTabs.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAE,EAAE,EAAE,MAAM,KAAM,IAAI,CAAC;IACjC,WAAW,EAAE,CAAE,EAAE,EAAE,MAAM,KAAM,SAAS,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAE,EAC/B,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,WAAW,EACX,SAAc,EACd,WAAkC,EAClC,aAAkB,EAClB,eAA2B,EAC3B,gBAAsB,EACtB,kBAAuB,EACvB,eAAsB,EACtB,YAAoB,EACpB,UAAmB,GACnB,EAAE,mBAAmB,kDA6IrB"}
|
package/dist/ResponsiveTabs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { Tabs } from "./Tabs.js";
|
|
4
|
-
export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className = '', classPrefix = 'ec-responsive-tabs', tabsClassName = '', tabsClassPrefix = 'ec-tabs', mobileBreakpoint = 480, accordionClassName = '', showDesktopTabs = true, }) {
|
|
4
|
+
export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className = '', classPrefix = 'ec-responsive-tabs', tabsClassName = '', tabsClassPrefix = 'ec-tabs', mobileBreakpoint = 480, accordionClassName = '', showDesktopTabs = true, syncWithHash = false, hashPrefix = 'tab-', }) {
|
|
5
5
|
const [isMobile, setIsMobile] = useState(() => {
|
|
6
6
|
if (typeof window === 'undefined') {
|
|
7
7
|
return false;
|
|
@@ -9,6 +9,24 @@ export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className
|
|
|
9
9
|
return window.innerWidth < mobileBreakpoint;
|
|
10
10
|
});
|
|
11
11
|
const [mobileActive, setMobileActive] = useState(active);
|
|
12
|
+
const resolveTabIdFromHash = (hash) => {
|
|
13
|
+
const normalized = hash.replace(/^#/, '');
|
|
14
|
+
if (!normalized.startsWith(hashPrefix)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const id = normalized.slice(hashPrefix.length);
|
|
18
|
+
return tabs.some((tab) => tab.id === id) ? id : null;
|
|
19
|
+
};
|
|
20
|
+
const updateHash = (id) => {
|
|
21
|
+
if (!syncWithHash || typeof window === 'undefined') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
window.location.hash = `${hashPrefix}${id}`;
|
|
25
|
+
};
|
|
26
|
+
const handleChange = (id) => {
|
|
27
|
+
onChange(id);
|
|
28
|
+
updateHash(id);
|
|
29
|
+
};
|
|
12
30
|
useEffect(() => {
|
|
13
31
|
if (typeof window === 'undefined') {
|
|
14
32
|
return undefined;
|
|
@@ -25,6 +43,22 @@ export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className
|
|
|
25
43
|
setMobileActive(active);
|
|
26
44
|
}
|
|
27
45
|
}, [active, isMobile]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!syncWithHash || typeof window === 'undefined' || tabs.length === 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const syncFromHash = () => {
|
|
51
|
+
const id = resolveTabIdFromHash(window.location.hash);
|
|
52
|
+
if (!id) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
onChange(id);
|
|
56
|
+
setMobileActive(id);
|
|
57
|
+
};
|
|
58
|
+
syncFromHash();
|
|
59
|
+
window.addEventListener('hashchange', syncFromHash);
|
|
60
|
+
return () => window.removeEventListener('hashchange', syncFromHash);
|
|
61
|
+
}, [hashPrefix, onChange, syncWithHash, tabs]);
|
|
28
62
|
const rootClass = useMemo(() => [
|
|
29
63
|
classPrefix,
|
|
30
64
|
isMobile ? `${classPrefix}--mobile` : `${classPrefix}--desktop`,
|
|
@@ -36,7 +70,7 @@ export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className
|
|
|
36
70
|
return null;
|
|
37
71
|
}
|
|
38
72
|
if (!isMobile) {
|
|
39
|
-
return (_jsxs("div", { className: rootClass, children: [showDesktopTabs && (_jsx(Tabs, { tabs: tabs, active: active, onChange:
|
|
73
|
+
return (_jsxs("div", { className: rootClass, children: [showDesktopTabs && (_jsx(Tabs, { tabs: tabs, active: active, onChange: handleChange, className: tabsClassName, classPrefix: tabsClassPrefix })), _jsx("div", { className: `${classPrefix}__desktop-panel`, children: renderPanel(active) })] }));
|
|
40
74
|
}
|
|
41
75
|
return (_jsx("div", { className: rootClass, children: _jsx("div", { className: [`${classPrefix}__accordion`, accordionClassName].filter(Boolean).join(' '), children: tabs.map((tab) => {
|
|
42
76
|
const isActive = tab.id === mobileActive;
|
|
@@ -46,7 +80,7 @@ export function ResponsiveTabs({ tabs, active, onChange, renderPanel, className
|
|
|
46
80
|
return;
|
|
47
81
|
}
|
|
48
82
|
setMobileActive(tab.id);
|
|
49
|
-
|
|
83
|
+
handleChange(tab.id);
|
|
50
84
|
}, children: [_jsx("span", { children: tab.label }), _jsx("span", { className: `${classPrefix}__arrow`, "aria-hidden": "true", children: isActive ? '▲' : '▼' })] }), isActive && _jsx("div", { className: `${classPrefix}__panel`, children: renderPanel(tab.id) })] }, tab.id));
|
|
51
85
|
}) }) }));
|
|
52
86
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { SearchBox, type SearchBoxProps } from './SearchBox.tsx';
|
|
|
9
9
|
export { Modal, type ModalProps } from './Modal.tsx';
|
|
10
10
|
export { Tabs, type TabsProps, type TabItem } from './Tabs.tsx';
|
|
11
11
|
export { ResponsiveTabs, type ResponsiveTabsProps } from './ResponsiveTabs.tsx';
|
|
12
|
+
export { initResponsiveTabsDom, type ResponsiveTabsDomOptions } from './initResponsiveTabsDom.ts';
|
|
12
13
|
export { ShellTabs, type ShellTabsProps } from './ShellTabs.tsx';
|
|
13
14
|
export { Panel, type PanelProps } from './Panel.tsx';
|
|
14
15
|
export { Surface, type SurfaceProps } from './Surface.tsx';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,KAAK,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { SearchBox } from "./SearchBox.js";
|
|
|
9
9
|
export { Modal } from "./Modal.js";
|
|
10
10
|
export { Tabs } from "./Tabs.js";
|
|
11
11
|
export { ResponsiveTabs } from "./ResponsiveTabs.js";
|
|
12
|
+
export { initResponsiveTabsDom } from "./initResponsiveTabsDom.js";
|
|
12
13
|
export { ShellTabs } from "./ShellTabs.js";
|
|
13
14
|
export { Panel } from "./Panel.js";
|
|
14
15
|
export { Surface } from "./Surface.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ResponsiveTabsDomOptions {
|
|
2
|
+
selector?: string;
|
|
3
|
+
mobileBreakpoint?: number;
|
|
4
|
+
hashPrefix?: string;
|
|
5
|
+
onPanelRender?: (panel: HTMLElement, tabId: string, root: HTMLElement) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function initResponsiveTabsDom(options?: ResponsiveTabsDomOptions): void;
|
|
8
|
+
//# sourceMappingURL=initResponsiveTabsDom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initResponsiveTabsDom.d.ts","sourceRoot":"","sources":["../src/initResponsiveTabsDom.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,CAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAM,IAAI,CAAC;CACjF;AA2KD,wBAAgB,qBAAqB,CAAE,OAAO,GAAE,wBAA6B,QAO5E"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_SELECTOR = '[data-ec-responsive-tabs]';
|
|
2
|
+
function createNodeFromTemplate(template) {
|
|
3
|
+
const fragment = template.content.cloneNode(true);
|
|
4
|
+
const wrapper = document.createElement('div');
|
|
5
|
+
wrapper.appendChild(fragment);
|
|
6
|
+
return wrapper.firstElementChild instanceof HTMLElement ? wrapper.firstElementChild : wrapper;
|
|
7
|
+
}
|
|
8
|
+
function renderDesktopPanel(root, tabId, onPanelRender) {
|
|
9
|
+
const desktopPanel = root.querySelector('.ec-responsive-tabs__desktop-panel');
|
|
10
|
+
const template = root.querySelector(`template[data-tab-panel="${tabId}"]`);
|
|
11
|
+
if (!desktopPanel || !template) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
desktopPanel.innerHTML = '';
|
|
15
|
+
const content = createNodeFromTemplate(template);
|
|
16
|
+
if (!content) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
desktopPanel.appendChild(content);
|
|
20
|
+
onPanelRender?.(desktopPanel, tabId, root);
|
|
21
|
+
}
|
|
22
|
+
function renderAccordion(root, activeTabId, hashPrefix, onPanelRender) {
|
|
23
|
+
const accordion = root.querySelector('.ec-responsive-tabs__accordion');
|
|
24
|
+
const tabs = Array.from(root.querySelectorAll('.ec-tabs__tab[data-tab-id]'));
|
|
25
|
+
if (!accordion) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
accordion.innerHTML = '';
|
|
29
|
+
tabs.forEach((tab) => {
|
|
30
|
+
const tabId = tab.dataset.tabId;
|
|
31
|
+
if (!tabId) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const label = tab.textContent?.trim() || tabId;
|
|
35
|
+
const item = document.createElement('div');
|
|
36
|
+
item.className = `ec-responsive-tabs__item${tabId === activeTabId ? ' is-active' : ''}`;
|
|
37
|
+
const trigger = document.createElement('button');
|
|
38
|
+
trigger.type = 'button';
|
|
39
|
+
trigger.className = `ec-responsive-tabs__trigger${tabId === activeTabId ? ' is-active' : ''}`;
|
|
40
|
+
trigger.setAttribute('aria-expanded', tabId === activeTabId ? 'true' : 'false');
|
|
41
|
+
trigger.dataset.tabId = tabId;
|
|
42
|
+
trigger.innerHTML = `<span>${label}</span><span class="ec-responsive-tabs__arrow" aria-hidden="true">${tabId === activeTabId ? '▲' : '▼'}</span>`;
|
|
43
|
+
item.appendChild(trigger);
|
|
44
|
+
if (tabId === activeTabId) {
|
|
45
|
+
const template = root.querySelector(`template[data-tab-panel="${tabId}"]`);
|
|
46
|
+
if (template) {
|
|
47
|
+
const panel = document.createElement('div');
|
|
48
|
+
panel.className = 'ec-responsive-tabs__panel';
|
|
49
|
+
const content = createNodeFromTemplate(template);
|
|
50
|
+
if (content) {
|
|
51
|
+
panel.appendChild(content);
|
|
52
|
+
item.appendChild(panel);
|
|
53
|
+
onPanelRender?.(panel, tabId, root);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
trigger.addEventListener('click', () => {
|
|
58
|
+
const isActive = root.dataset.activeTab === tabId;
|
|
59
|
+
if (isActive) {
|
|
60
|
+
root.dataset.activeTab = '';
|
|
61
|
+
window.location.hash = '';
|
|
62
|
+
renderAccordion(root, null, hashPrefix, onPanelRender);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
root.dataset.activeTab = tabId;
|
|
66
|
+
window.location.hash = `${hashPrefix}${tabId}`;
|
|
67
|
+
renderDesktopPanel(root, tabId, onPanelRender);
|
|
68
|
+
renderAccordion(root, tabId, hashPrefix, onPanelRender);
|
|
69
|
+
setActiveDesktopTab(root, tabId);
|
|
70
|
+
});
|
|
71
|
+
accordion.appendChild(item);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function setActiveDesktopTab(root, activeTabId) {
|
|
75
|
+
root.querySelectorAll('.ec-tabs__tab[data-tab-id]').forEach((tab) => {
|
|
76
|
+
tab.classList.toggle('is-active', tab.dataset.tabId === activeTabId);
|
|
77
|
+
if (tab.dataset.tabId === activeTabId) {
|
|
78
|
+
tab.setAttribute('aria-selected', 'true');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
tab.setAttribute('aria-selected', 'false');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function resolveHashTabId(root, hashPrefix) {
|
|
86
|
+
const normalized = window.location.hash.replace(/^#/, '');
|
|
87
|
+
if (!normalized.startsWith(hashPrefix)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const id = normalized.slice(hashPrefix.length);
|
|
91
|
+
return root.querySelector(`.ec-tabs__tab[data-tab-id="${id}"]`) ? id : null;
|
|
92
|
+
}
|
|
93
|
+
function initResponsiveTabsRoot(root, options) {
|
|
94
|
+
if (root.dataset.ecResponsiveTabsInitialized === '1') {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
root.dataset.ecResponsiveTabsInitialized = '1';
|
|
98
|
+
const hashPrefix = root.dataset.hashPrefix || options.hashPrefix || 'tab-';
|
|
99
|
+
const tabs = Array.from(root.querySelectorAll('.ec-tabs__tab[data-tab-id]'));
|
|
100
|
+
const firstTabId = tabs[0]?.dataset.tabId || '';
|
|
101
|
+
const initialTabId = resolveHashTabId(root, hashPrefix) || root.dataset.activeTab || firstTabId;
|
|
102
|
+
if (!initialTabId) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
root.dataset.activeTab = initialTabId;
|
|
106
|
+
setActiveDesktopTab(root, initialTabId);
|
|
107
|
+
renderDesktopPanel(root, initialTabId, options.onPanelRender);
|
|
108
|
+
renderAccordion(root, initialTabId, hashPrefix, options.onPanelRender);
|
|
109
|
+
tabs.forEach((tab) => {
|
|
110
|
+
tab.addEventListener('click', () => {
|
|
111
|
+
const tabId = tab.dataset.tabId;
|
|
112
|
+
if (!tabId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
root.dataset.activeTab = tabId;
|
|
116
|
+
window.location.hash = `${hashPrefix}${tabId}`;
|
|
117
|
+
setActiveDesktopTab(root, tabId);
|
|
118
|
+
renderDesktopPanel(root, tabId, options.onPanelRender);
|
|
119
|
+
renderAccordion(root, tabId, hashPrefix, options.onPanelRender);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
window.addEventListener('hashchange', () => {
|
|
123
|
+
const tabId = resolveHashTabId(root, hashPrefix);
|
|
124
|
+
if (!tabId) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
root.dataset.activeTab = tabId;
|
|
128
|
+
setActiveDesktopTab(root, tabId);
|
|
129
|
+
renderDesktopPanel(root, tabId, options.onPanelRender);
|
|
130
|
+
renderAccordion(root, tabId, hashPrefix, options.onPanelRender);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function initResponsiveTabsDom(options = {}) {
|
|
134
|
+
if (typeof document === 'undefined') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const selector = options.selector || DEFAULT_SELECTOR;
|
|
138
|
+
document.querySelectorAll(selector).forEach((root) => initResponsiveTabsRoot(root, options));
|
|
139
|
+
}
|
package/package.json
CHANGED
package/src/ResponsiveTabs.tsx
CHANGED
|
@@ -14,6 +14,8 @@ export interface ResponsiveTabsProps {
|
|
|
14
14
|
mobileBreakpoint?: number;
|
|
15
15
|
accordionClassName?: string;
|
|
16
16
|
showDesktopTabs?: boolean;
|
|
17
|
+
syncWithHash?: boolean;
|
|
18
|
+
hashPrefix?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function ResponsiveTabs( {
|
|
@@ -28,6 +30,8 @@ export function ResponsiveTabs( {
|
|
|
28
30
|
mobileBreakpoint = 480,
|
|
29
31
|
accordionClassName = '',
|
|
30
32
|
showDesktopTabs = true,
|
|
33
|
+
syncWithHash = false,
|
|
34
|
+
hashPrefix = 'tab-',
|
|
31
35
|
}: ResponsiveTabsProps ) {
|
|
32
36
|
const [ isMobile, setIsMobile ] = useState( () => {
|
|
33
37
|
if ( typeof window === 'undefined' ) {
|
|
@@ -38,6 +42,29 @@ export function ResponsiveTabs( {
|
|
|
38
42
|
} );
|
|
39
43
|
const [ mobileActive, setMobileActive ] = useState<string | null>( active );
|
|
40
44
|
|
|
45
|
+
const resolveTabIdFromHash = ( hash: string ) => {
|
|
46
|
+
const normalized = hash.replace( /^#/, '' );
|
|
47
|
+
if ( ! normalized.startsWith( hashPrefix ) ) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const id = normalized.slice( hashPrefix.length );
|
|
52
|
+
return tabs.some( ( tab ) => tab.id === id ) ? id : null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const updateHash = ( id: string ) => {
|
|
56
|
+
if ( ! syncWithHash || typeof window === 'undefined' ) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
window.location.hash = `${ hashPrefix }${ id }`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleChange = ( id: string ) => {
|
|
64
|
+
onChange( id );
|
|
65
|
+
updateHash( id );
|
|
66
|
+
};
|
|
67
|
+
|
|
41
68
|
useEffect( () => {
|
|
42
69
|
if ( typeof window === 'undefined' ) {
|
|
43
70
|
return undefined;
|
|
@@ -59,6 +86,27 @@ export function ResponsiveTabs( {
|
|
|
59
86
|
}
|
|
60
87
|
}, [ active, isMobile ] );
|
|
61
88
|
|
|
89
|
+
useEffect( () => {
|
|
90
|
+
if ( ! syncWithHash || typeof window === 'undefined' || tabs.length === 0 ) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const syncFromHash = () => {
|
|
95
|
+
const id = resolveTabIdFromHash( window.location.hash );
|
|
96
|
+
if ( ! id ) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onChange( id );
|
|
101
|
+
setMobileActive( id );
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
syncFromHash();
|
|
105
|
+
window.addEventListener( 'hashchange', syncFromHash );
|
|
106
|
+
|
|
107
|
+
return () => window.removeEventListener( 'hashchange', syncFromHash );
|
|
108
|
+
}, [ hashPrefix, onChange, syncWithHash, tabs ] );
|
|
109
|
+
|
|
62
110
|
const rootClass = useMemo(
|
|
63
111
|
() => [
|
|
64
112
|
classPrefix,
|
|
@@ -81,7 +129,7 @@ export function ResponsiveTabs( {
|
|
|
81
129
|
<Tabs
|
|
82
130
|
tabs={ tabs }
|
|
83
131
|
active={ active }
|
|
84
|
-
onChange={
|
|
132
|
+
onChange={ handleChange }
|
|
85
133
|
className={ tabsClassName }
|
|
86
134
|
classPrefix={ tabsClassPrefix }
|
|
87
135
|
/>
|
|
@@ -104,15 +152,15 @@ export function ResponsiveTabs( {
|
|
|
104
152
|
className={ `${ classPrefix }__trigger${ isActive ? ' is-active' : '' }` }
|
|
105
153
|
aria-expanded={ isActive }
|
|
106
154
|
onClick={ () => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
155
|
+
if ( isActive ) {
|
|
156
|
+
setMobileActive( null );
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setMobileActive( tab.id );
|
|
161
|
+
handleChange( tab.id );
|
|
162
|
+
} }
|
|
163
|
+
>
|
|
116
164
|
<span>{ tab.label }</span>
|
|
117
165
|
<span className={ `${ classPrefix }__arrow` } aria-hidden="true">
|
|
118
166
|
{ isActive ? '▲' : '▼' }
|
package/src/index.tsx
CHANGED
|
@@ -10,6 +10,7 @@ export { SearchBox, type SearchBoxProps } from './SearchBox.tsx';
|
|
|
10
10
|
export { Modal, type ModalProps } from './Modal.tsx';
|
|
11
11
|
export { Tabs, type TabsProps, type TabItem } from './Tabs.tsx';
|
|
12
12
|
export { ResponsiveTabs, type ResponsiveTabsProps } from './ResponsiveTabs.tsx';
|
|
13
|
+
export { initResponsiveTabsDom, type ResponsiveTabsDomOptions } from './initResponsiveTabsDom.ts';
|
|
13
14
|
export { ShellTabs, type ShellTabsProps } from './ShellTabs.tsx';
|
|
14
15
|
export { Panel, type PanelProps } from './Panel.tsx';
|
|
15
16
|
export { Surface, type SurfaceProps } from './Surface.tsx';
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export interface ResponsiveTabsDomOptions {
|
|
2
|
+
selector?: string;
|
|
3
|
+
mobileBreakpoint?: number;
|
|
4
|
+
hashPrefix?: string;
|
|
5
|
+
onPanelRender?: ( panel: HTMLElement, tabId: string, root: HTMLElement ) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_SELECTOR = '[data-ec-responsive-tabs]';
|
|
9
|
+
|
|
10
|
+
function createNodeFromTemplate( template: HTMLTemplateElement ): HTMLElement | null {
|
|
11
|
+
const fragment = template.content.cloneNode( true );
|
|
12
|
+
const wrapper = document.createElement( 'div' );
|
|
13
|
+
wrapper.appendChild( fragment );
|
|
14
|
+
|
|
15
|
+
return wrapper.firstElementChild instanceof HTMLElement ? wrapper.firstElementChild : wrapper;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderDesktopPanel(
|
|
19
|
+
root: HTMLElement,
|
|
20
|
+
tabId: string,
|
|
21
|
+
onPanelRender?: ResponsiveTabsDomOptions['onPanelRender']
|
|
22
|
+
) {
|
|
23
|
+
const desktopPanel = root.querySelector<HTMLElement>( '.ec-responsive-tabs__desktop-panel' );
|
|
24
|
+
const template = root.querySelector<HTMLTemplateElement>( `template[data-tab-panel="${ tabId }"]` );
|
|
25
|
+
|
|
26
|
+
if ( ! desktopPanel || ! template ) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
desktopPanel.innerHTML = '';
|
|
31
|
+
const content = createNodeFromTemplate( template );
|
|
32
|
+
if ( ! content ) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
desktopPanel.appendChild( content );
|
|
37
|
+
onPanelRender?.( desktopPanel, tabId, root );
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderAccordion(
|
|
41
|
+
root: HTMLElement,
|
|
42
|
+
activeTabId: string | null,
|
|
43
|
+
hashPrefix: string,
|
|
44
|
+
onPanelRender?: ResponsiveTabsDomOptions['onPanelRender']
|
|
45
|
+
) {
|
|
46
|
+
const accordion = root.querySelector<HTMLElement>( '.ec-responsive-tabs__accordion' );
|
|
47
|
+
const tabs = Array.from( root.querySelectorAll<HTMLElement>( '.ec-tabs__tab[data-tab-id]' ) );
|
|
48
|
+
|
|
49
|
+
if ( ! accordion ) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
accordion.innerHTML = '';
|
|
54
|
+
|
|
55
|
+
tabs.forEach( ( tab ) => {
|
|
56
|
+
const tabId = tab.dataset.tabId;
|
|
57
|
+
if ( ! tabId ) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const label = tab.textContent?.trim() || tabId;
|
|
62
|
+
const item = document.createElement( 'div' );
|
|
63
|
+
item.className = `ec-responsive-tabs__item${ tabId === activeTabId ? ' is-active' : '' }`;
|
|
64
|
+
|
|
65
|
+
const trigger = document.createElement( 'button' );
|
|
66
|
+
trigger.type = 'button';
|
|
67
|
+
trigger.className = `ec-responsive-tabs__trigger${ tabId === activeTabId ? ' is-active' : '' }`;
|
|
68
|
+
trigger.setAttribute( 'aria-expanded', tabId === activeTabId ? 'true' : 'false' );
|
|
69
|
+
trigger.dataset.tabId = tabId;
|
|
70
|
+
trigger.innerHTML = `<span>${ label }</span><span class="ec-responsive-tabs__arrow" aria-hidden="true">${ tabId === activeTabId ? '▲' : '▼' }</span>`;
|
|
71
|
+
item.appendChild( trigger );
|
|
72
|
+
|
|
73
|
+
if ( tabId === activeTabId ) {
|
|
74
|
+
const template = root.querySelector<HTMLTemplateElement>( `template[data-tab-panel="${ tabId }"]` );
|
|
75
|
+
if ( template ) {
|
|
76
|
+
const panel = document.createElement( 'div' );
|
|
77
|
+
panel.className = 'ec-responsive-tabs__panel';
|
|
78
|
+
const content = createNodeFromTemplate( template );
|
|
79
|
+
if ( content ) {
|
|
80
|
+
panel.appendChild( content );
|
|
81
|
+
item.appendChild( panel );
|
|
82
|
+
onPanelRender?.( panel, tabId, root );
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
trigger.addEventListener( 'click', () => {
|
|
88
|
+
const isActive = root.dataset.activeTab === tabId;
|
|
89
|
+
if ( isActive ) {
|
|
90
|
+
root.dataset.activeTab = '';
|
|
91
|
+
window.location.hash = '';
|
|
92
|
+
renderAccordion( root, null, hashPrefix, onPanelRender );
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
root.dataset.activeTab = tabId;
|
|
97
|
+
window.location.hash = `${ hashPrefix }${ tabId }`;
|
|
98
|
+
renderDesktopPanel( root, tabId, onPanelRender );
|
|
99
|
+
renderAccordion( root, tabId, hashPrefix, onPanelRender );
|
|
100
|
+
setActiveDesktopTab( root, tabId );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
accordion.appendChild( item );
|
|
104
|
+
} );
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setActiveDesktopTab( root: HTMLElement, activeTabId: string ) {
|
|
108
|
+
root.querySelectorAll<HTMLElement>( '.ec-tabs__tab[data-tab-id]' ).forEach( ( tab ) => {
|
|
109
|
+
tab.classList.toggle( 'is-active', tab.dataset.tabId === activeTabId );
|
|
110
|
+
if ( tab.dataset.tabId === activeTabId ) {
|
|
111
|
+
tab.setAttribute( 'aria-selected', 'true' );
|
|
112
|
+
} else {
|
|
113
|
+
tab.setAttribute( 'aria-selected', 'false' );
|
|
114
|
+
}
|
|
115
|
+
} );
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveHashTabId( root: HTMLElement, hashPrefix: string ) {
|
|
119
|
+
const normalized = window.location.hash.replace( /^#/, '' );
|
|
120
|
+
if ( ! normalized.startsWith( hashPrefix ) ) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const id = normalized.slice( hashPrefix.length );
|
|
125
|
+
return root.querySelector<HTMLElement>( `.ec-tabs__tab[data-tab-id="${ id }"]` ) ? id : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function initResponsiveTabsRoot( root: HTMLElement, options: ResponsiveTabsDomOptions ) {
|
|
129
|
+
if ( root.dataset.ecResponsiveTabsInitialized === '1' ) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
root.dataset.ecResponsiveTabsInitialized = '1';
|
|
134
|
+
|
|
135
|
+
const hashPrefix = root.dataset.hashPrefix || options.hashPrefix || 'tab-';
|
|
136
|
+
const tabs = Array.from( root.querySelectorAll<HTMLElement>( '.ec-tabs__tab[data-tab-id]' ) );
|
|
137
|
+
const firstTabId = tabs[ 0 ]?.dataset.tabId || '';
|
|
138
|
+
const initialTabId = resolveHashTabId( root, hashPrefix ) || root.dataset.activeTab || firstTabId;
|
|
139
|
+
|
|
140
|
+
if ( ! initialTabId ) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
root.dataset.activeTab = initialTabId;
|
|
145
|
+
setActiveDesktopTab( root, initialTabId );
|
|
146
|
+
renderDesktopPanel( root, initialTabId, options.onPanelRender );
|
|
147
|
+
renderAccordion( root, initialTabId, hashPrefix, options.onPanelRender );
|
|
148
|
+
|
|
149
|
+
tabs.forEach( ( tab ) => {
|
|
150
|
+
tab.addEventListener( 'click', () => {
|
|
151
|
+
const tabId = tab.dataset.tabId;
|
|
152
|
+
if ( ! tabId ) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
root.dataset.activeTab = tabId;
|
|
157
|
+
window.location.hash = `${ hashPrefix }${ tabId }`;
|
|
158
|
+
setActiveDesktopTab( root, tabId );
|
|
159
|
+
renderDesktopPanel( root, tabId, options.onPanelRender );
|
|
160
|
+
renderAccordion( root, tabId, hashPrefix, options.onPanelRender );
|
|
161
|
+
} );
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
window.addEventListener( 'hashchange', () => {
|
|
165
|
+
const tabId = resolveHashTabId( root, hashPrefix );
|
|
166
|
+
if ( ! tabId ) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
root.dataset.activeTab = tabId;
|
|
171
|
+
setActiveDesktopTab( root, tabId );
|
|
172
|
+
renderDesktopPanel( root, tabId, options.onPanelRender );
|
|
173
|
+
renderAccordion( root, tabId, hashPrefix, options.onPanelRender );
|
|
174
|
+
} );
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function initResponsiveTabsDom( options: ResponsiveTabsDomOptions = {} ) {
|
|
178
|
+
if ( typeof document === 'undefined' ) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const selector = options.selector || DEFAULT_SELECTOR;
|
|
183
|
+
document.querySelectorAll<HTMLElement>( selector ).forEach( ( root ) => initResponsiveTabsRoot( root, options ) );
|
|
184
|
+
}
|