aria-voyager
Advanced tools
Comparing version 0.1.0 to 0.1.1
@@ -11,9 +11,2 @@ interface EmitterOptions<T> { | ||
interface FocusStrategy { | ||
activeItem?: Item; | ||
prevActiveItem?: Item; | ||
activateItem(item: Item): void; | ||
updateItems(): void; | ||
} | ||
type NavigationParameterBag = { | ||
@@ -31,2 +24,78 @@ event: Event; | ||
interface SelectionBehavior { | ||
/** | ||
* Selection behavior: | ||
* | ||
* - `automatic`: active item becomes selected item | ||
* - `manual`: user must select manually with spacebar | ||
* | ||
* @defaultValue `automatic` | ||
*/ | ||
singleSelection?: 'automatic' | 'manual'; | ||
} | ||
type EventHandler = (...args: unknown[]) => void; | ||
declare class SelectionStrategy implements NavigationPattern { | ||
#private; | ||
private control; | ||
eventListeners: EventNames[]; | ||
get selection(): Item[]; | ||
private shiftItem?; | ||
private behavior; | ||
constructor(control: Control, behavior?: SelectionBehavior); | ||
dispose(): void; | ||
addListener(event: 'read', handler: EventHandler): void; | ||
removeListener(event: 'read', handler: EventHandler): void; | ||
matches(event: Event): boolean; | ||
prepare(event: Event): void; | ||
handle(bag: NavigationParameterBag): NavigationParameterBag; | ||
select(selection: Item[]): void; | ||
readSelection(): void; | ||
private handleChange; | ||
private handleFocus; | ||
private handlePointer; | ||
private handleKeyboard; | ||
private handleItem; | ||
private handleKeys; | ||
/** | ||
* Handles special keyboard control cases, such as handling the spacebar key | ||
* and cmd/ctrl + a | ||
*/ | ||
private handleKeyCombinations; | ||
private deselect; | ||
private selectSingle; | ||
private selectAdd; | ||
private selectAll; | ||
private selectRange; | ||
private selectShift; | ||
private persistSelection; | ||
} | ||
interface FocusStrategy { | ||
activeItem?: Item; | ||
prevActiveItem?: Item; | ||
activateItem(item: Item): void; | ||
updateItems(): void; | ||
} | ||
/** | ||
* @see https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyboardnavigationinsidecomponents | ||
*/ | ||
declare abstract class AbstractFocusStrategy implements NavigationPattern, FocusStrategy { | ||
protected control: Control; | ||
private selectionStrategy?; | ||
eventListeners: EventNames[]; | ||
activeItem?: Item; | ||
prevActiveItem?: Item; | ||
get selection(): Item[]; | ||
constructor(control: Control, selectionStrategy?: SelectionStrategy | undefined); | ||
dispose(): void; | ||
readSelectionHandler(): void; | ||
hasFocus(): boolean; | ||
matches(): boolean; | ||
handle(bag: NavigationParameterBag): NavigationParameterBag; | ||
handleFocus(event: FocusEvent): void; | ||
activateSelection(): void; | ||
abstract activateItem(item: Item, forceFocus?: boolean): void; | ||
abstract updateItems(): void; | ||
} | ||
interface UpdateStrategy { | ||
@@ -55,6 +124,5 @@ setControl(control: Control): void; | ||
#private; | ||
protected abstract focusStrategy: FocusStrategy; | ||
protected abstract focusStrategy: AbstractFocusStrategy; | ||
items: Item[]; | ||
get enabledItems(): HTMLElement[]; | ||
abstract get selection(): Item[]; | ||
abstract get activeItem(): Item | undefined; | ||
@@ -83,11 +151,3 @@ abstract get prevActiveItem(): Item | undefined; | ||
declare class ActiveDescendentStrategy implements NavigationPattern, FocusStrategy { | ||
private control; | ||
eventListeners: EventNames[]; | ||
activeItem?: Item; | ||
prevActiveItem?: Item; | ||
constructor(control: Control); | ||
matches(): boolean; | ||
handle(bag: NavigationParameterBag): NavigationParameterBag; | ||
handleFocus(): void; | ||
declare class ActiveDescendentStrategy extends AbstractFocusStrategy { | ||
activateItem(item: Item): void; | ||
@@ -108,2 +168,3 @@ updateItems(): void; | ||
constructor(element: HTMLElement, options?: ListboxOptions); | ||
dispose(): void; | ||
readItems(): void; | ||
@@ -117,11 +178,4 @@ readSelection(): void; | ||
*/ | ||
declare class RovingTabindexStrategy implements NavigationPattern, FocusStrategy { | ||
private control; | ||
eventListeners: EventNames[]; | ||
activeItem?: Item; | ||
prevActiveItem?: Item; | ||
constructor(control: Control); | ||
matches(): boolean; | ||
handle(bag: NavigationParameterBag): NavigationParameterBag; | ||
activateItem(item: Item): void; | ||
declare class RovingTabindexStrategy extends AbstractFocusStrategy { | ||
activateItem(item: Item, forceFocus?: boolean): void; | ||
updateItems(): void; | ||
@@ -144,14 +198,2 @@ } | ||
interface SelectionBehavior { | ||
/** | ||
* Selection behavior: | ||
* | ||
* - `automatic`: active item becomes selected item | ||
* - `manual`: user must select manually with spacebar | ||
* | ||
* @defaultValue `automatic` | ||
*/ | ||
singleSelection?: 'automatic' | 'manual'; | ||
} | ||
type TablistBehavior = SelectionBehavior; | ||
@@ -170,2 +212,3 @@ interface TablistOptions { | ||
constructor(element: HTMLElement, options?: TablistOptions); | ||
dispose(): void; | ||
readItems(): void; | ||
@@ -172,0 +215,0 @@ readSelection(): void; |
@@ -137,2 +137,3 @@ // src/controls/-utils.ts | ||
this.emitter?.dispose?.(); | ||
this.focusStrategy.dispose(); | ||
const eventNames = new Set(this.navigationPatterns.map((p) => p.eventListeners ?? []).flat()); | ||
@@ -167,11 +168,32 @@ for (const eventName of eventNames) { | ||
import { v4 as uuidv4 } from "uuid"; | ||
var ActiveDescendentStrategy = class { | ||
constructor(control) { | ||
// src/navigation-patterns/focus-strategy.ts | ||
var AbstractFocusStrategy = class { | ||
constructor(control, selectionStrategy) { | ||
this.control = control; | ||
this.selectionStrategy = selectionStrategy; | ||
this.selectionStrategy?.addListener("read", this.readSelectionHandler.bind(this)); | ||
} | ||
eventListeners = ["focusin", "keydown", "pointerup"]; | ||
eventListeners = ["focus", "focusin"]; | ||
activeItem; | ||
prevActiveItem; | ||
get selection() { | ||
if (this.selectionStrategy) { | ||
return this.selectionStrategy.selection; | ||
} | ||
return []; | ||
} | ||
dispose() { | ||
this.selectionStrategy?.removeListener("read", this.readSelectionHandler.bind(this)); | ||
} | ||
readSelectionHandler() { | ||
if (!this.hasFocus() && this.selection.length > 0) { | ||
this.activateSelection(); | ||
} | ||
} | ||
hasFocus() { | ||
return this.control.element.contains(document.activeElement) || this.control.element === document.activeElement; | ||
} | ||
matches() { | ||
return this.control.items.length > 0; | ||
return this.control.enabledItems.length > 0; | ||
} | ||
@@ -181,18 +203,29 @@ handle(bag) { | ||
if (event.type === "focusin") { | ||
this.handleFocus(); | ||
this.handleFocus(event); | ||
return bag; | ||
} | ||
if (item) { | ||
this.activateItem(item); | ||
this.activateItem(item, event.type === "pointerover"); | ||
} | ||
return bag; | ||
} | ||
handleFocus() { | ||
const selectionPresent = this.control.selection.length > 0; | ||
if (selectionPresent) { | ||
this.activateItem(this.control.selection[0]); | ||
} else { | ||
this.activateItem(this.control.items[0]); | ||
handleFocus(event) { | ||
if (this.control.element === event.target) { | ||
const selectionPresent = this.selection.length > 0; | ||
if (selectionPresent) { | ||
this.activateSelection(); | ||
} else { | ||
this.activateItem(this.control.enabledItems[0]); | ||
} | ||
} else if (this.control.enabledItems.includes(event.target)) { | ||
this.activateItem(event.target); | ||
} | ||
} | ||
activateSelection() { | ||
this.activateItem(this.selection[0]); | ||
} | ||
}; | ||
// src/navigation-patterns/active-descendent-strategy.ts | ||
var ActiveDescendentStrategy = class extends AbstractFocusStrategy { | ||
activateItem(item) { | ||
@@ -412,2 +445,5 @@ if (item === this.activeItem) { | ||
} | ||
#listeners = { | ||
read: /* @__PURE__ */ new Set() | ||
}; | ||
eventListeners = ["focusin", "keydown", "keyup", "pointerup", "change"]; | ||
@@ -420,2 +456,11 @@ #selection = []; | ||
behavior; | ||
dispose() { | ||
Object.values(this.#listeners).forEach((listeners) => listeners.clear()); | ||
} | ||
addListener(event, handler) { | ||
this.#listeners[event].add(handler); | ||
} | ||
removeListener(event, handler) { | ||
this.#listeners[event].delete(handler); | ||
} | ||
matches(event) { | ||
@@ -450,2 +495,5 @@ return this.control.items.length > 0 && this.eventListeners.includes(event.type); | ||
]; | ||
for (const listener of this.#listeners.read) { | ||
listener(); | ||
} | ||
} | ||
@@ -458,3 +506,3 @@ handleChange() { | ||
const multiple = this.control.options.multiple; | ||
const selectionPresent = this.control.selection.length > 0; | ||
const selectionPresent = this.#selection.length > 0; | ||
if (this.control.capabilities.singleSelection && !multiple && !selectionPresent) { | ||
@@ -468,3 +516,3 @@ this.selectSingle(this.control.items[0]); | ||
} else if (event.metaKey) { | ||
if (this.control.selection.includes(item)) { | ||
if (this.#selection.includes(item)) { | ||
this.deselect(item); | ||
@@ -512,3 +560,3 @@ } else { | ||
if (event.key === " " && this.control.activeItem && this.control.options.multiple) { | ||
if (this.control.selection.includes(this.control.activeItem)) { | ||
if (this.#selection.includes(this.control.activeItem)) { | ||
this.deselect(this.control.activeItem); | ||
@@ -526,4 +574,4 @@ } else { | ||
deselect(item) { | ||
if (this.control.selection.includes(item)) { | ||
const selection = this.control.selection.slice(); | ||
if (this.#selection.includes(item)) { | ||
const selection = this.#selection.slice(); | ||
selection.splice(selection.indexOf(item), 1); | ||
@@ -538,3 +586,3 @@ this.persistSelection(selection); | ||
selectAdd(item) { | ||
const selection = this.control.options.multiple ? this.control.selection.slice() : []; | ||
const selection = this.control.options.multiple ? this.#selection.slice() : []; | ||
selection.push(item); | ||
@@ -588,3 +636,6 @@ this.shiftItem = item; | ||
#selectionStrategy = new SelectionStrategy(this); | ||
focusStrategy = new ActiveDescendentStrategy(this); | ||
focusStrategy = new ActiveDescendentStrategy( | ||
this, | ||
this.#selectionStrategy | ||
); | ||
get selection() { | ||
@@ -625,2 +676,6 @@ return this.#selectionStrategy.selection; | ||
} | ||
dispose() { | ||
super.dispose(); | ||
this.#selectionStrategy.dispose(); | ||
} | ||
readItems() { | ||
@@ -774,35 +829,15 @@ this.items = [...this.element.querySelectorAll('[role="option"]')]; | ||
// src/navigation-patterns/roving-tabindex-strategy.ts | ||
var RovingTabindexStrategy = class { | ||
constructor(control) { | ||
this.control = control; | ||
} | ||
eventListeners = ["focus", "focusin"]; | ||
activeItem; | ||
prevActiveItem; | ||
matches() { | ||
return this.control.enabledItems.length > 0; | ||
} | ||
handle(bag) { | ||
const { event, item } = bag; | ||
if (event.type === "focusin" && !this.activeItem) { | ||
if (this.control.element === event.target) { | ||
this.activateItem(this.control.enabledItems[0]); | ||
} else if (this.control.enabledItems.includes(event.target)) { | ||
this.activateItem(event.target); | ||
var RovingTabindexStrategy = class extends AbstractFocusStrategy { | ||
activateItem(item, forceFocus = false) { | ||
if (item !== this.activeItem) { | ||
item.setAttribute("tabindex", "0"); | ||
if (this.activeItem) { | ||
this.prevActiveItem = this.activeItem; | ||
this.prevActiveItem.setAttribute("tabindex", "-1"); | ||
} | ||
return bag; | ||
} | ||
if (item) { | ||
this.activateItem(item); | ||
if (this.hasFocus() || forceFocus) { | ||
item.focus(); | ||
} | ||
return bag; | ||
} | ||
activateItem(item) { | ||
if (item !== this.activeItem) { | ||
item.setAttribute("tabindex", "0"); | ||
this.prevActiveItem = this.activeItem; | ||
this.control.prevActiveItem?.setAttribute("tabindex", "-1"); | ||
} | ||
item.focus(); | ||
if (item !== this.activeItem) { | ||
this.activeItem = item; | ||
@@ -885,3 +920,3 @@ this.control.emitter?.itemActivated(item); | ||
#selectionStrategy; | ||
focusStrategy = new RovingTabindexStrategy(this); | ||
focusStrategy; | ||
#nextNavigation = new NextNavigation(this, "ArrowRight"); | ||
@@ -908,2 +943,3 @@ #prevNavigation = new PreviousNavigation(this, "ArrowLeft"); | ||
this.#selectionStrategy = new SelectionStrategy(this, options?.behavior ?? {}); | ||
this.focusStrategy = new RovingTabindexStrategy(this, this.#selectionStrategy); | ||
this.registerNavigationPatterns([ | ||
@@ -922,2 +958,6 @@ this.#nextNavigation, | ||
} | ||
dispose() { | ||
super.dispose(); | ||
this.#selectionStrategy.dispose(); | ||
} | ||
readItems() { | ||
@@ -924,0 +964,0 @@ this.items = [...this.element.querySelectorAll('[role="tab"]')]; |
{ | ||
"name": "aria-voyager", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "A framework agnostic / universal package that implements navigation patterns for various aria roles and features", | ||
@@ -32,3 +32,3 @@ "author": "gossi", | ||
"@swc/cli": "0.5.2", | ||
"@swc/core": "1.10.0", | ||
"@swc/core": "1.10.1", | ||
"@testing-library/dom": "10.4.0", | ||
@@ -44,3 +44,3 @@ "@types/css-modules": "1.0.5", | ||
"eslint": "8.57.1", | ||
"playwright": "^1.49.0", | ||
"playwright": "^1.49.1", | ||
"prettier": "3.4.2", | ||
@@ -51,3 +51,3 @@ "tsup": "8.3.5", | ||
"vitest": "^2.1.8", | ||
"webdriverio": "^9.4.1" | ||
"webdriverio": "^9.4.2" | ||
}, | ||
@@ -54,0 +54,0 @@ "engines": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
87407
2318