-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
232e351
commit 99940f5
Showing
10 changed files
with
753 additions
and
483 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@chialab/loock': major | ||
--- | ||
|
||
Introduce focus and keyboard navigation behaviors. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/** | ||
* Default focusable selectors. | ||
*/ | ||
export const DEFAULT_SELECTORS = [ | ||
'a[href]', | ||
'area[href]', | ||
'button', | ||
'input', | ||
'select', | ||
'textarea', | ||
'video[controls]', | ||
'audio[controls]', | ||
'embed', | ||
'iframe', | ||
'summary', | ||
'[contenteditable]', | ||
'[tabindex]', | ||
]; | ||
|
||
/** | ||
* Default ignore selectors. | ||
*/ | ||
export const DEFAULT_IGNORE_SELECTORS = [ | ||
'[tabindex="-1"]', | ||
'[disabled]', | ||
'[hidden]', | ||
'[aria-hidden="true"]', | ||
'[aria-disabled="true"]', | ||
'[inert]', | ||
'details:not([open]) *:not(summary)', | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { DEFAULT_IGNORE_SELECTORS, DEFAULT_SELECTORS } from './constants'; | ||
|
||
/** | ||
* Find all focusable children of a node. | ||
* @param node The target node. | ||
* @param include The selectors to include. | ||
* @param exclude The selectors to exclude. | ||
* @returns The focusable children list. | ||
*/ | ||
export function findFocusableChildren( | ||
node: Element, | ||
include: string[] = DEFAULT_SELECTORS, | ||
exclude: string[] = DEFAULT_IGNORE_SELECTORS | ||
) { | ||
return (Array.from(node.querySelectorAll(include.join(', '))) as HTMLElement[]).filter((element) => { | ||
if (exclude.some((selector) => element.matches(selector))) { | ||
return false; | ||
} | ||
|
||
if (element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'radio') { | ||
const name = (element as HTMLInputElement).name; | ||
const inputs = node.querySelectorAll(`input[type="radio"][name="${name}"]`); | ||
const checked = Array.from(inputs).find((input) => (input as HTMLInputElement).checked); | ||
if (checked) { | ||
if (checked !== element) { | ||
return false; | ||
} | ||
} else if (element !== inputs[0]) { | ||
return false; | ||
} | ||
} | ||
|
||
const { width, height } = element.getBoundingClientRect(); | ||
return !!height && !!width; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/** | ||
* The focus enter options. | ||
*/ | ||
export interface FocusEnterOptions { | ||
/** | ||
* The callback when focus enter. | ||
*/ | ||
onEnter: (element: Element) => void; | ||
/** | ||
* The callback when focus exit. | ||
*/ | ||
onExit: () => void; | ||
} | ||
|
||
/** | ||
* Focus first option on focus enter. | ||
* @param element The target element. | ||
* @param options The options. | ||
* @returns The behavior controller. | ||
*/ | ||
export function focusEnterBehavior(element: HTMLElement, options: FocusEnterOptions) { | ||
const document = element.ownerDocument; | ||
const { onEnter, onExit } = options; | ||
let focused = false; | ||
let connected = false; | ||
|
||
const onFocusIn = () => { | ||
const activeElement = document.activeElement; | ||
if (focused || !activeElement) { | ||
return; | ||
} | ||
|
||
focused = true; | ||
onEnter(activeElement); | ||
}; | ||
|
||
const onFocusOut = () => { | ||
if (!focused) { | ||
return; | ||
} | ||
|
||
setTimeout(() => { | ||
const activeElement = document.activeElement; | ||
if (element !== activeElement && !element.contains(activeElement)) { | ||
focused = false; | ||
onExit(); | ||
} | ||
}); | ||
}; | ||
|
||
return { | ||
get connected() { | ||
return connected; | ||
}, | ||
connect() { | ||
if (connected) { | ||
return; | ||
} | ||
connected = true; | ||
focused = false; | ||
element.addEventListener('focusin', onFocusIn); | ||
element.addEventListener('focusout', onFocusOut); | ||
}, | ||
disconnect() { | ||
if (!connected) { | ||
return; | ||
} | ||
connected = false; | ||
focused = false; | ||
element.removeEventListener('focusin', onFocusIn); | ||
element.removeEventListener('focusout', onFocusOut); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { findFocusableChildren } from './findFocusableChildren'; | ||
|
||
/** | ||
* The options for focus first option on focus enter. | ||
*/ | ||
export interface FocusFirstChildOptions { | ||
/** | ||
* The focusable elements. | ||
*/ | ||
elements?: HTMLElement[]; | ||
/** | ||
* The selectors for focusable nodes. | ||
*/ | ||
include?: string[]; | ||
/** | ||
* The selectors for ignored nodes. | ||
*/ | ||
exclude?: string[]; | ||
} | ||
|
||
/** | ||
* Focus first option on focus enter. | ||
* @param element The target element. | ||
* @param options The options. | ||
* @returns The behavior controller. | ||
*/ | ||
export function focusFirstChildBehavior(element: HTMLElement, options: FocusFirstChildOptions = {}) { | ||
const document = element.ownerDocument; | ||
let activeElement: HTMLElement | null = null; | ||
let connected = false; | ||
|
||
const onFocus = () => { | ||
const { include, exclude, elements = findFocusableChildren(element, include, exclude) } = options; | ||
const target = document.activeElement as HTMLElement; | ||
if (target === element) { | ||
if (activeElement && element.contains(activeElement)) { | ||
activeElement.focus(); | ||
return; | ||
} | ||
|
||
elements[0]?.focus(); | ||
return; | ||
} else if (elements.includes(target)) { | ||
activeElement = target; | ||
} | ||
}; | ||
|
||
return { | ||
get connected() { | ||
return connected; | ||
}, | ||
connect() { | ||
if (connected) { | ||
return; | ||
} | ||
connected = true; | ||
activeElement = null; | ||
element.addEventListener('focus', onFocus, true); | ||
}, | ||
disconnect() { | ||
if (!connected) { | ||
return; | ||
} | ||
connected = false; | ||
activeElement = null; | ||
element.removeEventListener('focus', onFocus, true); | ||
}, | ||
}; | ||
} |
Oops, something went wrong.