mnfst 0.5.123 → 0.5.125
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/lib/manifest.code.js +3 -1
- package/lib/manifest.css +77 -16
- package/lib/manifest.data.js +9 -38
- package/lib/manifest.dropdowns.js +4 -2
- package/lib/manifest.form.css +77 -16
- package/lib/manifest.integrity.json +6 -6
- package/lib/manifest.js +82 -31
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.tabs.js +101 -88
- package/lib/manifest.tooltips.js +18 -3
- package/package.json +1 -1
package/lib/manifest.tabs.js
CHANGED
|
@@ -74,62 +74,79 @@ function initializeTabsPlugin() {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// Default tab value (first panel's id)
|
|
78
|
+
const defaultValue = panels.length > 0 ? panels[0].id : 'a';
|
|
79
|
+
|
|
80
|
+
// When Alpine has already initialized the scope (content injected at
|
|
81
|
+
// runtime, e.g. by the markdown plugin), x-data attribute edits are
|
|
82
|
+
// never re-read — write the property into the live scope instead.
|
|
83
|
+
const scopeHost = commonParent.hasAttribute('x-data') ? commonParent : commonParent.closest('[x-data]');
|
|
84
|
+
const liveData = scopeHost && scopeHost._x_dataStack && window.Alpine && typeof window.Alpine.$data === 'function'
|
|
85
|
+
? window.Alpine.$data(scopeHost)
|
|
86
|
+
: null;
|
|
87
|
+
|
|
88
|
+
if (liveData) {
|
|
89
|
+
if (!(tabProp in liveData)) liveData[tabProp] = defaultValue;
|
|
90
|
+
} else {
|
|
91
|
+
// Ensure x-data exists
|
|
92
|
+
if (!commonParent.hasAttribute('x-data')) {
|
|
93
|
+
commonParent.setAttribute('x-data', '{}');
|
|
94
|
+
}
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
newXData = beforeBrace + separator + tabProperty + afterBrace;
|
|
96
|
+
// Set up x-data with default value
|
|
97
|
+
const existingXData = commonParent.getAttribute('x-data') || '{}';
|
|
98
|
+
let newXData = existingXData;
|
|
99
|
+
|
|
100
|
+
// Check if the tab property already exists
|
|
101
|
+
// Escape special regex characters in tabProp
|
|
102
|
+
const escapedTabProp = tabProp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103
|
+
const propertyRegex = new RegExp(`${escapedTabProp}\\s*:\\s*'[^']*'`, 'g');
|
|
104
|
+
if (!propertyRegex.test(newXData)) {
|
|
105
|
+
const tabProperty = `${tabProp}: '${defaultValue}'`;
|
|
106
|
+
|
|
107
|
+
if (newXData === '{}') {
|
|
108
|
+
newXData = `{ ${tabProperty} }`;
|
|
109
|
+
} else {
|
|
110
|
+
const lastBraceIndex = newXData.lastIndexOf('}');
|
|
111
|
+
if (lastBraceIndex > 0) {
|
|
112
|
+
const beforeBrace = newXData.substring(0, lastBraceIndex);
|
|
113
|
+
const afterBrace = newXData.substring(lastBraceIndex);
|
|
114
|
+
const separator = beforeBrace.trim().endsWith(',') ? '' : ', ';
|
|
115
|
+
newXData = beforeBrace + separator + tabProperty + afterBrace;
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
|
-
}
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
if (newXData !== existingXData) {
|
|
120
|
+
commonParent.setAttribute('x-data', newXData);
|
|
121
|
+
}
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
// ----- Accessibility wiring (WAI-ARIA Tabs pattern) -----
|
|
113
126
|
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
// The full APG tabs pattern (role="tab"/"tabpanel", aria-controls,
|
|
128
|
+
// roving tabindex, arrow-key navigation) is applied only when the
|
|
129
|
+
// author wraps the buttons in an element with role="tablist" —
|
|
130
|
+
// those roles are only valid inside one, and inferring a container
|
|
131
|
+
// risks claiming the wrong element. Without a tablist, buttons stay
|
|
132
|
+
// plain buttons and panels plain regions; :aria-selected still
|
|
133
|
+
// syncs as the selected-state hook.
|
|
134
|
+
const tablists = new Set();
|
|
135
|
+
relevantButtons.forEach((b) => {
|
|
136
|
+
const t = b.closest('[role=tablist]');
|
|
137
|
+
if (t) tablists.add(t);
|
|
138
|
+
});
|
|
139
|
+
const wireAria = tablists.size > 0;
|
|
123
140
|
|
|
124
|
-
// Assign ids where missing.
|
|
141
|
+
// Assign ids where missing (needed for aria-labelledby/controls).
|
|
125
142
|
const buttonIdByTabValue = {};
|
|
126
143
|
relevantButtons.forEach((button, i) => {
|
|
127
144
|
const tabValue = button.getAttribute('x-tab');
|
|
128
145
|
if (!tabValue) return;
|
|
129
|
-
if (!button.id) {
|
|
146
|
+
if (!button.id && wireAria) {
|
|
130
147
|
button.id = `mnfst-tab-${sanitizedPanelSet || 'g'}-${tabValue.replace(/[^a-zA-Z0-9_-]/g, '-')}-${i}`;
|
|
131
148
|
}
|
|
132
|
-
buttonIdByTabValue[tabValue] = button.id;
|
|
149
|
+
if (button.id) buttonIdByTabValue[tabValue] = button.id;
|
|
133
150
|
});
|
|
134
151
|
|
|
135
152
|
// Process panels for this group - add x-show + a11y attributes
|
|
@@ -142,12 +159,14 @@ function initializeTabsPlugin() {
|
|
|
142
159
|
if (!panel.element.id) panel.element.id = panel.id;
|
|
143
160
|
|
|
144
161
|
// ARIA: role + label + focusable
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
panel.element.
|
|
162
|
+
if (wireAria) {
|
|
163
|
+
panel.element.setAttribute('role', 'tabpanel');
|
|
164
|
+
if (!panel.element.hasAttribute('tabindex')) {
|
|
165
|
+
panel.element.setAttribute('tabindex', '0');
|
|
166
|
+
}
|
|
167
|
+
const labelledBy = buttonIdByTabValue[panel.id];
|
|
168
|
+
if (labelledBy) panel.element.setAttribute('aria-labelledby', labelledBy);
|
|
148
169
|
}
|
|
149
|
-
const labelledBy = buttonIdByTabValue[panel.id];
|
|
150
|
-
if (labelledBy) panel.element.setAttribute('aria-labelledby', labelledBy);
|
|
151
170
|
|
|
152
171
|
// Remove x-tabpanel attribute since we've converted it
|
|
153
172
|
panel.element.removeAttribute('x-tabpanel');
|
|
@@ -163,56 +182,50 @@ function initializeTabsPlugin() {
|
|
|
163
182
|
const clickHandler = `${tabProp} = '${tabValue}'`;
|
|
164
183
|
button.setAttribute('x-on:click', clickHandler);
|
|
165
184
|
|
|
166
|
-
//
|
|
167
|
-
|
|
185
|
+
// Selection state (reactive via :aria-selected) — always synced;
|
|
186
|
+
// it doubles as the documented selected-state styling hook
|
|
168
187
|
button.setAttribute(':aria-selected', `String(${tabProp} === '${tabValue}')`);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
|
|
189
|
+
// ARIA: role, roving tabindex, controls — only inside an author tablist
|
|
190
|
+
if (button.closest('[role=tablist]')) {
|
|
191
|
+
button.setAttribute('role', 'tab');
|
|
192
|
+
// Roving tabindex: -1 when not active so arrow keys, not Tab, move between tabs.
|
|
193
|
+
button.setAttribute(':tabindex', `${tabProp} === '${tabValue}' ? '0' : '-1'`);
|
|
194
|
+
const panel = panels.find((p) => p.id === tabValue);
|
|
195
|
+
if (panel && panel.element.id) {
|
|
196
|
+
button.setAttribute('aria-controls', panel.element.id);
|
|
197
|
+
}
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
// Remove x-tab attribute since we've converted it
|
|
177
201
|
button.removeAttribute('x-tab');
|
|
178
202
|
});
|
|
179
203
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
// Arrow-key navigation (Left/Right/Home/End) on each author tablist.
|
|
205
|
+
tablists.forEach((tablistEl) => {
|
|
206
|
+
if (!tablistEl.__mnfstTabsKeydown) {
|
|
207
|
+
tablistEl.__mnfstTabsKeydown = (e) => {
|
|
208
|
+
const target = e.target;
|
|
209
|
+
if (!target || target.getAttribute('role') !== 'tab') return;
|
|
210
|
+
const tabs = Array.from(tablistEl.querySelectorAll('[role="tab"]'));
|
|
211
|
+
const idx = tabs.indexOf(target);
|
|
212
|
+
if (idx === -1) return;
|
|
213
|
+
let nextIdx = null;
|
|
214
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') nextIdx = (idx + 1) % tabs.length;
|
|
215
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
216
|
+
else if (e.key === 'Home') nextIdx = 0;
|
|
217
|
+
else if (e.key === 'End') nextIdx = tabs.length - 1;
|
|
218
|
+
if (nextIdx == null) return;
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
// Automatic activation: focusing a tab selects it. This matches
|
|
221
|
+
// the most common APG variant and Manifest's existing click-to-
|
|
222
|
+
// activate semantics.
|
|
223
|
+
tabs[nextIdx].focus();
|
|
224
|
+
tabs[nextIdx].click();
|
|
225
|
+
};
|
|
226
|
+
tablistEl.addEventListener('keydown', tablistEl.__mnfstTabsKeydown);
|
|
189
227
|
}
|
|
190
|
-
|
|
191
|
-
tablistEl.setAttribute('role', 'tablist');
|
|
192
|
-
if (!tablistEl.__mnfstTabsKeydown) {
|
|
193
|
-
tablistEl.__mnfstTabsKeydown = (e) => {
|
|
194
|
-
const target = e.target;
|
|
195
|
-
if (!target || target.getAttribute('role') !== 'tab') return;
|
|
196
|
-
const tabs = Array.from(tablistEl.querySelectorAll('[role="tab"]'));
|
|
197
|
-
const idx = tabs.indexOf(target);
|
|
198
|
-
if (idx === -1) return;
|
|
199
|
-
let nextIdx = null;
|
|
200
|
-
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') nextIdx = (idx + 1) % tabs.length;
|
|
201
|
-
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') nextIdx = (idx - 1 + tabs.length) % tabs.length;
|
|
202
|
-
else if (e.key === 'Home') nextIdx = 0;
|
|
203
|
-
else if (e.key === 'End') nextIdx = tabs.length - 1;
|
|
204
|
-
if (nextIdx == null) return;
|
|
205
|
-
e.preventDefault();
|
|
206
|
-
// Automatic activation: focusing a tab selects it. This matches
|
|
207
|
-
// the most common APG variant and Manifest's existing click-to-
|
|
208
|
-
// activate semantics.
|
|
209
|
-
tabs[nextIdx].focus();
|
|
210
|
-
tabs[nextIdx].click();
|
|
211
|
-
};
|
|
212
|
-
tablistEl.addEventListener('keydown', tablistEl.__mnfstTabsKeydown);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
228
|
+
})
|
|
216
229
|
|
|
217
230
|
// Ensure Alpine processes the updated x-data and x-show attributes
|
|
218
231
|
if (window.Alpine && typeof window.Alpine.initTree === 'function') {
|
package/lib/manifest.tooltips.js
CHANGED
|
@@ -42,6 +42,7 @@ function initializeTooltipPlugin() {
|
|
|
42
42
|
const TOOLTIP_CHAIN_GRACE_MS = 250;
|
|
43
43
|
let _lastTooltipHideTime = 0;
|
|
44
44
|
const markTooltipHidden = () => { _lastTooltipHideTime = Date.now(); };
|
|
45
|
+
const clearChainWindow = () => { _lastTooltipHideTime = 0; };
|
|
45
46
|
const isInChainWindow = () => (Date.now() - _lastTooltipHideTime) < TOOLTIP_CHAIN_GRACE_MS;
|
|
46
47
|
|
|
47
48
|
// ---- Singletons per host ----
|
|
@@ -148,7 +149,10 @@ function initializeTooltipPlugin() {
|
|
|
148
149
|
trigger._tooltipAnchorName = `--tooltip-trigger-${code}`;
|
|
149
150
|
}
|
|
150
151
|
const anchorName = trigger._tooltipAnchorName;
|
|
151
|
-
|
|
152
|
+
// Compose with --co-anchor so stylesheet anchors (e.g. the tablist
|
|
153
|
+
// slider's --selected-tab on the selected tab) survive this inline
|
|
154
|
+
// override — the var re-resolves reactively as selection changes.
|
|
155
|
+
trigger.style.setProperty('anchor-name', `${anchorName}, var(--co-anchor, --no-anchor)`);
|
|
152
156
|
void trigger.offsetHeight; // reflow so anchor-name registers
|
|
153
157
|
s.el.style.setProperty('position-anchor', anchorName);
|
|
154
158
|
|
|
@@ -173,7 +177,9 @@ function initializeTooltipPlugin() {
|
|
|
173
177
|
|
|
174
178
|
// Hide the singleton that's currently showing (if any), regardless of host.
|
|
175
179
|
function hideAnySingleton() {
|
|
180
|
+
let wasOpen = false;
|
|
176
181
|
document.querySelectorAll('.tooltip[popover="hint"]:popover-open').forEach(el => {
|
|
182
|
+
wasOpen = true;
|
|
177
183
|
try { el.hidePopover(); } catch {}
|
|
178
184
|
});
|
|
179
185
|
// Restore each tooltip's prior aria-describedby on the trigger it had been
|
|
@@ -186,7 +192,9 @@ function initializeTooltipPlugin() {
|
|
|
186
192
|
else el.removeAttribute('aria-describedby');
|
|
187
193
|
el._tooltipPriorDescribedBy = undefined;
|
|
188
194
|
});
|
|
189
|
-
|
|
195
|
+
// Only arm the chain window when something was actually open — marking
|
|
196
|
+
// unconditionally let a plain click fast-track the next focus show.
|
|
197
|
+
if (wasOpen) markTooltipHidden();
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
// ---- Directive ----
|
|
@@ -283,7 +291,10 @@ function initializeTooltipPlugin() {
|
|
|
283
291
|
|
|
284
292
|
// Keyboard / focus interactions — WCAG 2.1 SC 1.4.13 requires tooltip
|
|
285
293
|
// content to be accessible to keyboard users via focus, not hover only.
|
|
286
|
-
|
|
294
|
+
// Gated on :focus-visible so mouse-click focus doesn't flash the tooltip.
|
|
295
|
+
el.addEventListener('focus', () => {
|
|
296
|
+
if (el.matches(':focus-visible')) requestShow();
|
|
297
|
+
});
|
|
287
298
|
el.addEventListener('blur', requestHide);
|
|
288
299
|
|
|
289
300
|
// Mousedown/click: always hide immediately; scheduleAnchorRestore so the
|
|
@@ -292,6 +303,10 @@ function initializeTooltipPlugin() {
|
|
|
292
303
|
const hideAndScheduleRestore = () => {
|
|
293
304
|
cancelPendingShow();
|
|
294
305
|
hideAnySingleton();
|
|
306
|
+
// A click is a deliberate dismissal — clear the chain window so a
|
|
307
|
+
// synthetic re-hover (e.g. content shifting under the cursor after
|
|
308
|
+
// navigation) waits the full delay instead of showing instantly.
|
|
309
|
+
clearChainWindow();
|
|
295
310
|
scheduleAnchorRestore(el);
|
|
296
311
|
};
|
|
297
312
|
el.addEventListener('mousedown', hideAndScheduleRestore);
|