Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/public-crabs-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/focus": patch
---

Add focusGroup
Comment thread
davedbase marked this conversation as resolved.
83 changes: 81 additions & 2 deletions packages/focus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The native `autofocus` attribute only works on page load, which makes it incompa
- [`createAutofocus`](#createautofocus) - Reactive primitive to autofocus an element on render.
- [`createFocusTrap`](#createfocustrap) - Traps focus inside a given DOM element.
- [`createFocusRestore`](#createfocusrestore) - Restores focus to the previously focused element, without trapping.
- [`createFocusGroup`](#createfocusgroup) - Imperatively moves focus between the focusable elements of a container.

## Installation

Expand Down Expand Up @@ -174,17 +175,95 @@ const Popover: Component<{ open: boolean }> = props => {

### Props

| Prop | Type | Default | Description |
| ------------------- | ----------------------------------- | -------------------------- | ------------------------------------------------------------------------ |
| Prop | Type | Default | Description |
| ------------------- | ---------------------------------- | -------------------------- | ---------------------------------------------------------------------- |
| `enabled` | `MaybeAccessor<boolean>` | `true` | Whether focus-restore is active. |
| `element` | `MaybeAccessor<HTMLElement\|null>` | `document.body` | Element to dispatch the `onFinalFocus` event on. |
| `finalFocusElement` | `MaybeAccessor<HTMLElement\|null>` | Previously focused element | Element to focus when deactivated. |
| `onFinalFocus` | `(event: Event) => void` | — | Callback when focus restores. Call `event.preventDefault()` to cancel. |

## `createFocusGroup`

`createFocusGroup` creates a [FocusGroup](#focusgroup) that moves focus between the focusable elements of a container — e.g. arrow-key navigation in a menu, listbox or toolbar. It walks the DOM with a `TreeWalker`, either restricting itself to tabbable elements or considering everything focusable. Keyboard navigation (arrow keys, Home/End, Tab) is enabled by default: the `keydown` listener is attached to the focus group ref automatically.

### How to use it

```tsx
import { createFocusGroup } from "@solid-primitives/focus";

const [ref, setRef] = createSignal<HTMLElement>();

// Keyboard navigation is attached to the ref automatically.
createFocusGroup(ref);

return (
<div ref={setRef} role="menu">
<button role="menuitem">One</button>
<button role="menuitem">Two</button>
<button role="menuitem">Three</button>
</div>
);
```

The returned group also exposes imperative methods for moving focus, e.g. inside a click handler:

```tsx
const group = createFocusGroup(ref);

return <button onClick={() => group.focusNext()}>Next</button>;
```

### `FocusGroup`

The object returned by `createFocusGroup`. Each method focuses its target and returns it (or `undefined` when there is nothing to move to). Methods accept an options object:

| Method | Description |
| ----------------- | ------------------------------------------------------- |
| `focusNext()` | Moves focus to the next focusable/tabbable element. |
| `focusPrevious()` | Moves focus to the previous focusable/tabbable element. |
| `focusFirst()` | Moves focus to the first focusable/tabbable element. |
| `focusLast()` | Moves focus to the last focusable/tabbable element. |

### Keyboard navigation

Keyboard navigation is enabled by default and can be disabled with the `keyboardNavigation` option. The `keydown` listener is attached to the focus group ref (removed when the ref changes or the group is disposed):

- **Arrow keys** move focus between items, following `orientation` and `textDirection`. Home/End jump to the first/last item.
- **Tab/Shift+Tab** move within the group when `handleTab` is enabled and focus is already inside it; at a boundary the browser takes over.
- **`wrap: true`** loops around at the ends.

```tsx
createFocusGroup(ref, () => ({
orientation: "horizontal",
wrap: true,
}));
```

### Options

Options can be passed per-method-call or as default options (second argument to `createFocusGroup`, applied to every method call):

```tsx
const group = createFocusGroup(ref, () => ({ wrap: true, tabbable: true }));
```

| Option | Type | Default | Description |
| -------------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------- |
| `from` | `Element` | Currently focused | Element to start searching from. |
| `tabbable` | `boolean` | `false` | Only include tabbable elements (`tabindex="-1"` excluded). |
| `wrap` | `boolean` | `false` | Wrap around when reaching the end of the container. |
| `accept` | `(node) => boolean` | — | Callback determining whether an element is eligible for focus. |
| `orientation` | `MaybeAccessor<Orientation>` | `"vertical"` | The orientation of the focus group (`"vertical"` or `"horizontal"`). |
| `textDirection` | `MaybeAccessor<TextDirection>` | `"ltr"` | The text direction of the focus group (`"ltr"` or `"rtl"`). |
| `handleTab` | `MaybeAccessor<boolean>` | `true` | Whether tab key presses should be handled. |
| `keyboardNavigation` | `MaybeAccessor<boolean>` | `true` | Whether the `keydown` listener is attached to the ref. |
Comment thread
davedbase marked this conversation as resolved.

## Credits

`createFocusTrap` is ported from [solid-focus-trap](https://github.com/corvudev/corvu/tree/main/packages/solid-focus-trap), part of the [corvu](https://corvu.dev) UI toolkit by [Jasmin Noetzli (GiyoMoon)](https://github.com/GiyoMoon). Licensed under the MIT License.

`createFocusGroup` is ported from [kobalte](https://kobalte.dev)'s [`createFocusManager`](https://github.com/kobaltedev/kobalte/blob/main/packages/utils/src/focus-manager.ts), which in turn is based on [react-spectrum](https://react-spectrum.adobe.com)'s `FocusManager` (Apache License 2.0, Copyright 2020 Adobe).

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)
2 changes: 2 additions & 0 deletions packages/focus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"autofocus",
"createAutofocus",
"createFocusTrap",
"createFocusRestore",
"createFocusGroup",
"makeFocusListener",
"createFocusSignal"
],
Expand Down
Loading