@react-lit/descendants
v0.7.0
Published
A descendant index solution for better accessibility support in compound components.
Downloads
9
Readme
@react-lit/descendants
A descendant index solution for better accessibility support in compound components.
This package provides these key tools:
createDescendantContext
: Creates a special context object to deal with registering descendants in a tree.useDescendantsInit
: A hook to create a state object containing a descendants array and setter function.DescendantProvider
: A provider that accepts the descendants array, the state setter, and the component's context object for use at the top of the component tree.useDescendant
: A hook called in the body of a nested descendant component that registers its DOM node and returns its index relative to other descendants in the tree.useDescendants
: A hook that accepts the descendant context and returns descendants registered to the passed context.
Installation
$ npm i @react-lit/descendants
# or
$ yarn add @react-lit/descendants
Example
import * as React from 'react';
import {
createDescendantContext,
DescendantProvider,
useDescendant,
useDescendantsInit,
} from "@react-lit/descendants";
const DescendantContext = createDescendantContext("DescendantContext");
const MenuContext = React.createContext();
function Menu({ id, children }) {
// NOTE(joel): We could be less explicit here and set this up in the
// `DescendantProvider`, but you may want to do something with `descendants`
// in your top-level component and we don't want to force creating an
// arbitrary child component just so we can consume the context.
const [descendants, setDescendants] = useDescendantsInit();
const [activeIndex, setActiveIndex] = React.useState(-1);
return (
<DescendantProvider
context={DescendantContext}
items={descendants}
set={setDescendants}
>
<MenuContext.Provider
value={{ buttonId: `button`, activeIndex, setActiveIndex }}
>
{children}
</MenuContext.Provider>
</DescendantProvider>
);
}
function MenuList(props) {
const { buttonId, activeIndex } = React.useContext(MenuContext);
return (
<div
role="menu"
aria-labelledby={buttonId}
aria-activedescendant={activeIndex}
tabIndex={-1}
>
{children}
</div>
);
}
function MenuItem({ index: explicitIndex, ...props }) {
const { activeIndex, setActiveIndex } = React.useContext(MenuContext);
const ref = React.useRef(null);
// NOTE(joel): We use a stateful ref here because we need the actual DOM
// element for our descendant object, but also need to update state after
// the dom ref is placed.
const [element, elementSet] = React.useState(null);
const handleRefSet = React.useCallback((refValue) => {
ref.current = refValue;
elementSet(refValue);
}, []);
// NOTE(joel): The descendant should be memoized to prevent endless render
// loops after the collection state is updated.
const descendant = React.useMemo(() => {
return {
element,
// NOTE(joel): You can pass arbitrary data into a descendant object which
// can come in handy for features like typeahead!
key: props.label,
};
}, [element, props.label]);
// NOTE(joel): Tell the `useDescendant` hook to use a specific context.
// This is key in case you have a compound component that needs index
// tracking in separate correlating descendant components (like `Tabs`)
// If you want to declare a specific index value, you can pass it as the
// third argument here. This is almost never needed but we provide it as an
// escape hatch for special circumstances.
const index = useDescendant(descendant, DescendantContext, explicitIndex);
// NOTE(joel): After we know the index, we can use it!
const isSelected = index === activeIndex;
function select() {
if (!isSelected) {
setActiveIndex(index);
}
}
return (
<div
role="menuitem"
ref={handleRefSet}
data-selected={isSelected ? "" : undefined}
tabIndex={-1}
onMouseEnter={select}
{...props}
/>
);
}
Development
(1) Install dependencies
$ npm i
# or
$ yarn
(2) Run initial validation
$ ./Taskfile.sh validate
(3) Run tests in watch-mode to validate functionality.
$ ./Taskfile test -w
This project was set up by @jvdx/core