From d79fc02167774a83befbd94268bfa4f5e8b94cb4 Mon Sep 17 00:00:00 2001 From: Nathan Knowler Date: Sat, 7 Mar 2026 16:41:37 -0600 Subject: [PATCH 1/7] refactor: use interest invokers for contributor hovercard --- app/pages/about.vue | 370 ++++++-------------------------------------- 1 file changed, 48 insertions(+), 322 deletions(-) diff --git a/app/pages/about.vue b/app/pages/about.vue index b250064969..f1038f2da0 100644 --- a/app/pages/about.vue +++ b/app/pages/about.vue @@ -73,227 +73,13 @@ const roleLabels = computed( }) as Partial>, ) -// --- Popover Logic (Global Single Instance with Event Delegation) --- -// We use a single global popover instance for performance (especially in Firefox with many items). -// Event delegation on the list handles interactions, avoiding listeners on every item. const activeContributor = shallowRef() -const popoverPos = reactive({ top: 0, left: 0, align: 'center' as 'left' | 'center' | 'right' }) -const panelRef = useTemplateRef('panelRef') -const activeBtnEl = shallowRef() -let closeTimer: ReturnType | undefined -let lastOpenTime = 0 - -// Mouse tracking for scroll interactions -let mouseX = 0 -let mouseY = 0 -let scrollTimer: ReturnType | undefined - -function cancelClose() { - if (closeTimer) { - clearTimeout(closeTimer) - closeTimer = undefined - } -} - -function computePos(btn: HTMLElement) { - const r = btn.getBoundingClientRect() - const vw = window.innerWidth - const POP_W = 256 - const GAP = 8 - - popoverPos.top = r.bottom + GAP - const center = r.left + r.width / 2 - - if (center - POP_W / 2 < GAP) { - popoverPos.align = 'left' - popoverPos.left = r.left - } else if (center + POP_W / 2 > vw - GAP) { - popoverPos.align = 'right' - popoverPos.left = r.right - } else { - popoverPos.align = 'center' - popoverPos.left = center - } -} - -// DON'T MOVE aria-expanded to the template, Firefox performance issues -function setActiveBtnExpanded(btn: HTMLElement | null, value: boolean) { - if (activeBtnDom && activeBtnDom !== btn) { - activeBtnDom.removeAttribute('aria-controls') - activeBtnDom.setAttribute('aria-expanded', 'false') - } - activeBtnDom = btn - if (btn) { - if (value) { - btn.setAttribute('aria-expanded', 'true') - btn.setAttribute('aria-controls', 'contributor-popover') - } else { - btn.setAttribute('aria-expanded', 'false') - btn.removeAttribute('aria-controls') - } - } -} - -function openById(id: number, btnEl: HTMLElement, focus = false) { - const c = contributors.value.find(x => x.id === id) - if (!c || !isExpandable(c)) return - cancelClose() - computePos(btnEl) - activeBtnEl.value = btnEl - setActiveBtnExpanded(btnEl, true) - activeContributor.value = c - lastOpenTime = Date.now() - - if (focus) { - nextTick(() => { - panelRef.value?.focus() - }) - } -} - -function scheduleCloseActive() { - cancelClose() - closeTimer = setTimeout(() => { - setActiveBtnExpanded(null, false) - activeContributor.value = undefined - }, 80) -} - -function getButtonFromEvent(e: Event): HTMLButtonElement | null { - return (e.target as Element).closest('button[data-cid]') -} - -function onListMouseEnter(e: MouseEvent) { - const btn = getButtonFromEvent(e) - if (!btn) return - openById(Number(btn.dataset.cid), btn) -} - -function onListMouseLeave(e: MouseEvent) { - // only close if we exist >ul> - const related = e.relatedTarget as Element | null - if (related?.closest('[data-popover-panel]')) return - if (!related?.closest('button[data-cid]')) scheduleCloseActive() -} - -function onListClick(e: MouseEvent) { - const btn = getButtonFromEvent(e) - if (!btn) return - const id = Number(btn.dataset.cid) - if (activeContributor.value?.id === id && Date.now() - lastOpenTime > 50) { - setActiveBtnExpanded(null, false) - activeContributor.value = undefined - // Return focus to button when closing via click - btn.focus() - } else { - // Open and focus the panel for keyboard accessibility - openById(id, btn, true) - } -} - -// Panel mouse events -function onPanelMouseLeave(e: MouseEvent) { - const related = e.relatedTarget as Element | null - if (!related?.closest('button[data-cid]')) scheduleCloseActive() -} - -// Tab management inside the panel (manual focus) -function onPanelKeydown(e: KeyboardEvent) { - if (e.key !== 'Tab' || !panelRef.value) return - const focusables = [...panelRef.value.querySelectorAll('a[href]')] - if (!focusables.length) { - e.preventDefault() - activeBtnEl.value?.focus() - return - } - - const first = focusables[0] - const last = focusables.at(-1)! - - if (e.shiftKey && document.activeElement === panelRef.value) { - e.preventDefault() - activeBtnEl.value?.focus() - return - } - - if (e.shiftKey && document.activeElement === first) { - e.preventDefault() - // Keep open but focus button - activeBtnEl.value?.focus() - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault() - setActiveBtnExpanded(null, false) - activeContributor.value = undefined - - // Find next button - const allBtns = [...document.querySelectorAll('button[data-cid]')] - const idx = allBtns.indexOf(activeBtnEl.value!) - const nextBtn = allBtns[idx + 1] - - if (nextBtn) { - nextBtn.focus() - } else { - activeBtnEl.value?.focus() - } - } -} - -// Document listeners -function onDocumentPointerDown(e: PointerEvent) { - if (!activeContributor.value) return - const t = e.target as Element - if (!t.closest('[data-popover-panel]') && !t.closest('button[data-cid]')) { - setActiveBtnExpanded(null, false) - activeContributor.value = undefined - } -} - -function onDocumentKeydown(e: KeyboardEvent) { - if (e.key === 'Escape' && activeContributor.value) { - setActiveBtnExpanded(null, false) - activeContributor.value = undefined - activeBtnEl.value?.focus() - } -} - -function onMouseMove(e: MouseEvent) { - mouseX = e.clientX - mouseY = e.clientY -} - -function checkHover() { - const el = document.elementFromPoint(mouseX, mouseY) - const btn = el?.closest('button[data-cid]') as HTMLElement | null - if (btn) { - openById(Number(btn.dataset.cid), btn) - } -} -function onScroll() { - if (activeContributor.value) { - setActiveBtnExpanded(null, false) - activeContributor.value = undefined - } - clearTimeout(scrollTimer) - scrollTimer = setTimeout(checkHover, 150) +function onBeforeToggleHoverCard(event) { + const { cid } = event.source.dataset + const contributor = contributors.value.find(c => c.id === Number(cid)) + activeContributor.value = contributor } - -let activeBtnDom: HTMLElement | null = null - -onMounted(() => { - document.addEventListener('pointerdown', onDocumentPointerDown) - document.addEventListener('keydown', onDocumentKeydown) - window.addEventListener('scroll', onScroll, { passive: true }) - window.addEventListener('mousemove', onMouseMove, { passive: true }) -}) -onBeforeUnmount(() => { - cancelClose() - clearTimeout(scrollTimer) - document.removeEventListener('pointerdown', onDocumentPointerDown) - document.removeEventListener('keydown', onDocumentKeydown) - window.removeEventListener('scroll', onScroll) - window.removeEventListener('mousemove', onMouseMove) -}) From b6c1f798240f6aed74dc23982747dc2feb1179d6 Mon Sep 17 00:00:00 2001 From: Nathan Knowler Date: Sat, 7 Mar 2026 17:02:27 -0600 Subject: [PATCH 2/7] refactor: remove Transition --- app/pages/about.vue | 231 ++++++++++++++++++++++---------------------- 1 file changed, 114 insertions(+), 117 deletions(-) diff --git a/app/pages/about.vue b/app/pages/about.vue index f1038f2da0..28808c0d22 100644 --- a/app/pages/about.vue +++ b/app/pages/about.vue @@ -278,130 +278,127 @@ function onBeforeToggleHoverCard(event) { - -
+
-
-
- - {{ activeContributor.name || activeContributor.login }} - -
- {{ roleLabels[activeContributor.role] }} -
-

- "{{ activeContributor.bio }}" -

-
-
-
+ + From d9c61ced16c08effcee526444659a564390e4c55 Mon Sep 17 00:00:00 2001 From: Nathan Knowler Date: Sat, 7 Mar 2026 17:04:34 -0600 Subject: [PATCH 3/7] refactor: set hovercard label with aria-labelledby ref --- app/pages/about.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/pages/about.vue b/app/pages/about.vue index 28808c0d22..7195432a7e 100644 --- a/app/pages/about.vue +++ b/app/pages/about.vue @@ -281,7 +281,7 @@ function onBeforeToggleHoverCard(event) {
@@ -290,9 +290,12 @@ function onBeforeToggleHoverCard(event) { class="flex flex-col gap-y-3 w-64 rounded-xl border border-border-subtle bg-bg-elevated p-4 shadow-2xl text-start" >
- +

{{ activeContributor.name || activeContributor.login }} - +

Date: Sat, 7 Mar 2026 21:19:29 -0600 Subject: [PATCH 4/7] feat: add skip links for long lists of focusables --- app/components/CallToAction.vue | 2 +- app/pages/about.vue | 19 ++++++++++++++++--- i18n/locales/en.json | 3 +++ i18n/schema.json | 9 +++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/components/CallToAction.vue b/app/components/CallToAction.vue index ba79780c9e..d3e931cf89 100644 --- a/app/components/CallToAction.vue +++ b/app/components/CallToAction.vue @@ -42,7 +42,7 @@ function handleCardClick(event: MouseEvent) {