Skip to content

Commit 737d7b3

Browse files
committed
Rename and simplify focus manager methods
- rename getRightValidPreviousElement to getValidPreviousElement and use a clearer variable name (previousElement) where it's used to restore focus simplifies history manipulation and improves readability. - rename fireOnActiveForHierarchy to triggerOnActiveCallbacks for a more descriptive name. - extract findFocusableChild to centralize logic that prefers a cached or preferred child when activating a container; try preferred child first, then other children, returning the first successfully activated child. - change setActive to return the activated element (or undefined) and simplify early returns; centralize behavior: - validate element and metadata early, log a warning if metadata is missing - attempt to find/focus a child via findFocusableChild before acting on the element itself - consistently trigger onActive(false) for previous element, add it to history, set _currentElement, cache active index, trigger onActive(true) - update cursor only once with the resolved target - rename findNext to findNextByType to clarify purpose and update the recursive call accordingly. - rename getPreferredChildFromContainer to getPreferredChild and adjust signature to align with usage. These changes improve naming clarity, reduce duplicated logic, and make focus activation flow easier to follow and more reliable when containers should forward focus to children.
1 parent 4476f23 commit 737d7b3

File tree

2 files changed

+55
-56
lines changed

2 files changed

+55
-56
lines changed

packages/ui/src/lib/components/file/FileViewHeader.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import ExecutableLabel from '$components/file/ExecutableLabel.svelte';
77
import FileName from '$components/file/FileName.svelte';
88
import FileStatusBadge from '$components/file/FileStatusBadge.svelte';
9-
import { focusable } from '$lib/focus/focusable';
109
import type { FileStatus } from '$components/file/types';
1110
1211
interface Props {
@@ -56,7 +55,6 @@
5655
oncontextmenu(e);
5756
}
5857
}}
59-
use:focusable
6058
>
6159
{#if draggable}
6260
<div class="file-header__drag-handle">

packages/ui/src/lib/focus/focusManager.ts

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class FocusManager {
118118
}
119119
}
120120

121-
private getRightValidPreviousElement(): HTMLElement | undefined {
121+
private getValidPreviousElement(): HTMLElement | undefined {
122122
// Find the first connected element in the history
123123
for (let i = 0; i < this._previousElements.length; i++) {
124124
const element = this._previousElements[i];
@@ -386,11 +386,11 @@ export class FocusManager {
386386

387387
// Clear current if it matches
388388
if (this._currentElement === element) {
389-
const rightElement = this.getRightValidPreviousElement();
390-
if (rightElement) {
391-
this._currentElement = rightElement;
389+
const previousElement = this.getValidPreviousElement();
390+
if (previousElement) {
391+
this._currentElement = previousElement;
392392
// Remove the selected element from history since it's now current
393-
removeFromArray(this._previousElements, rightElement);
393+
removeFromArray(this._previousElements, previousElement);
394394
} else {
395395
this._currentElement = undefined;
396396
}
@@ -401,7 +401,7 @@ export class FocusManager {
401401
/**
402402
* Fires onActive callbacks for an element and all its parent focusables
403403
*/
404-
private fireOnActiveForHierarchy(element: HTMLElement, active: boolean): void {
404+
private triggerOnActiveCallbacks(element: HTMLElement, active: boolean): void {
405405
let metadata = this.getMetadata(element);
406406
while (metadata) {
407407
try {
@@ -415,6 +415,23 @@ export class FocusManager {
415415
}
416416
}
417417

418+
private findFocusableChild(element: HTMLElement): HTMLElement | undefined {
419+
const metadata = this.getMetadata(element);
420+
if (!metadata?.children.length) return undefined;
421+
422+
// Try preferred child first, then iterate through all children
423+
const childrenToTry = [this.getPreferredChild(element), ...metadata.children].filter(
424+
(child, index, arr) => child && arr.indexOf(child) === index
425+
); // Remove duplicates
426+
427+
for (const child of childrenToTry) {
428+
const activated = this.setActive(child!);
429+
if (activated) return activated;
430+
}
431+
432+
return undefined;
433+
}
434+
418435
/**
419436
* Sets the specified element as the currently active (focused) element.
420437
* If the element has skip=true, it will try to focus the preferred child instead.
@@ -423,44 +440,29 @@ export class FocusManager {
423440
* for all parent focusables. Updates focus history and caches the active index
424441
* for list focusables.
425442
*/
426-
setActive(element: HTMLElement) {
427-
if (!element || !element.isConnected || !this.isElementRegistered(element)) {
428-
return;
429-
}
443+
setActive(element: HTMLElement): HTMLElement | undefined {
444+
if (!element?.isConnected || !this.isElementRegistered(element)) return undefined;
430445

431446
const metadata = this.getMetadata(element);
432-
433-
// If element should be skipped, try to focus preferred child (cached or first) instead
434-
if (metadata?.options.skip) {
435-
const preferredChild = this.getPreferredChildFromContainer(element);
436-
if (preferredChild) {
437-
this.setActive(preferredChild);
438-
return;
439-
}
440-
// If no children, don't set this element as active
441-
return;
447+
if (!metadata) {
448+
console.warn('Could not find metadata', element);
449+
return undefined;
442450
}
443451

452+
const targetElement = this.findFocusableChild(element) || element;
444453
const previousElement = this._currentElement;
445454

446-
// Fire onActive(false) for previous element and all its parents
447455
if (previousElement) {
448-
this.fireOnActiveForHierarchy(previousElement, false);
456+
this.triggerOnActiveCallbacks(previousElement, false);
457+
this.addToPreviousElements(previousElement);
449458
}
450459

451-
// Cache the active child index for all parent elements
452-
this.cacheActiveIndexForAllParents(element);
453-
454-
// Add current element to history before changing
455-
if (this._currentElement) {
456-
this.addToPreviousElements(this._currentElement);
457-
}
458-
this._currentElement = element;
460+
this._currentElement = targetElement;
461+
this.cacheActiveIndex(targetElement);
462+
this.triggerOnActiveCallbacks(targetElement, true);
463+
this.cursor.set(targetElement);
459464

460-
// Fire onActive(true) for new element and all its parents
461-
this.fireOnActiveForHierarchy(element, true);
462-
463-
this.cursor.set(element);
465+
return targetElement;
464466
}
465467

466468
/**
@@ -473,7 +475,7 @@ export class FocusManager {
473475
* @param forward - Whether to search forward or backward through siblings
474476
* @returns The right matching element or undefined if none found
475477
*/
476-
private findNext(
478+
private findNextByType(
477479
searchElement: HTMLElement,
478480
searchTypes: DefinedFocusable | DefinedFocusable[],
479481
forward: boolean
@@ -495,7 +497,7 @@ export class FocusManager {
495497
}
496498

497499
if (!currentMeta?.parentElement) return undefined;
498-
return this.findNext(currentMeta.parentElement, searchTypes, forward);
500+
return this.findNextByType(currentMeta.parentElement, searchTypes, forward);
499501
}
500502

501503
/**
@@ -557,7 +559,7 @@ export class FocusManager {
557559
* @param containerElement - The container element to get a child from
558560
* @returns The preferred child element to focus, or undefined if no children
559561
*/
560-
private getPreferredChildFromContainer(containerElement: HTMLElement): HTMLElement | undefined {
562+
private getPreferredChild(containerElement: HTMLElement): HTMLElement | undefined {
561563
const containerMetadata = this.getMetadata(containerElement);
562564
if (!containerMetadata?.children.length) {
563565
return undefined;
@@ -595,7 +597,7 @@ export class FocusManager {
595597
visited.add(current);
596598

597599
// Get the preferred child from current container
598-
const preferredChild = this.getPreferredChildFromContainer(current);
600+
const preferredChild = this.getPreferredChild(current);
599601
if (!preferredChild) {
600602
// No children available, return current container
601603
return current;
@@ -626,10 +628,7 @@ export class FocusManager {
626628
* @param forward - Whether to search forward (next) or backward (previous) through siblings
627629
* @returns The next non-skipped focusable element or undefined if none found
628630
*/
629-
private findNextNonSkippedInList(
630-
searchElement: HTMLElement,
631-
forward: boolean
632-
): HTMLElement | undefined {
631+
private findNextInList(searchElement: HTMLElement, forward: boolean): HTMLElement | undefined {
633632
const currentMeta = this.getMetadata(searchElement);
634633
const parentMeta = this.getParentMetadata(currentMeta);
635634
if (!currentMeta || !parentMeta || !this._currentElement) return undefined;
@@ -654,7 +653,7 @@ export class FocusManager {
654653

655654
// Only continue traversing up if the parent has list: true
656655
if (parentMeta.options.list && currentMeta.parentElement) {
657-
return this.findNextNonSkippedInList(currentMeta.parentElement, forward);
656+
return this.findNextInList(currentMeta.parentElement, forward);
658657
}
659658

660659
return undefined;
@@ -702,8 +701,10 @@ export class FocusManager {
702701

703702
private activateAndFocus(element: HTMLElement | undefined): boolean {
704703
if (!element) return false;
705-
this.setActive(element);
706-
this.focusElement(element);
704+
const activatedElement = this.setActive(element);
705+
if (activatedElement) {
706+
this.focusElement(activatedElement);
707+
}
707708
return true;
708709
}
709710

@@ -761,7 +762,7 @@ export class FocusManager {
761762
const metadata = this.getCurrentMetadata();
762763
if (!metadata?.parentElement || !this._currentElement) return false;
763764

764-
const cousinTarget = this.findNext(
765+
const cousinTarget = this.findNextByType(
765766
this._currentElement,
766767
metadata.logicalId as DefinedFocusable,
767768
forward
@@ -781,7 +782,7 @@ export class FocusManager {
781782
if (!metadata?.parentElement || !this._currentElement) return false;
782783

783784
// Find the next non-skipped focusable, traversing parents while list: true
784-
const linkedTarget = this.findNextNonSkippedInList(this._currentElement, forward);
785+
const linkedTarget = this.findNextInList(this._currentElement, forward);
785786
if (!linkedTarget) return false;
786787

787788
return this.activateAndFocus(linkedTarget);
@@ -795,7 +796,7 @@ export class FocusManager {
795796
if (!linkToIds || linkToIds.length === 0) return false;
796797

797798
// Find the nearest element of any of the target types
798-
const linkedTarget = this.findNext(this._currentElement, linkToIds, forward);
799+
const linkedTarget = this.findNextByType(this._currentElement, linkToIds, forward);
799800
return this.activateAndFocus(linkedTarget);
800801
}
801802

@@ -875,14 +876,14 @@ export class FocusManager {
875876
if (!metadata || !this._currentElement) return false;
876877

877878
// Get the preferred child (cached or first child)
878-
const preferredChild = this.getPreferredChildFromContainer(this._currentElement);
879+
const preferredChild = this.getPreferredChild(this._currentElement);
879880
if (!preferredChild) return false;
880881

881882
const childMetadata = this.getMetadata(preferredChild);
882883

883884
// If the preferred child should be skipped, try to focus its preferred child (cached or first)
884885
if (childMetadata?.options.skip) {
885-
const preferredGrandChild = this.getPreferredChildFromContainer(preferredChild);
886+
const preferredGrandChild = this.getPreferredChild(preferredChild);
886887
if (preferredGrandChild) {
887888
return this.activateAndFocus(preferredGrandChild);
888889
}
@@ -1140,7 +1141,7 @@ export class FocusManager {
11401141
*
11411142
* @param element - The currently active element
11421143
*/
1143-
private cacheActiveIndexForAllParents(element: HTMLElement) {
1144+
private cacheActiveIndex(element: HTMLElement) {
11441145
let currentElement = element;
11451146
const metadata = this.getMetadata(currentElement);
11461147

@@ -1277,7 +1278,7 @@ export class FocusManager {
12771278

12781279
// Check each child of the non-list parent to see which one contains our current element
12791280
for (const child of parentMetadata.children) {
1280-
if (this.elementContains(child, currentElement)) {
1281+
if (this.containsElement(child, currentElement)) {
12811282
return child;
12821283
}
12831284
}
@@ -1292,15 +1293,15 @@ export class FocusManager {
12921293
* @param target - The target element to find
12931294
* @returns True if container contains target
12941295
*/
1295-
private elementContains(container: HTMLElement, target: HTMLElement): boolean {
1296+
private containsElement(container: HTMLElement, target: HTMLElement): boolean {
12961297
if (container === target) return true;
12971298

12981299
const containerMetadata = this.getMetadata(container);
12991300
if (!containerMetadata) return false;
13001301

13011302
// Recursively check children
13021303
for (const child of containerMetadata.children) {
1303-
if (this.elementContains(child, target)) {
1304+
if (this.containsElement(child, target)) {
13041305
return true;
13051306
}
13061307
}

0 commit comments

Comments
 (0)