Skip to content

Commit

Permalink
feat(utils): Implemented new keyboard focus behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Jan 30, 2022
1 parent ac60bdb commit 77f0d01
Show file tree
Hide file tree
Showing 17 changed files with 2,426 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./Dir";
export * from "./events";
export * from "./getPercentage";
export * from "./hover";
export * from "./keyboardMovement";
export * from "./layout";
export * from "./loop";
export * from "./mode";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { ReactElement, ReactNode, useMemo } from "react";
import {
ActiveDescendantContext,
ActiveDescendantContextProvider,
} from "./activeDescendantContext";

/**
* @internal
* @remarks \@since 5.0.0
*/
export interface ActiveDescendantMovementProviderProps
extends ActiveDescendantContext {
children: ReactNode;
}

/**
* This component should be used with the {@link KeyboardMovementProvider}
* component to implement custom keyboard focusable behavior using
* `aria-activedescendant`.
*
* @example
* Base Example
* ```tsx
* function Descendant({ id, children, ...props }: HTMLAttributes<HTMLDivElement>): ReactElement {
* const { ref, active } = useActiveDescendant({ id });
* return (
* <div
* {...props}
* id={id}
* ref={ref}
* role="option"
* tabIndex={-1}
* className={active ? "active" : undefined}
* >
* {children}
* </div>
* );
* }
*
* function CustomFocus(): ReactElement {
* const { providerProps, focusIndex, ...containerProps } =
* useActiveDescendantFocus()
*
* return (
* <ActiveDescendantMovementProvider>
* <div
* {...containerProps}
* id="some-unique-id"
* role="listbox"
* tabIndex={0}
* >
* <Descendant id="some-descendant-id">
* Some Option
* </Descendant>
* </div>
* </ActiveDescendantMovementProvider>
* );
* }
*
* function Example() {
* return (
* <KeyboardMovementProvider loopable searchable>
* <CustomFocus />
* </KeyboardMovementProvider>
* );
* }
* ```
*
* @see https://www.w3.org/TR/wai-aria-practices/#kbd_focus_activedescendant
* @internal
* @remarks \@since 5.0.0
*/
export function ActiveDescendantMovementProvider({
children,
activeId,
setActiveId,
}: ActiveDescendantMovementProviderProps): ReactElement {
return (
<ActiveDescendantContextProvider
value={useMemo(
() => ({
activeId,
setActiveId,
}),
[activeId, setActiveId]
)}
>
{children}
</ActiveDescendantContextProvider>
);
}
105 changes: 105 additions & 0 deletions packages/utils/src/keyboardMovement/KeyboardMovementProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { ReactElement, ReactNode, useMemo, useRef } from "react";

import {
DEFAULT_KEYBOARD_MOVEMENT,
KeyboardMovementContextProvider,
} from "./movementContext";
import type {
KeyboardFocusContext,
KeyboardFocusElementData,
KeyboardMovementBehavior,
KeyboardMovementConfig,
KeyboardMovementConfiguration,
} from "./types";
import { getSearchText } from "./utils";

/**
* @remarks \@since 5.0.0
*/
export interface KeyboardMovementProviderProps
extends KeyboardMovementBehavior,
KeyboardMovementConfiguration {
children: ReactNode;
}

/**
* @example
* Main Usage
* ```tsx
* function Example() {
* return (
* <KeyboardMovementProvider>
* <CustomKeyboardFocusWidget />
* </KeyboardMovementProvider>
* );
* }
*
* function CustomKeyboardFocusWidget() {
* const { onKeyDown } = useKeyboardFocus();
* return (
* <div onKeyDown={onKeyDown}>
* <FocusableChild />
* <FocusableChild />
* <FocusableChild />
* <FocusableChild />
* </div>
* );
* }
*
* function FocusableChild() {
* const refCallback = useKeyboardFocusableElement()
*
* return <div role="menuitem" tabIndex={-1} ref={refCallback}>Content</div>;
* }
* ```
*
* @remarks \@since 5.0.0
*/
export function KeyboardMovementProvider({
children,
loopable = false,
searchable = false,
includeDisabled = false,
incrementKeys = DEFAULT_KEYBOARD_MOVEMENT.incrementKeys,
decrementKeys = DEFAULT_KEYBOARD_MOVEMENT.decrementKeys,
jumpToFirstKeys = DEFAULT_KEYBOARD_MOVEMENT.jumpToFirstKeys,
jumpToLastKeys = DEFAULT_KEYBOARD_MOVEMENT.jumpToLastKeys,
}: KeyboardMovementProviderProps): ReactElement {
const watching = useRef<KeyboardFocusElementData[]>([]);
const configuration: KeyboardMovementConfig = {
incrementKeys,
decrementKeys,
jumpToFirstKeys,
jumpToLastKeys,
};
const config = useRef(configuration);
config.current = configuration;

const value = useMemo<KeyboardFocusContext>(
() => ({
attach(element) {
watching.current.push({
element,
content: getSearchText(element, searchable),
});
},
detach(element) {
watching.current = watching.current.filter(
(cache) => cache.element !== element
);
},
watching,
config,
loopable,
searchable,
includeDisabled: includeDisabled,
}),
[includeDisabled, loopable, searchable]
);

return (
<KeyboardMovementContextProvider value={value}>
{children}
</KeyboardMovementContextProvider>
);
}

0 comments on commit 77f0d01

Please sign in to comment.