panelset 0.5.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/dist/index.d.ts +111 -0
- package/dist/panelset.css +1 -0
- package/dist/panelset.js +317 -0
- package/dist/panelset.js.map +1 -0
- package/package.json +49 -0
- package/src/docs/assets/scripts/copybutton.js +44 -0
- package/src/docs/assets/scripts/example-async.js +161 -0
- package/src/docs/assets/scripts/example-closable.js +27 -0
- package/src/docs/assets/scripts/example-megamenu.js +84 -0
- package/src/docs/assets/scripts/example.js +29 -0
- package/src/docs/assets/scripts/main.js +7 -0
- package/src/docs/assets/styles/_base.scss +13 -0
- package/src/docs/assets/styles/_code.scss +121 -0
- package/src/docs/assets/styles/_demos.scss +180 -0
- package/src/docs/assets/styles/_landingpage.scss +41 -0
- package/src/docs/assets/styles/_layout.scss +80 -0
- package/src/docs/assets/styles/_sidebar.scss +67 -0
- package/src/docs/assets/styles/_typography.scss +116 -0
- package/src/docs/assets/styles/_variables.scss +32 -0
- package/src/docs/assets/styles/docs.scss +64 -0
- package/src/docs/views/api-reference.pug +474 -0
- package/src/docs/views/configuration.pug +173 -0
- package/src/docs/views/events.pug +222 -0
- package/src/docs/views/examples/async.pug +268 -0
- package/src/docs/views/examples/basic.pug +155 -0
- package/src/docs/views/examples/closable.pug +97 -0
- package/src/docs/views/getting-started.pug +99 -0
- package/src/docs/views/index.pug +38 -0
- package/src/docs/views/templates/includes/_head.pug +11 -0
- package/src/docs/views/templates/includes/_mixins.pug +100 -0
- package/src/docs/views/templates/includes/_scripts.pug +14 -0
- package/src/docs/views/templates/includes/_sidebar.pug +18 -0
- package/src/docs/views/templates/layouts/_base.pug +36 -0
- package/src/docs/views/transitions.pug +141 -0
- package/src/lib/index.ts +685 -0
- package/src/lib/styles/_base.scss +99 -0
- package/src/lib/styles/_loading.scss +47 -0
- package/src/lib/styles/_variables.scss +19 -0
- package/src/lib/styles/panelset.scss +3 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
extends /docs/views/templates/layouts/_base
|
|
2
|
+
|
|
3
|
+
append variables
|
|
4
|
+
- pagetitle = "Events"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
block content
|
|
8
|
+
.lane
|
|
9
|
+
.container
|
|
10
|
+
.page-header
|
|
11
|
+
h1 Events
|
|
12
|
+
p.lead PanelSet dispatches custom events for activation lifecycle tracking
|
|
13
|
+
p All events are dispatched from the panelset container element and bubble up the DOM.
|
|
14
|
+
|
|
15
|
+
.lane
|
|
16
|
+
.container
|
|
17
|
+
h2 Available events and possible uses
|
|
18
|
+
|
|
19
|
+
.example.api
|
|
20
|
+
h3.code.method-signature Event: 'ps:ready'
|
|
21
|
+
p Fires when the PanelSet is fully initialized
|
|
22
|
+
h4 Detail:
|
|
23
|
+
dl
|
|
24
|
+
dt.code container: HTMLElement
|
|
25
|
+
dd The container element the PanelSet was initialized on
|
|
26
|
+
|
|
27
|
+
dt.code instance: PanelSet
|
|
28
|
+
dd The PanelSet instance that was initialized
|
|
29
|
+
br
|
|
30
|
+
+codeblock('JavaScript').
|
|
31
|
+
document.addEventListener('ps:ready', (e) => {
|
|
32
|
+
const container = e.detail.container; // or e.target
|
|
33
|
+
const instance = e.detail.instance;
|
|
34
|
+
|
|
35
|
+
// Track which panelset was initialized
|
|
36
|
+
analytics.track('PanelSet Initialized', {
|
|
37
|
+
id: container.id,
|
|
38
|
+
panelCount: instance.panels.length
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//-
|
|
42
|
+
|
|
43
|
+
.example.api.white
|
|
44
|
+
h3.code.method-signature Event: 'ps:beforeactivate'
|
|
45
|
+
p Fires before activation begins, can be used for async content loading
|
|
46
|
+
|
|
47
|
+
h4 Detail:
|
|
48
|
+
dl
|
|
49
|
+
dt.code panelId: string
|
|
50
|
+
dd ID of the panel being activated
|
|
51
|
+
|
|
52
|
+
dt.code targetPanel: HTMLElement
|
|
53
|
+
dd The panel element being activated
|
|
54
|
+
|
|
55
|
+
dt.code outgoingPanel: HTMLElement | null
|
|
56
|
+
dd The panel being deactivated
|
|
57
|
+
|
|
58
|
+
dt.code signal: AbortSignal
|
|
59
|
+
dd Signal to abort async operations if user clicks away
|
|
60
|
+
|
|
61
|
+
dt.code promise: Promise | null
|
|
62
|
+
dd Set this to a promise to delay activation until it resolves
|
|
63
|
+
|
|
64
|
+
br
|
|
65
|
+
+codeblock('JavaScript').
|
|
66
|
+
container.addEventListener('ps:beforeactivate', (e) => {
|
|
67
|
+
const { targetPanel, signal } = e.detail;
|
|
68
|
+
|
|
69
|
+
// Skip if already loaded
|
|
70
|
+
if (targetPanel.dataset.loaded === 'true') return;
|
|
71
|
+
|
|
72
|
+
// Set promise to load content
|
|
73
|
+
e.detail.promise = fetch(`/api/panel/${targetPanel.id}`, { signal })
|
|
74
|
+
.then(response => response.text())
|
|
75
|
+
.then(html => {
|
|
76
|
+
targetPanel.innerHTML = html;
|
|
77
|
+
targetPanel.dataset.loaded = 'true';
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//-
|
|
81
|
+
br
|
|
82
|
+
p Or use the convenience method:
|
|
83
|
+
+codeblock('JavaScript').
|
|
84
|
+
panelSet.onBeforeActivate((targetPanel, signal) => {
|
|
85
|
+
return fetch(`/api/panel/${targetPanel.id}`, { signal })
|
|
86
|
+
.then(response => response.text())
|
|
87
|
+
.then(html => targetPanel.innerHTML = html);
|
|
88
|
+
}, { once: true }); // Auto-caching
|
|
89
|
+
//-
|
|
90
|
+
|
|
91
|
+
.example.api.white
|
|
92
|
+
h3.code.method-signature Event: 'ps:activationstart'
|
|
93
|
+
p Fires before any transitions begin
|
|
94
|
+
h4 Detail:
|
|
95
|
+
dl
|
|
96
|
+
dt.code panelId: string
|
|
97
|
+
dd ID of the panel being activated
|
|
98
|
+
|
|
99
|
+
dt.code trigger: HTMLElement | null
|
|
100
|
+
dd The button/element that triggered the activation (if provided)
|
|
101
|
+
br
|
|
102
|
+
+codeblock('JavaScript').
|
|
103
|
+
container.addEventListener('ps:activationstart', (e) => {
|
|
104
|
+
console.log('Starting activation:', e.detail.panelId);
|
|
105
|
+
console.log('Triggered by:', e.detail.trigger);
|
|
106
|
+
|
|
107
|
+
// Update UI, analytics, etc.
|
|
108
|
+
if (e.detail.trigger) {
|
|
109
|
+
e.detail.trigger.setAttribute('aria-busy', 'true');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
//-
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
.example.api.white
|
|
116
|
+
h3.code.method-signature Event: 'ps:activationcomplete'
|
|
117
|
+
p Fires after height and panel transitions complete
|
|
118
|
+
h4 Detail:
|
|
119
|
+
dl
|
|
120
|
+
dt.code panelId: string
|
|
121
|
+
dd ID of the panel being activated
|
|
122
|
+
|
|
123
|
+
dt.code trigger: HTMLElement | null
|
|
124
|
+
dd The button/element that triggered the activation (if provided)
|
|
125
|
+
br
|
|
126
|
+
+codeblock('JavaScript').
|
|
127
|
+
container.addEventListener('ps:activationcomplete', (e) => {
|
|
128
|
+
console.log('Activation complete:', e.detail.panelId);
|
|
129
|
+
|
|
130
|
+
// Focus management, scroll, analytics
|
|
131
|
+
const panel = document.getElementById(e.detail.panelId);
|
|
132
|
+
const firstInput = panel.querySelector('input, button');
|
|
133
|
+
firstInput?.focus();
|
|
134
|
+
});
|
|
135
|
+
//-
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
.example.api.white
|
|
139
|
+
h3.code.method-signature Event: 'ps:activationaborted'
|
|
140
|
+
p Fires if an activation is aborted due to a new activation request
|
|
141
|
+
h4 Detail:
|
|
142
|
+
dl
|
|
143
|
+
dt.code panelId: string
|
|
144
|
+
dd ID of the panel of which the activation was aborted
|
|
145
|
+
|
|
146
|
+
dt.code trigger: HTMLElement | null
|
|
147
|
+
dd The button/element that triggered the event
|
|
148
|
+
br
|
|
149
|
+
+codeblock('JavaScript').
|
|
150
|
+
container.addEventListener('ps:activationaborted', (e) => {
|
|
151
|
+
console.log('Activation aborted:', e.detail.panelId);
|
|
152
|
+
|
|
153
|
+
// Cleanup, remove loading states
|
|
154
|
+
if (e.detail.trigger) {
|
|
155
|
+
e.detail.trigger.setAttribute('aria-busy', 'false');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
//-
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
append scripts
|
|
162
|
+
if isProd
|
|
163
|
+
script(type="module", vite-ignore).
|
|
164
|
+
import { PanelSet } from '/lib/panelset.js';
|
|
165
|
+
PanelSet.init();
|
|
166
|
+
else
|
|
167
|
+
script(type="module").
|
|
168
|
+
import { PanelSet } from '/src/lib/index.js';
|
|
169
|
+
import '/src/lib/styles/panelset.scss';
|
|
170
|
+
PanelSet.init({debug: true});
|
|
171
|
+
|
|
172
|
+
script.
|
|
173
|
+
let transitionsEnabled = true;
|
|
174
|
+
|
|
175
|
+
let closablePanelSet = null;
|
|
176
|
+
document.addEventListener('panelset:ready', (e) => {
|
|
177
|
+
const instance = e.detail.instance;
|
|
178
|
+
const container = e.target;
|
|
179
|
+
if (container.id == 'config-closable-demo') {
|
|
180
|
+
closablePanelSet = instance;
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
document.addEventListener('click', (e) => {
|
|
185
|
+
const button = e.target.closest('button');
|
|
186
|
+
if (!button) return;
|
|
187
|
+
|
|
188
|
+
if (button.dataset.panel) {
|
|
189
|
+
const panelId = button.getAttribute('data-panel');
|
|
190
|
+
const panel = document.getElementById(panelId);
|
|
191
|
+
const container = panel?.closest('[data-panelset]');
|
|
192
|
+
|
|
193
|
+
if (container?.panelSet) {
|
|
194
|
+
container.panelSet.setActive(panelId, true, { trigger: button });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (button.id == "toggle-transitions") {
|
|
199
|
+
const transDemo = document.getElementById('config-transitions-demo');
|
|
200
|
+
if (transDemo?.panelSet.config.transitions == false) {
|
|
201
|
+
transDemo.panelSet.config.transitions = true;
|
|
202
|
+
button.textContent = "Disable Transitions";
|
|
203
|
+
} else {
|
|
204
|
+
transDemo.panelSet.config.transitions = false;
|
|
205
|
+
button.textContent = "Enable Transitions";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
// Log events
|
|
213
|
+
document.addEventListener('activationstart', (e) => {
|
|
214
|
+
console.log('[Demo PanelSet log] Activationstart:', e.detail);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
document.addEventListener('activationcomplete', (e) => {
|
|
218
|
+
console.log('[Demo PanelsSet log] Activationcomplete:', e.detail);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
extends /docs/views/templates/layouts/_base
|
|
2
|
+
|
|
3
|
+
append variables
|
|
4
|
+
- pagetitle = "Async Loading"
|
|
5
|
+
|
|
6
|
+
block content
|
|
7
|
+
.lane
|
|
8
|
+
.container
|
|
9
|
+
.page-header
|
|
10
|
+
h1 Async Loading
|
|
11
|
+
p.lead Load panel content on-demand with promises, fetch, or heavy computation
|
|
12
|
+
|
|
13
|
+
.lane
|
|
14
|
+
.container
|
|
15
|
+
.title-text
|
|
16
|
+
h2 Overview
|
|
17
|
+
p PanelSet supports async content loading through the #[code ps:beforeactivate] event. This allows you to fetch data from APIs, process large datasets, or perform any async operation before a panel becomes visible.
|
|
18
|
+
|
|
19
|
+
h3 Key Features
|
|
20
|
+
ul
|
|
21
|
+
li Load content only when needed (lazy loading)
|
|
22
|
+
li Automatic loading spinner with configurable delay
|
|
23
|
+
li Built-in abort support for cancelled activations
|
|
24
|
+
li Auto-caching with the #[code once] option
|
|
25
|
+
li Smooth height transitions after content loads
|
|
26
|
+
|
|
27
|
+
.lane
|
|
28
|
+
.container
|
|
29
|
+
h2 The onBeforeActivate() Method
|
|
30
|
+
|
|
31
|
+
.title-text
|
|
32
|
+
p The easiest way to add async loading is with the #[code onBeforeActivate()] convenience method:
|
|
33
|
+
|
|
34
|
+
+codeblock('JS').
|
|
35
|
+
const panelSet = new PanelSet('#my-panelset');
|
|
36
|
+
|
|
37
|
+
panelSet.onBeforeActivate((targetPanel, signal) => {
|
|
38
|
+
// Return a promise to delay activation
|
|
39
|
+
return fetch(`/api/content/${targetPanel.id}`, { signal })
|
|
40
|
+
.then(response => response.text())
|
|
41
|
+
.then(html => {
|
|
42
|
+
targetPanel.innerHTML = html;
|
|
43
|
+
});
|
|
44
|
+
}, { once: true }); // Load once, cache forever
|
|
45
|
+
|
|
46
|
+
.example.api
|
|
47
|
+
h3 Parameters
|
|
48
|
+
dl
|
|
49
|
+
dt targetPanel
|
|
50
|
+
dd The HTMLElement being activated
|
|
51
|
+
|
|
52
|
+
dt signal
|
|
53
|
+
dd AbortSignal for canceling the operation if user clicks away
|
|
54
|
+
|
|
55
|
+
dt options.once
|
|
56
|
+
dd If #[code true], content loads only once and is cached. Set to #[code false] to always reload. Default: #[code false]
|
|
57
|
+
|
|
58
|
+
.lane
|
|
59
|
+
.container
|
|
60
|
+
h2 Live Example: Fetch from API
|
|
61
|
+
p This example uses a 'slowfetch' to mimic a slow network request to fetch recipe data. The renderRecipe function just formats the data into HTML.
|
|
62
|
+
|
|
63
|
+
.example
|
|
64
|
+
.demo
|
|
65
|
+
.demo-controls
|
|
66
|
+
button(data-panel="async-panel-1") Panel 1 (Static)
|
|
67
|
+
button(data-panel="async-panel-2") Panel 2 (Fetch Recipe)
|
|
68
|
+
button(data-panel="async-panel-3") Panel 3 (Static)
|
|
69
|
+
|
|
70
|
+
#async-demo(data-panelset data-empty-panel-height="300" data-loading-delay="200")
|
|
71
|
+
.panel-wrapper
|
|
72
|
+
#async-panel-1(role="tabpanel" class="active" data-loaded="true")
|
|
73
|
+
h3 Static Content
|
|
74
|
+
p This panel has pre-loaded content.
|
|
75
|
+
|
|
76
|
+
#async-panel-2(role="tabpanel" hidden)
|
|
77
|
+
// Empty - will load async
|
|
78
|
+
|
|
79
|
+
#async-panel-3(role="tabpanel" hidden)
|
|
80
|
+
h3 Another Static Panel
|
|
81
|
+
p More pre-loaded content.
|
|
82
|
+
|
|
83
|
+
+codeblock('JS').
|
|
84
|
+
asyncPanelSet.onBeforeActivate((targetPanel, signal) => {
|
|
85
|
+
if (targetPanel.id === 'async-panel-2') {
|
|
86
|
+
return slowfetch(`https://dummyjson.com/recipes/${randomIntFromInterval(1, 10)}`, { signal })
|
|
87
|
+
.then(response => response.json())
|
|
88
|
+
.then(data => {
|
|
89
|
+
targetPanel.innerHTML = renderRecipe(data);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
.lane
|
|
95
|
+
.container
|
|
96
|
+
h2 Example: Heavy Computation
|
|
97
|
+
|
|
98
|
+
.title-text
|
|
99
|
+
p You can also use async loading for CPU-intensive operations:
|
|
100
|
+
|
|
101
|
+
+codeblock('JS').
|
|
102
|
+
panelSet.onBeforeActivate((targetPanel, signal) => {
|
|
103
|
+
if (targetPanel.id === 'heavy-calc') {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
// Simulate heavy calculation
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
const result = fibonacci(40); // Actually slow!
|
|
108
|
+
targetPanel.innerHTML = `
|
|
109
|
+
<h3>Calculation Complete</h3>
|
|
110
|
+
<p>Fibonacci(40) = ${result}</p>
|
|
111
|
+
`;
|
|
112
|
+
resolve();
|
|
113
|
+
}, 2000);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function fibonacci(n) {
|
|
119
|
+
if (n <= 1) return n;
|
|
120
|
+
return fibonacci(n - 1) + fibonacci(n - 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.lane
|
|
124
|
+
.container
|
|
125
|
+
.title-text
|
|
126
|
+
h2 Using the Event Directly
|
|
127
|
+
p For more control, listen to the #[code ps:beforeactivate] event directly:
|
|
128
|
+
|
|
129
|
+
+codeblock('JS').
|
|
130
|
+
container.addEventListener('ps:beforeactivate', (e) => {
|
|
131
|
+
const { panelId, targetPanel, signal } = e.detail;
|
|
132
|
+
|
|
133
|
+
// Skip if already loaded
|
|
134
|
+
if (targetPanel.dataset.loaded === 'true') return;
|
|
135
|
+
|
|
136
|
+
// Set the promise property to delay activation
|
|
137
|
+
e.detail.promise = fetch(`/api/panel/${panelId}`, { signal })
|
|
138
|
+
.then(response => response.text())
|
|
139
|
+
.then(html => {
|
|
140
|
+
targetPanel.innerHTML = html;
|
|
141
|
+
targetPanel.dataset.loaded = 'true';
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
.example.api
|
|
146
|
+
h3 Event Detail Properties
|
|
147
|
+
dl
|
|
148
|
+
dt panelId
|
|
149
|
+
dd String - ID of the panel being activated
|
|
150
|
+
|
|
151
|
+
dt targetPanel
|
|
152
|
+
dd HTMLElement - The panel element
|
|
153
|
+
|
|
154
|
+
dt outgoingPanel
|
|
155
|
+
dd HTMLElement | null - The panel being deactivated
|
|
156
|
+
|
|
157
|
+
dt signal
|
|
158
|
+
dd AbortSignal - Use with fetch to cancel if user clicks away
|
|
159
|
+
|
|
160
|
+
dt promise
|
|
161
|
+
dd Promise | null - Set this to delay activation until it resolves
|
|
162
|
+
|
|
163
|
+
.lane
|
|
164
|
+
.container
|
|
165
|
+
h2 Loading States
|
|
166
|
+
|
|
167
|
+
.title-text
|
|
168
|
+
h3 Loading Spinner
|
|
169
|
+
p During async operations, PanelSet shows a loading spinner. Configure the delay to prevent flashing on fast connections:
|
|
170
|
+
|
|
171
|
+
+codeblock('JS').
|
|
172
|
+
const panelSet = new PanelSet('#my-panelset', {
|
|
173
|
+
loadingDelay: 300 // Wait 300ms before showing spinner
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
.title-text
|
|
177
|
+
p Or via data attribute:
|
|
178
|
+
|
|
179
|
+
+codeblock('HTML').
|
|
180
|
+
<div data-panelset data-loading-delay="300">
|
|
181
|
+
|
|
182
|
+
.title-text
|
|
183
|
+
h3 Empty Panel Height
|
|
184
|
+
p Set a minimum height during loading (shows spinner in that space):
|
|
185
|
+
|
|
186
|
+
+codeblock('JS').
|
|
187
|
+
const panelSet = new PanelSet('#my-panelset', {
|
|
188
|
+
emptyPanelHeight: 200 // 200px minimum during load
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
.lane
|
|
192
|
+
.container
|
|
193
|
+
h2 Abort Handling
|
|
194
|
+
|
|
195
|
+
.title-text
|
|
196
|
+
p When users click away during loading, the operation is automatically aborted via the AbortSignal:
|
|
197
|
+
|
|
198
|
+
+codeblock('JS').
|
|
199
|
+
panelSet.onBeforeActivate((targetPanel, signal) => {
|
|
200
|
+
// Pass signal to fetch - automatically cancels on abort
|
|
201
|
+
return fetch('/api/data', { signal })
|
|
202
|
+
.then(/* ... */);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
.title-text
|
|
206
|
+
p The #[code ps:activationaborted] event fires when an async load is cancelled:
|
|
207
|
+
|
|
208
|
+
+codeblock('JS').
|
|
209
|
+
container.addEventListener('ps:activationaborted', (e) => {
|
|
210
|
+
console.log('Load cancelled:', e.detail.panelId);
|
|
211
|
+
// Clean up, show message, etc.
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
.lane
|
|
215
|
+
.container
|
|
216
|
+
h2 Complete Example
|
|
217
|
+
|
|
218
|
+
+codeblock('JS').
|
|
219
|
+
import { PanelSet } from 'panelset';
|
|
220
|
+
import 'panelset/dist/panelset.css';
|
|
221
|
+
|
|
222
|
+
const panelSet = new PanelSet('#my-panelset', {
|
|
223
|
+
emptyPanelHeight: 300,
|
|
224
|
+
loadingDelay: 200
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Async loading with error handling
|
|
228
|
+
panelSet.onBeforeActivate((targetPanel, signal) => {
|
|
229
|
+
if (targetPanel.id === 'async-content') {
|
|
230
|
+
return fetch('/api/content', { signal })
|
|
231
|
+
.then(response => {
|
|
232
|
+
if (!response.ok) throw new Error('Load failed');
|
|
233
|
+
return response.json();
|
|
234
|
+
})
|
|
235
|
+
.then(data => {
|
|
236
|
+
targetPanel.innerHTML = renderContent(data);
|
|
237
|
+
})
|
|
238
|
+
.catch(error => {
|
|
239
|
+
if (error.name === 'AbortError') return; // User clicked away
|
|
240
|
+
targetPanel.innerHTML = `<p class="error">Failed to load content</p>`;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}, { once: true });
|
|
244
|
+
|
|
245
|
+
// Event listeners
|
|
246
|
+
panelSet.element.addEventListener('ps:activationaborted', (e) => {
|
|
247
|
+
console.log('Cancelled:', e.detail.panelId);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
panelSet.element.addEventListener('ps:activationcomplete', (e) => {
|
|
251
|
+
console.log('Complete:', e.detail.panelId);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
append scripts
|
|
255
|
+
if isProd
|
|
256
|
+
script(type="module", vite-ignore).
|
|
257
|
+
import { PanelSet } from '/lib/panelset.js';
|
|
258
|
+
PanelSet.init({debug: true});
|
|
259
|
+
else
|
|
260
|
+
script(type="module").
|
|
261
|
+
import { PanelSet } from '/src/lib/index.js';
|
|
262
|
+
import '/src/lib/styles/panelset.scss';
|
|
263
|
+
PanelSet.init({debug: true});
|
|
264
|
+
|
|
265
|
+
if isProd
|
|
266
|
+
script(src="/assets/scripts/examples/async.js" type="module" vite-ignore)
|
|
267
|
+
else
|
|
268
|
+
script(type="module" src="/src/docs/assets/scripts/example-async.js")
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
extends /docs/views/templates/layouts/_base
|
|
2
|
+
|
|
3
|
+
append variables
|
|
4
|
+
- pagetitle = "Basic tabs"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
block content
|
|
8
|
+
.lane
|
|
9
|
+
.container
|
|
10
|
+
.page-header
|
|
11
|
+
h1 Basic tabs example
|
|
12
|
+
p.lead The most common use case - a simple tabbed interface
|
|
13
|
+
|
|
14
|
+
.lane
|
|
15
|
+
.container
|
|
16
|
+
h2 Live demo
|
|
17
|
+
|
|
18
|
+
.example
|
|
19
|
+
|
|
20
|
+
.demo
|
|
21
|
+
nav.tabs(role="tablist" aria-label="Content sections")
|
|
22
|
+
button(role="tab" aria-controls="demo-panel-1" aria-selected="true") Features
|
|
23
|
+
button(role="tab" aria-controls="demo-panel-2") Pricing
|
|
24
|
+
button(role="tab" aria-controls="demo-panel-3") Support
|
|
25
|
+
|
|
26
|
+
+examplepanelset('demo').tab-content
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
+codeblock('HTML')
|
|
30
|
+
+examplepanelset('demo').tab-content
|
|
31
|
+
|
|
32
|
+
+codeblock('JavaScript').
|
|
33
|
+
const tabs = document.querySelectorAll('[role="tab"]');
|
|
34
|
+
|
|
35
|
+
document.addEventListener('click', (e) => {
|
|
36
|
+
const button = e.target.closest('button');
|
|
37
|
+
if (!button) return;
|
|
38
|
+
|
|
39
|
+
if (button.hasAttribute('aria-controls')) {
|
|
40
|
+
const panelId = button.getAttribute('aria-controls');
|
|
41
|
+
const panel = document.getElementById(panelId);
|
|
42
|
+
const container = panel?.closest('[data-panelset]');
|
|
43
|
+
|
|
44
|
+
if (container?.panelSet) {
|
|
45
|
+
container.panelSet.setActive(panelId, true, { trigger: button });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Update ARIA states
|
|
51
|
+
document.addEventListener('ps:activationstart', (e) => {
|
|
52
|
+
tabs.forEach(tab => {
|
|
53
|
+
const isActive = tab.getAttribute('aria-controls') === e.detail.panelId;
|
|
54
|
+
tab.setAttribute('aria-selected', isActive);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
.lane
|
|
60
|
+
.container
|
|
61
|
+
h2 Accessibility features
|
|
62
|
+
p PanelSet is built with accessibility in mind, but because it provides low-level functionality, there are only a few of them built in:
|
|
63
|
+
ul
|
|
64
|
+
li
|
|
65
|
+
strong ARIA roles:
|
|
66
|
+
| Uses #[code role='tabpanel']. This is used internally to find the panels.
|
|
67
|
+
li
|
|
68
|
+
strong Hidden attribute:
|
|
69
|
+
| Properly hides inactive panels from screen readers
|
|
70
|
+
|
|
71
|
+
p Beyond that it's up to you to implement proper ARIA roles and attributes in your markup, like linking the button and panel ID's with aria. Futhermore, PanelSet does not manage focus or keyboard shortcuts like arrow keys support for you, so you'll need to handle that as appropriate for your use case.
|
|
72
|
+
|
|
73
|
+
append scripts
|
|
74
|
+
if isProd
|
|
75
|
+
script(type="module", vite-ignore).
|
|
76
|
+
import { PanelSet } from '/lib/panelset.js';
|
|
77
|
+
PanelSet.init();
|
|
78
|
+
else
|
|
79
|
+
script(type="module").
|
|
80
|
+
import { PanelSet } from '/src/lib/index.js';
|
|
81
|
+
import '/src/lib/styles/panelset.scss';
|
|
82
|
+
PanelSet.init({debug: true});
|
|
83
|
+
|
|
84
|
+
script.
|
|
85
|
+
const tabs = document.querySelectorAll('[role="tab"]');
|
|
86
|
+
|
|
87
|
+
document.addEventListener('click', (e) => {
|
|
88
|
+
const button = e.target.closest('button');
|
|
89
|
+
if (!button) return;
|
|
90
|
+
|
|
91
|
+
if (button.hasAttribute('aria-controls')) {
|
|
92
|
+
const panelId = button.getAttribute('aria-controls');
|
|
93
|
+
const panel = document.getElementById(panelId);
|
|
94
|
+
const container = panel?.closest('[data-panelset]');
|
|
95
|
+
|
|
96
|
+
if (container?.panelSet) {
|
|
97
|
+
container.panelSet.show(panelId, true, { trigger: button });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Update ARIA states
|
|
103
|
+
document.addEventListener('ps:activationstart', (e) => {
|
|
104
|
+
tabs.forEach(tab => {
|
|
105
|
+
const isActive = tab.getAttribute('aria-controls') === e.detail.panelId;
|
|
106
|
+
tab.setAttribute('aria-selected', isActive);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
append styles
|
|
113
|
+
style.
|
|
114
|
+
/* Tab-specific styles for this example */
|
|
115
|
+
//- #basic-tabs {
|
|
116
|
+
//- --fadeout-speed: 0.5s;
|
|
117
|
+
//- --fadein-speed: 0.5s;
|
|
118
|
+
//- --fadein-delay: 0.25s;
|
|
119
|
+
//- }
|
|
120
|
+
.tabs {
|
|
121
|
+
padding: 0 0.5rem;
|
|
122
|
+
display: flex;
|
|
123
|
+
//- gap: 0.5rem;
|
|
124
|
+
margin-bottom: 2px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.tabs button {
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: none;
|
|
130
|
+
border-bottom: 3px solid transparent;
|
|
131
|
+
padding: 0.75rem 1.5rem;
|
|
132
|
+
font-size: 1rem;
|
|
133
|
+
font-weight: 500;
|
|
134
|
+
color: #6b7280;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
transition: all 0.2s;
|
|
137
|
+
margin-bottom: -2px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.tabs button:hover {
|
|
141
|
+
color: #3b82f6;
|
|
142
|
+
background: #f9fafb;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.tabs button[aria-selected="true"] {
|
|
146
|
+
color: #3b82f6;
|
|
147
|
+
border-bottom-color: #3b82f6;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.tab-content {
|
|
151
|
+
padding: 1.5rem;
|
|
152
|
+
background: white;
|
|
153
|
+
border: 1px solid #e5e7eb;
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
}
|