Skip to content

Commit

Permalink
Add preventScroll option
Browse files Browse the repository at this point in the history
  • Loading branch information
colebemis committed Oct 20, 2022
1 parent 1413e2a commit cd6d27d
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 6 deletions.
3 changes: 2 additions & 1 deletion docs/focus-zone.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ The `focusZone` function takes the following arguments.
| abortSignal | `AbortSignal` | | If passed, the focus zone will be deactivated and all event listeners removed when this signal is aborted. If not passed, an `AbortSignal` will be returned by the `focusZone` function. |
| activeDescendantControl | `HTMLElement` | | If `activeDescendantControl` is supplied, do not move focus or alter `tabindex` on any element. Instead, manage `aria-activedescendant` according to the [ARIA best practices guidelines](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant).<br /><br />The given `activeDescendantControl` will be given an `aria-controls` attribute that references the ID of the `container`. Additionally, it will be given an `aria-activedescendant` attribute that references the ID of the currently-active descendant.<br /><br />This element will retain DOM focus as arrow keys are pressed. |
| onActiveDescendantChanged | `Function` | | This function is called each time the active descendant changes (only applicable if `activeDescendantControl` is given). The function takes two arguments: `newActiveDescendant` and `previousActiveDescendant`, both `HTMLElement`, either of which can be undefined (e.g. when an element in the container first becomes active, or when the controlling element becomes unfocused). |
| preventScroll | `boolean` | `false` | A boolean value indicating whether or not the browser should scroll the document to bring the newly-focused element into view. A value of `false` for `preventScroll` (the default) means that the browser will scroll the element into view after focusing it. If `preventScroll` is set to `true`, no scrolling will occur. |

## Best practices

We highly recommend reading [Section 6: Developing a Keyboard Interface](https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard) from the WAI-ARIA Authoring Practices document. |
We highly recommend reading [Section 6: Developing a Keyboard Interface](https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard) from the WAI-ARIA Authoring Practices document. |
20 changes: 15 additions & 5 deletions src/focus-zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@ export interface FocusZoneSettings {
* For more information, @see https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_general_within
*/
focusInStrategy?: 'first' | 'closest' | 'previous' | ((previousFocusedElement: Element) => HTMLElement | undefined)

/**
* A boolean value indicating whether or not the browser should scroll the
* document to bring the newly-focused element into view. A value of `false`
* for `preventScroll` (the default) means that the browser will scroll the
* element into view after focusing it. If `preventScroll` is set to `true`,
* no scrolling will occur.
*/
preventScroll?: boolean
}

function getDirection(keyboardEvent: KeyboardEvent) {
Expand Down Expand Up @@ -345,6 +354,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
const activeDescendantControl = settings?.activeDescendantControl
const activeDescendantCallback = settings?.onActiveDescendantChanged
let currentFocusedElement: HTMLElement | undefined
const preventScroll = settings?.preventScroll ?? false

function getFirstFocusableElement() {
return focusableElements[0] as HTMLElement | undefined
Expand Down Expand Up @@ -516,7 +526,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
container.addEventListener('focusin', event => {
if (event.target instanceof HTMLElement && focusableElements.includes(event.target)) {
// Move focus to the activeDescendantControl if one of the descendants is focused
activeDescendantControl.focus()
activeDescendantControl.focus({preventScroll})
updateFocusedElement(event.target)
}
})
Expand Down Expand Up @@ -576,7 +586,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
// be undefined.
const targetElementIndex = lastKeyboardFocusDirection === 'previous' ? focusableElements.length - 1 : 0
const targetElement = focusableElements[targetElementIndex] as HTMLElement | undefined
targetElement?.focus()
targetElement?.focus({preventScroll})
return
} else {
updateFocusedElement(event.target)
Expand All @@ -586,10 +596,10 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
const elementToFocus = focusInStrategy(event.relatedTarget)
const requestedFocusElementIndex = elementToFocus ? focusableElements.indexOf(elementToFocus) : -1
if (requestedFocusElementIndex >= 0 && elementToFocus instanceof HTMLElement) {
// Since we are calling focus() this handler will run again synchronously. Therefore,
// Since we are calling focus({preventScroll}) this handler will run again synchronously. Therefore,
// we don't want to let this invocation finish since it will clobber the value of
// currentFocusedElement.
elementToFocus.focus()
elementToFocus.focus({preventScroll})
return
} else {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -696,7 +706,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
lastKeyboardFocusDirection = direction

// updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
nextElementToFocus.focus()
nextElementToFocus.focus({preventScroll})
}
// Tab should always allow escaping from this container, so only
// preventDefault if tab key press already resulted in a focus movement
Expand Down

0 comments on commit cd6d27d

Please sign in to comment.