Skip to content

Commit

Permalink
Add focus manager to behaviors
Browse files Browse the repository at this point in the history
  • Loading branch information
edoardocavazza committed Nov 8, 2023
1 parent 44685d8 commit 858450c
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 131 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-crabs-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chialab/loock': minor
---

Add focus manager to behaviors.
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,11 @@ Should restore the focus to the previously focused element when the context is e

Default: `true`.

#### `trap`

Should trap the focus inside the context when the context is active.

Default: `true`.

#### `focusContainer`

Focus the context container when the context is entered.

Default: `true`.
Default: `false`.

#### `onEnter`

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"scripts": {
"dev": "vite",
"build": "rimraf 'dist' 'types' && yarn types && yarn build:esm && yarn build:cjs",
"build:esm": "esbuild src/index.ts --outfile=dist/esm/loock.js --format=esm --bundle --minify --sourcemap",
"build:cjs": "esbuild src/index.ts --outfile=dist/cjs/loock.cjs --format=cjs --bundle --minify --sourcemap",
"build:esm": "esbuild src/index.ts --outfile=dist/esm/loock.js --format=esm --bundle --sourcemap",
"build:cjs": "esbuild src/index.ts --outfile=dist/cjs/loock.cjs --format=cjs --bundle --sourcemap",
"types": "tsc --declaration --emitDeclarationOnly --declarationDir 'types' --incremental false",
"test": "playwright test",
"lint": "prettier --check . && eslint src",
Expand Down
7 changes: 6 additions & 1 deletion src/focusEnterBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { focusManager, type FocusManagerOptions } from './focusManager';

/**
* The focus enter options.
*/
export interface FocusEnterOptions {
export interface FocusEnterOptions extends FocusManagerOptions {
/**
* The callback when focus enter.
*/
Expand All @@ -24,6 +26,8 @@ export function focusEnterBehavior(node: HTMLElement, options: FocusEnterOptions
let focused = false;
let connected = false;

const manager = focusManager(node, options);

const onFocusIn = () => {
const activeElement = document.activeElement;
if (focused || !activeElement) {
Expand Down Expand Up @@ -73,5 +77,6 @@ export function focusEnterBehavior(node: HTMLElement, options: FocusEnterOptions
node.removeEventListener('focusin', onFocusIn);
node.removeEventListener('focusout', onFocusOut);
},
manager,
};
}
48 changes: 23 additions & 25 deletions src/focusFirstChildBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
import { findFocusableByOptions } from './findFocusableChildren';
import { focusEnterBehavior } from './focusEnterBehavior';
import { type FocusManagerOptions } from './focusManager';
import { restoreAttribute } from './helpers';

/**
* The options for focus first option on focus enter.
*/
export interface FocusFirstChildOptions {
/**
* The focusable elements.
*/
elements?: HTMLElement[] | (() => HTMLElement[]);
/**
* The selectors for focusable nodes.
*/
include?: string[];
/**
* The selectors for ignored nodes.
*/
exclude?: string[];
}

/**
* Focus first option on focus enter.
* @param node The target element.
* @param options The options.
* @returns The behavior controller.
*/
export function focusFirstChildBehavior(node: HTMLElement, options: FocusFirstChildOptions = {}) {
export function focusFirstChildBehavior(node: HTMLElement, options: FocusManagerOptions = {}) {
const document = node.ownerDocument;
let activeElement: HTMLElement | null = null;
let connected = false;
let tabindex: string | null = null;
let tabIndex: string | null = null;

const enterBehavior = focusEnterBehavior(node, {
...options,
onEnter() {
tabIndex = node.getAttribute('tabindex');
node.setAttribute('tabindex', '-1');
},
onExit() {
restoreAttribute(node, 'tabindex', tabIndex);
},
});
const { manager } = enterBehavior;

const onFocus = () => {
const elements = findFocusableByOptions(node, options);
const elements = manager.findFocusable();
const target = document.activeElement as HTMLElement;
if (target === node) {
if (activeElement && node.contains(activeElement)) {
Expand All @@ -55,20 +50,23 @@ export function focusFirstChildBehavior(node: HTMLElement, options: FocusFirstCh
if (connected) {
return;
}
enterBehavior.connect();
connected = true;
activeElement = null;
tabindex = node.getAttribute('tabindex');
node.setAttribute('tabindex', '-1');
if (document.activeElement && node.contains(document.activeElement)) {
onFocus();
}
node.addEventListener('focus', onFocus, true);
},
disconnect() {
if (!connected) {
return;
}
enterBehavior.disconnect();
connected = false;
activeElement = null;
restoreAttribute(node, 'tabindex', tabindex);
node.removeEventListener('focus', onFocus, true);
},
manager,
};
}
38 changes: 38 additions & 0 deletions src/focusManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { findFocusableByOptions } from './findFocusableChildren';

export interface FocusManagerOptions {
/**
* The focusable elements.
*/
elements?: HTMLElement[] | (() => HTMLElement[]);
/**
* The selectors for focusable nodes.
*/
include?: string[];
/**
* The selectors for ignored nodes.
*/
exclude?: string[];
}

export function focusManager(node: HTMLElement, options: FocusManagerOptions = {}) {
return {
findFocusable() {
return findFocusableByOptions(node, options);
},

/**
* Focus the first focusable child.
*/
focusFirst() {
this.findFocusable().shift()?.focus();
},

/**
* Focus the last focusable child.
*/
focusLast() {
this.findFocusable().pop()?.focus();
},
};
}
Loading

0 comments on commit 858450c

Please sign in to comment.