Skip to content

Commit 3010141

Browse files
committed
fix doclets
1 parent c36daa1 commit 3010141

5 files changed

Lines changed: 152 additions & 172 deletions

File tree

packages/spotlight/src/container.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,61 @@ const getOwnedNodes = (node, selector) => {
311311
return [];
312312
};
313313

314+
/**
315+
* Returns the nearest `[aria-owns]` owner whose owned subtree contains `containerNode`.
316+
*
317+
* @param {Node} containerNode Popup container node or descendant
318+
* @returns {Node|null} Owner element, or null
319+
* @memberof spotlight/container
320+
* @private
321+
*/
322+
const getPopupOwnerElement = (containerNode) => {
323+
if (!containerNode) {
324+
return null;
325+
}
326+
327+
const owners = document.querySelectorAll('[aria-owns]');
328+
for (const owner of owners) {
329+
if (getOwnedNodes(owner, '*').some(n => n.contains(containerNode))) {
330+
return owner;
331+
}
332+
}
333+
334+
return null;
335+
};
336+
337+
/**
338+
* Returns IDs of `self-only` spotlight containers reachable through the `aria-owns` of
339+
* `ownerNode`, excluding `excludeContainerId`.
340+
*
341+
* @param {Node} ownerNode Owner element carrying `aria-owns`
342+
* @param {String} excludeContainerId Container ID to skip
343+
* @returns {String[]} Owned self-only container IDs
344+
* @memberof spotlight/container
345+
* @private
346+
*/
347+
const getOwnedSelfOnlyContainerIds = (ownerNode, excludeContainerId) => {
348+
const ownedContainerSelector = '[data-spotlight-container][data-spotlight-id]';
349+
const ids = [];
350+
351+
for (const ownedNode of getOwnedNodes(ownerNode, '*')) {
352+
const containerNodes = [
353+
...(ownedNode.matches?.(ownedContainerSelector) ? [ownedNode] : []),
354+
...ownedNode.querySelectorAll(ownedContainerSelector)
355+
];
356+
for (const containerNode of containerNodes) {
357+
const id = getContainerId(containerNode);
358+
if (!id || id === excludeContainerId) continue;
359+
const cfg = getContainerConfig(id);
360+
if (cfg?.restrict === 'self-only') {
361+
ids.push(id);
362+
}
363+
}
364+
}
365+
366+
return ids;
367+
};
368+
314369
/**
315370
* Determines all spottable elements and containers that are directly contained by the container
316371
* identified by `containerId` and no other subcontainers.
@@ -1146,6 +1201,8 @@ export {
11461201
addContainer,
11471202
containerAttribute,
11481203
getOwnedNodes,
1204+
getOwnedSelfOnlyContainerIds,
1205+
getPopupOwnerElement,
11491206
configureDefaults,
11501207
configureContainer,
11511208
getContainerFocusTarget,

packages/spotlight/src/spotlight.js

Lines changed: 26 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {
4141
getContainerNode,
4242
getContainersForNode,
4343
getLastContainer,
44-
getOwnedNodes,
44+
getOwnedSelfOnlyContainerIds,
45+
getPopupOwnerElement,
4546
getSpottableDescendants,
4647
isContainer,
4748
isContainer5WayHoldable,
@@ -98,19 +99,12 @@ const isRight = is('right');
9899
const isTab = is('tab');
99100
const isUp = is('up');
100101

101-
// Two elements whose vertical centers are within this many pixels are treated as being on the same
102-
// visual row, so horizontal position decides Tab order between them. 24 px covers typical
103-
// line-height variance (e.g. 20 px text + 4 px leading) while still separating distinct rows.
102+
// Vertical centers within this distance share a Tab row; horizontal position breaks ties.
104103
const TAB_ROW_THRESHOLD = 24;
105104

106-
// Per-keypress cache for getLinearTargetsInContainer. Keyed by containerId; populated lazily
107-
// during one synchronous Tab onKeyDown call and cleared before the next. The DOM cannot change
108-
// between two synchronous calls within the same event handler, so no further invalidation logic
109-
// is needed.
105+
// Per-keypress cache for getLinearTargetsInContainer, scoped to one handleTab call.
110106
let _linearTargetsCache = null;
111107

112-
// Populated by the Spotlight IIFE; exported for unit tests only.
113-
// Not part of the public Spotlight API — import from this module path directly in tests.
114108
let _tabNavTestHooks = {};
115109

116110
/**
@@ -537,65 +531,13 @@ const Spotlight = (function () {
537531
return false;
538532
}
539533

540-
function getPopupOwnerElement (containerNode) {
541-
if (!containerNode) {
542-
return null;
543-
}
544-
545-
const owners = document.querySelectorAll('[aria-owns]');
546-
for (const owner of owners) {
547-
if (getOwnedNodes(owner, '*').some(n => n.contains(containerNode))) {
548-
return owner;
549-
}
550-
}
551-
552-
return null;
553-
}
554-
555-
/*
556-
* Returns the IDs of all `self-only` spotlight containers reachable through the `aria-owns`
557-
* of `ownerNode`, excluding `excludeContainerId`. Handles both the common case where
558-
* `aria-owns` points to a float-layer wrapper div (so containers are found via querySelector)
559-
* and the direct case where `aria-owns` points to the container node itself.
560-
*/
561-
function getOwnedSelfOnlyContainerIds (ownerNode, excludeContainerId) {
562-
const containerSelector = '[data-spotlight-container][data-spotlight-id]';
563-
const ids = [];
564-
565-
for (const ownedNode of getOwnedNodes(ownerNode, '*')) {
566-
const containerNodes = [
567-
...(ownedNode.matches?.(containerSelector) ? [ownedNode] : []),
568-
...ownedNode.querySelectorAll(containerSelector)
569-
];
570-
for (const containerNode of containerNodes) {
571-
const id = getContainerId(containerNode);
572-
if (!id || id === excludeContainerId) continue;
573-
const cfg = getContainerConfig(id);
574-
if (cfg?.restrict === 'self-only') {
575-
ids.push(id);
576-
}
577-
}
578-
}
579-
580-
return ids;
581-
}
582-
583-
// Returns the nearest ancestor (including self) that carries `aria-owns`, or null when the
584-
// node is missing, reaches the body, or has no such ancestor. Used to identify popup owners.
585-
function getAriaOwnerNode (target) {
586-
const ownerNode = target?.closest('[aria-owns]');
587-
return ownerNode && ownerNode !== document.body ? ownerNode : null;
588-
}
589-
590534
function resolveTargetToOpenPopupItem (target, currentPopupContainerId, isForward) {
591-
const ownerNode = getAriaOwnerNode(target);
592-
if (!ownerNode) {
535+
const ownerNode = target?.closest?.('[aria-owns]');
536+
if (!ownerNode || ownerNode === document.body) {
593537
return target;
594538
}
595539

596540
for (const popupContainerId of getOwnedSelfOnlyContainerIds(ownerNode, currentPopupContainerId)) {
597-
// Preserve linear order when entering another open dropdown: Tab forward starts at
598-
// first option, Shift+Tab starts at last option.
599541
const popupTargets = getLinearTargetsInContainer(popupContainerId);
600542
const popupTargetEntry = isForward ? popupTargets[0] : popupTargets[popupTargets.length - 1];
601543
const popupTarget = popupTargetEntry?.target || getTargetByContainer(popupContainerId);
@@ -639,10 +581,7 @@ const Spotlight = (function () {
639581
comesBeforeInTabOrder(ax, ay, cx, cy, isRtl) :
640582
comesBeforeInTabOrder(cx, cy, ax, ay, isRtl);
641583
if (inOrder) {
642-
// Return the first in-order target regardless of whether it is a popup owner.
643-
// resolveTargetToOpenPopupItem will redirect into an open popup when appropriate.
644-
// Deferring popup owners as a fallback causes plain controls later in visual
645-
// order to win instead, which breaks the expected a→b→c traversal chain.
584+
// Do not defer popup owners; resolveTargetToOpenPopupItem redirects into open popups.
646585
return target;
647586
}
648587
}
@@ -666,8 +605,6 @@ const Spotlight = (function () {
666605
const isRtl = isRtlDocument();
667606
const rootTargets = getLinearTargetsInContainer(rootContainerId);
668607

669-
// For forward Tab, prefer continuity from the actual focused option so the next nearby
670-
// control in visual order wins after the popup boundary.
671608
if (isForward) {
672609
const fallbackTarget = findLinearTabExitTargetInTargets(
673610
rootTargets,
@@ -681,14 +618,9 @@ const Spotlight = (function () {
681618
}
682619
}
683620

684-
// Anchor exit traversal to the popup owner first so Tab completes the opened dropdown and
685-
// then continues through surrounding nearby controls in the base layout order.
686621
if (popupOwner) {
687622
const ownerRect = getRect(popupOwner);
688-
// Use top+1 rather than the vertical center so the anchor sits at the leading edge of
689-
// the owner's bounding box. This keeps controls that are visually level with the top
690-
// of the trigger button in the same TAB_ROW_THRESHOLD band, preventing them from
691-
// being incorrectly classified as a separate row when the owner button is tall.
623+
// top+1 keeps row-level neighbors in the same TAB_ROW_THRESHOLD band as a tall owner button.
692624
const ownerTarget = findLinearTabExitTargetInTargets(
693625
rootTargets,
694626
ownerRect.center.x,
@@ -701,7 +633,6 @@ const Spotlight = (function () {
701633
}
702634
}
703635

704-
// Reverse Shift+Tab fallback keeps previous behavior when owner-based handoff has no match.
705636
if (!isForward) {
706637
const fallbackTarget = findLinearTabExitTargetInTargets(
707638
rootTargets,
@@ -717,6 +648,20 @@ const Spotlight = (function () {
717648
return null;
718649
}
719650

651+
function getLinearTargetContainerId (target) {
652+
// Prefer DOM ancestry over getContainersForNode so aria-owns owners stay in root Tab order.
653+
const containerNode = target?.closest?.('[data-spotlight-container][data-spotlight-id]');
654+
if (containerNode) {
655+
const domContainerId = getContainerId(containerNode);
656+
if (domContainerId) {
657+
return domContainerId;
658+
}
659+
}
660+
661+
const containerIds = getContainersForNode(target);
662+
return last(containerIds);
663+
}
664+
720665
function getLinearTargetsInContainer (containerId) {
721666
if (_linearTargetsCache?.has(containerId)) {
722667
return _linearTargetsCache.get(containerId);
@@ -725,20 +670,6 @@ const Spotlight = (function () {
725670
const isRtl = isRtlDocument();
726671
const visitedContainers = new Set();
727672
const visitedTargets = new Set();
728-
const getLinearTargetContainerId = (target) => {
729-
// Resolve by DOM ancestry first so popup owners linked through aria-owns are treated as
730-
// part of their visual/root context rather than being pulled into an owned self-only popup.
731-
const containerNode = target?.closest?.('[data-spotlight-container][data-spotlight-id]');
732-
if (containerNode) {
733-
const domContainerId = getContainerId(containerNode);
734-
if (domContainerId) {
735-
return domContainerId;
736-
}
737-
}
738-
739-
const containerIds = getContainersForNode(target);
740-
return last(containerIds);
741-
};
742673

743674
const gatherSpottableLeaves = (id) => {
744675
if (!id || visitedContainers.has(id)) {
@@ -785,7 +716,6 @@ const Spotlight = (function () {
785716
function spotLinear (isForward) {
786717
let currentFocusedElement = getCurrent();
787718

788-
// If there is no currently focused spottable target, bootstrap focus first.
789719
if (!currentFocusedElement) {
790720
if (!restoreFocus()) {
791721
return false;
@@ -796,17 +726,6 @@ const Spotlight = (function () {
796726
}
797727
}
798728

799-
const currentContainerIds = getContainersForNode(currentFocusedElement);
800-
if (!currentContainerIds.length) {
801-
if (!restoreFocus()) {
802-
return false;
803-
}
804-
currentFocusedElement = getCurrent();
805-
if (!currentFocusedElement) {
806-
return false;
807-
}
808-
}
809-
810729
const searchContainerId = getLinearTabSearchContainerId(currentFocusedElement);
811730
const linearTargets = getLinearTargetsInContainer(searchContainerId);
812731
if (!linearTargets.length) {
@@ -859,21 +778,8 @@ const Spotlight = (function () {
859778
}
860779

861780
/*
862-
* Shared Tab/Shift+Tab handler: resets pointer mode, runs the linear traversal with a
863-
* fresh per-keypress cache, and calls preventDefault when focus actually moved.
864-
* Returns true when spotLinear successfully moved focus.
865-
*/
866-
/*
867-
* Unified Tab/Shift+Tab handler. All Tab-specific dispatch that previously lived inline in
868-
* onKeyDown is consolidated here so the 5-way path stays uncluttered.
869-
*
870-
* When shouldPreventNavigation() is true (paused or no containers), notifyKeyDown is called
871-
* without the pointer callback and navigation still runs. preventDefault is called when focus
872-
* moved OR when Spotlight is actively paused (modal open) to prevent the browser from
873-
* escaping the overlay even when no Spotlight target was found.
874-
*
875-
* When the pointer moved during this key press, notifyKeyDown already resets pointer mode so
876-
* navigation is skipped cleanly without an extra setPointerMode call.
781+
* Tab/Shift+Tab handler. Runs spotLinear with a per-keypress target cache and calls
782+
* preventDefault when focus moved or when paused (to keep Tab inside a modal overlay).
877783
*/
878784
function handleTab (evt) {
879785
const keyCode = evt.keyCode;
@@ -1429,23 +1335,16 @@ const Spotlight = (function () {
14291335

14301336
};
14311337

1432-
// Expose internals for unit tests without polluting the public Spotlight API.
14331338
_tabNavTestHooks = {
14341339
findLinearTabExitTarget,
14351340
getLinearTabSearchContainerId,
14361341
comesBeforeInTabOrder,
1437-
getPopupOwnerElement,
14381342
resolveTargetToOpenPopupItem,
14391343
isTargetInSelfOnlyContainer,
14401344
findLinearTabExitTargetInTargets,
1345+
getLinearTargetContainerId,
14411346
getLinearTargetsInContainer,
1442-
spotLinear,
1443-
onAcceleratedKeyDown,
1444-
onBlur,
1445-
onFocus,
1446-
handleWebOSMouseEvent,
1447-
handleKeyboardStateChangeEvent,
1448-
onKeyUp
1347+
spotLinear
14491348
};
14501349

14511350
return exports;

0 commit comments

Comments
 (0)