refactor(core): [CORE-23] complete keyboard controls and focus lifecycle

This commit is contained in:
Harvey Zhao committed 2026-09-11 21:06:33 +08:00
1 parent faaa3e8c2d
commit 8ed0a5228c
190 files changed
+53981 -489

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
import type ResourceScope from '../lifecycle/scope'
import type { InfoHost } from './poll'
import { keyboardButton } from '../accessibility/button'
import { claimKey, plainKey } from '../accessibility/keyboard'
import { resolveFocusOrigin } from '../accessibility/overlay-focus'
import { focusPlayer } from '../accessibility/player-focus'
import { isClosing } from '../lifecycle/instance'
import { listen } from '../lifecycle/resources'
const origins = new WeakMap<InfoHost, HTMLElement>()
export function infoKeyboard(art: InfoHost, scope: ResourceScope, close: () => void): void {
const { $player, $info, $infoClose } = art.template
if (!$info)
return
const active = () => !scope.closed && !isClosing(art)
const visible = () => $player.classList.contains('art-info-show')
const closeLabel = art.i18n.get('Close')
if (!active())
return
const groupLabel = art.i18n.get('Video Info')
if (!active())
return
$infoClose.setAttribute('aria-label', closeLabel)
$info.setAttribute('role', 'group')
$info.setAttribute('aria-label', groupLabel)
keyboardButton(scope, $infoClose, () => $infoClose.click(), visible)
listen(scope, $info, 'keydown', (input) => {
const event = input as KeyboardEvent
if (active() && visible() && plainKey(event) && !event.defaultPrevented && event.key === 'Escape') {
claimKey(event)
close()
}
})
const changed = (show: boolean) => {
if (!active())
return
const doc = $player.ownerDocument
const focused = doc.activeElement as HTMLElement | null
if (show) {
if (!$info.contains(focused)) {
origins.delete(art)
if (focused && $player.contains(focused)) {
origins.set(art, resolveFocusOrigin(focused))
if ($player.classList.contains('art-keyboard-focus'))
$infoClose.focus({ preventScroll: true })
}
}
}
else {
const origin = origins.get(art)
origins.delete(art)
if (!$info.contains(focused))
return
const visibility = origin && doc.defaultView?.getComputedStyle(origin).visibility
if (origin?.isConnected && $player.contains(origin) && origin.getClientRects().length && visibility !== 'hidden' && visibility !== 'collapse' && !origin.matches(':disabled') && !origin.closest('[inert]') && origin.getAttribute('aria-disabled') !== 'true')
origin.focus({ preventScroll: true })
else
focusPlayer(art)
}
}
art.on('info', changed)
scope.add(() => {
art.off('info', changed)
})
}
+5 -2
View File
@@ -3,9 +3,11 @@ import type { ComponentHost } from '../component/types'
import type ResourceScope from '../lifecycle/scope'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
import { infoKeyboard } from './keyboard'
export interface InfoHost extends ComponentHost, SubscriptionHost<{ destroy: [] }> {
template: { $player: HTMLElement, $infoPanel: HTMLElement, $infoClose: HTMLElement, $video: object }
export interface InfoHost extends ComponentHost, SubscriptionHost<{ destroy: [], info: [boolean] }> {
template: { $player: HTMLElement, $info: HTMLElement | null, $infoPanel: HTMLElement, $infoClose: HTMLElement, $video: object }
i18n: { get: (key: string) => string }
constructor: { INFO_LOOP_TIME: number }
proxy: ComponentHost['events']['proxy']
}
@@ -36,6 +38,7 @@ export function pollInfo(art: InfoHost, scope: ResourceScope, close: () => void)
scope.add(() => {
art.off('destroy', destroy)
})
infoKeyboard(art, scope, close)
const loop = () => {
try {
for (const item of items) {