feat(widgets): add phase 4 validation and accessibility

This commit is contained in:
2026-08-17 19:12:38 +08:00
parent 31e72748c3
commit 009b66326a
23 changed files with 8078 additions and 53 deletions
+73 -4
View File
@@ -11,6 +11,59 @@
return true;
}
function moveTabFocus(event, root) {
var current = event.target.closest('[role="tab"]');
if (!current) return;
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
var index = tabs.indexOf(current);
var next = index;
if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
else if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = tabs.length - 1;
else return;
event.preventDefault();
tabs[next].focus();
tabs[next].click();
}
function initSwiperPause(el, swiper) {
if (!swiper.autoplay) return;
var i18n = config.i18n || {};
var manuallyPaused = false;
var button = document.createElement('button');
button.type = 'button';
button.className = 'aksrt-widget-swiper__pause';
button.setAttribute('aria-pressed', 'false');
function pause() {
if (typeof swiper.autoplay.pause === 'function') swiper.autoplay.pause();
else if (typeof swiper.autoplay.stop === 'function') swiper.autoplay.stop();
}
function resume() {
if (typeof swiper.autoplay.resume === 'function') swiper.autoplay.resume();
else if (typeof swiper.autoplay.start === 'function') swiper.autoplay.start();
}
function updateButton() {
button.setAttribute('aria-pressed', manuallyPaused ? 'true' : 'false');
button.setAttribute('aria-label', manuallyPaused ? (i18n.resume || '继续自动播放') : (i18n.pause || '暂停自动播放'));
button.textContent = manuallyPaused ? '▶' : 'Ⅱ';
}
button.addEventListener('click', function () {
manuallyPaused = !manuallyPaused;
if (manuallyPaused) pause();
else resume();
updateButton();
});
el.addEventListener('focusin', pause);
el.addEventListener('focusout', function (event) {
if (!manuallyPaused && !el.contains(event.relatedTarget)) resume();
});
updateButton();
el.appendChild(button);
}
function initSwiper(el) {
if (!once(el, 'Swiper') || typeof window.Swiper !== 'function') return;
var kind = el.dataset.aksrtSwiper || 'cards';
@@ -37,11 +90,13 @@
options.effect = el.dataset.effect || 'slide';
options.parallax = !!el.querySelector('[data-swiper-parallax]');
}
if (!reducedMotion && (el.dataset.autoplay === '1' || kind === 'notice')) {
var shouldAutoplay = !reducedMotion && (el.dataset.autoplay === '1' || kind === 'notice');
if (shouldAutoplay) {
options.autoplay = { delay: Number(el.dataset.interval || 4000), disableOnInteraction: false, pauseOnMouseEnter: true };
}
try {
el.aksrtSwiper = new window.Swiper(el, options);
if (shouldAutoplay) initSwiperPause(el, el.aksrtSwiper);
} catch (error) {
el.classList.add('is-fallback');
}
@@ -161,6 +216,7 @@
if (!widget || !instance || !config.restUrl || container.dataset.loading === '1') return;
container.dataset.loading = '1';
container.classList.add('is-loading');
container.setAttribute('aria-busy', 'true');
fetch(itemsUrl(widget, instance, page, segment), { credentials: 'same-origin', headers: { Accept: 'application/json' } })
.then(function (response) { if (!response.ok) throw new Error('items'); return response.json(); })
.then(function (data) {
@@ -180,7 +236,11 @@
scan(container);
})
.catch(function () { container.innerHTML = errorMarkup(); })
.finally(function () { delete container.dataset.loading; container.classList.remove('is-loading'); });
.finally(function () {
delete container.dataset.loading;
container.classList.remove('is-loading');
container.setAttribute('aria-busy', 'false');
});
}
function initItems(container) {
@@ -207,12 +267,17 @@
function initTabs(tabs) {
if (!once(tabs, 'Tabs')) return;
tabs.addEventListener('keydown', function (event) { moveTabFocus(event, tabs); });
tabs.addEventListener('click', function (event) {
var button = event.target.closest('[data-segment]');
if (!button) return;
tabs.querySelectorAll('[role="tab"]').forEach(function (tab) { tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); });
tabs.querySelectorAll('[role="tab"]').forEach(function (tab) {
tab.setAttribute('aria-selected', tab === button ? 'true' : 'false');
tab.setAttribute('tabindex', tab === button ? '0' : '-1');
});
var panel = tabs.querySelector('[data-widget-items]');
panel.dataset.segment = button.dataset.segment;
panel.setAttribute('aria-labelledby', button.id);
loadItems(panel, 1, false);
});
}
@@ -236,11 +301,15 @@
function initFeatures(features) {
if (!once(features, 'Features')) return;
features.addEventListener('keydown', function (event) { moveTabFocus(event, features); });
features.addEventListener('click', function (event) {
var button = event.target.closest('[data-feature-index]');
if (!button) return;
var index = button.dataset.featureIndex;
features.querySelectorAll('[data-feature-index]').forEach(function (tab) { tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); });
features.querySelectorAll('[data-feature-index]').forEach(function (tab) {
tab.setAttribute('aria-selected', tab === button ? 'true' : 'false');
tab.setAttribute('tabindex', tab === button ? '0' : '-1');
});
features.querySelectorAll('[data-feature-panel]').forEach(function (panel) { panel.hidden = panel.dataset.featurePanel !== index; });
});
}