Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ffc1183
Add Navigation component and expose api
MartinBozhilov-coh Nov 21, 2025
1f38cdd
Add type suggestions for keys and buttons
MartinBozhilov-coh Nov 21, 2025
0464b56
Add directive support for components to implement
MartinBozhilov-coh Nov 21, 2025
c5ff49b
Add a way to pause and resume actions
MartinBozhilov-coh Nov 24, 2025
1e1a4e9
Add spatial nav pause/resume
MartinBozhilov-coh Nov 25, 2025
8c95e8c
Add documentation and move component to Utility folder
MartinBozhilov-coh Nov 25, 2025
c99a48c
Add demo integration in the Menu UI for testing
MartinBozhilov-coh Nov 25, 2025
60f6133
update im version
MartinBozhilov-coh Nov 26, 2025
8a5f22c
Make components default actions extendable
MartinBozhilov-coh Nov 26, 2025
6988ea9
Add navigation to components (#61)
MartinBozhilov-coh Jan 5, 2026
4b28782
Resolve PR comments
MartinBozhilov-coh Jan 7, 2026
109862d
Add nav actions to segment
MartinBozhilov-coh Jan 7, 2026
4b65c5b
Add nav actions to radio component
MartinBozhilov-coh Jan 7, 2026
a732361
Fix actions not executing due to focus loss on accidental clicks
MartinBozhilov-coh Jan 8, 2026
df949a7
Change how layout components with no methods access ref, added change…
MartinBozhilov-coh Jan 8, 2026
a3f6fbf
Fix dropdown bug that loses focus from navigation when closed with mouse
MartinBozhilov-coh Jan 8, 2026
c70f6e2
Extended textblocks to support navigation actions
MartinBozhilov-coh Jan 8, 2026
6c1d5b7
Add new default action and extend XYSlider
MartinBozhilov-coh Jan 9, 2026
a5adf0d
Change onAction prop type in docs to include passed args
MartinBozhilov-coh Jan 13, 2026
febd138
Make the pan action paused by default
MartinBozhilov-coh Jan 13, 2026
67bc23c
Add anchor and onAction props to Layout components
MartinBozhilov-coh Jan 19, 2026
e7b0bb0
Change how pausing an action works
MartinBozhilov-coh Jan 20, 2026
bffb9ed
Extended scroll to use pan action
MartinBozhilov-coh Jan 20, 2026
305e5ba
Add ref access guide in navigation docs
MartinBozhilov-coh Jan 20, 2026
14813f4
Improve pause/resume logic and resolve pr comments
MartinBozhilov-coh Jan 27, 2026
e75acae
Resolve PR comments
MartinBozhilov-coh Jan 28, 2026
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
Binary file not shown.
88 changes: 88 additions & 0 deletions docs/src/assets/components/utility/navigation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 43 additions & 1 deletion docs/src/content/docs/changelog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@ title: Changelog

import { Badge } from '@astrojs/starlight/components'

## 2.0.0 :badge[Latest]{variant="note"}

### ⚠️ Breaking Changes

- <Badge text="Breaking Change" variant="danger" size="small" /> The `ref` prop on layout components (Absolute, Flex, Block, etc.) that don't have any other methods or properties now exposes the DOM node directly. You no longer need to access the `.element` property.
<br/> Affected components:
[Absolute](/components/layout/absolute/), [Block](/components/layout/block/), [Bottom](/components/layout/bottom/), [Column](/components/layout/column/), [Content](/components/layout/content/), [Flex](/components/layout/flex/),
[Grid](/components/layout/grid/), [GridTile](/components/layout/grid-tile/), [Layout](/components/layout/layout/), [Layout3D](/components/layout/layout-3d/), [List](/components/layout/list/),
[Relative](/components/layout/relative/), [Row](/components/layout/row/), [TabLink](/components/layout/tab-link/), [Tab](/components/layout/tab/), [Top](/components/layout/top/), [Transform](/components/layout/transform/),
[BackgroundImage](/components/media/background-image/), [MaskImage](/components/media/mask-image/).

### ✨ New Features

- <Badge text="New" variant="tip" size="small" /> [**Navigation**](/components/utility/navigation/) - Introduced a new utility component for handling spatial navigation, gamepad/keyboard interactions and focus management.

- <Badge text="Feature" variant="success" size="small" /> **Added [Navigation.Area]((/components/utility/navigation/#navigationarea)) slot** for grouping navigable components within the [Navigation](/components/utility/navigation/) component.

- <Badge text="Feature" variant="success" size="small" /> **Custom Component Action Subscriptions:** Custom components can now subscribe to [Navigation actions](/components/utility/navigation/#extending-component-navigation-actions).

### 🛠 Component Enhancements (Navigation Support)

The following components have been extended to support onAction and anchor props. They now respond to specific spatial navigation actions when placed inside a Navigation area:

| Component | Supported Actions / Props |
| :--- | :--- |
| [Button](/components/basic/button#props), [RoundedButton](/components/basic/rounded-button#props) | <Badge text="Props Only" variant="note" size="small" /> (`onAction`, `anchor`) |
| [Checkbox](/components/basic/checkbox/#implemented-navigation-actions) | <Badge text="select" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [Accordion](/components/basic/accordion/#implemented-navigation-actions) | <Badge text="select" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [Dropdown](/components/basic/dropdown/#implemented-navigation-actions) | <Badge text="select" variant="success" size="small" /> <Badge text="back" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [NumberInput](/components/basic/number-input/#implemented-navigation-actions) | <Badge text="select" variant="success" size="small" /> <Badge text="back" variant="success" size="small" /> <Badge text="move-up/down" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [PasswordInput](/components/basic/password-input/#implemented-navigation-actions), [TextInput](/components/basic/text-input/#implemented-navigation-actions) | <Badge text="select" variant="success" size="small" /> <Badge text="back" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [Radio](/components/basic/radio/#implemented-navigation-actions), [Segment](/components/basic/segment/#implemented-navigation-actions), [Stepper](/components/basic/stepper/#implemented-navigation-actions), [Slider](/components/basic/slider/#implemented-navigation-actions), [TextSlider](/components/basic/text-slider/#implemented-navigation-actions) | <Badge text="move-left/right" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [XYSlider](/components/basic/xy-slider/#implemented-navigation-actions), [Scroll](/components/layout/scroll/#implemented-navigation-actions) | <Badge text="pan" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [Tooltip](/components/feedback/tooltip/#implemented-navigation-actions) | <Badge text="back" variant="success" size="small" />, <Badge text="Props" variant="note" size="small" /> |
| [TextBlock](/components/basic/text-block#props), [InlineTextBlock](/components/basic/inline-text-block#props) | <Badge text="Props Only" variant="note" size="small" /> (`onAction`, `anchor`) |

### 📖 New Guides

- <Badge text="New" variant="tip" size="small" /> [List](/components/layout/list) - Provide [a guide](/components/layout/list/#making-list-items-navigable) on how to initialize list items as navigable elements within a [Navigation](/components/utility/Navigation/) area.

- <Badge text="New" variant="tip" size="small" /> [GridTile](/components/layout/grid-tile) - Provide [a guide](/components/layout/grid-tile/#making-grid-tiles-navigable) on how to initialize grid tiles as navigable elements within a [Navigation](/components/utility/Navigation/) area.

## 1.2.0 :badge[Latest]{variant="note"}

<Badge text="New" variant="tip" size="small" /> Added [Icon](/components/media/icon/) component.
Expand All @@ -12,7 +54,7 @@ import { Badge } from '@astrojs/starlight/components'

<Badge text="Feature" variant="success" size="small" /> Added an automatic script that runs with the development server for updating the [Icon](/components/media/icon/) component's type whenever new images are added to the `src/assets/icons` directory.

## 1.1.0
## 1.1.0

<Badge text="New" variant="tip" size="small" /> Added [Tutorial](/components/complex/tutorial/) component.

Expand Down
169 changes: 169 additions & 0 deletions docs/src/content/docs/components/Basic/accordion.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ It displays the panel's title or label and handles user interaction for expandin
| `style` | `JSX.CSSProperties` | `{}` | Inline styles to apply to the heading element. |
| `class` | `string` | `""` | Additional CSS classes for the heading element. |
| `children` | `JSX.Element` | `""` | Content of the heading, used to render text, HTML, or JSX elements within the heading. |
| `onAction` | `Record<string, (scope: string) => void>` | `undefined` | Extends or overrides the heading's default navigation action handlers. See [Implemented Navigation Actions](#implemented-navigation-actions) for details. |
| `anchor` | `string \| HTMLElement` | `undefined` | Links navigation to another element. When the anchor element is focused, the heading's actions will execute. Can be a CSS selector or HTMLElement. |

### `Accordion.Icon`

Expand Down Expand Up @@ -185,6 +187,37 @@ It displays the main content associated with the panel.
| `class` | `string` | `""` | Additional CSS classes for the body element. |
| `children` | `JSX.Element` | `""` | Content of the body, used to render text, HTML, or JSX elements within the body. |

## Implemented Navigation Actions

The `Accordion.Heading` component slot implements the following navigation action by default:

| Action Name | Behavior |
|--------------|-----------------------------------------------------------------------------------------------|
| `select` | Toggles the accordion panel (expands if collapsed, collapses if expanded) |

You can extend the `Accordion.Heading` with additional navigation actions or override the default behavior using the `onAction` prop:

```tsx
import Accordion from '@components/Basic/Accordion/Accordion';

<Accordion>
<Accordion.Panel>
<Accordion.Heading
class="navigable"
onAction={{
'select': () => console.log('Custom select behavior!'),
'info': () => console.log('Show panel info')
}}
>
Heading 1
</Accordion.Heading>
<Accordion.Body>Accordion content</Accordion.Body>
</Accordion.Panel>
</Accordion>
```

For more information about navigation actions, see the [Navigation component documentation](/components/utility/navigation#extending-component-navigation-actions).

## Guide

### Retrieve the modified panel's title on change
Expand Down Expand Up @@ -262,4 +295,140 @@ const App = () => {
};

export default App;
```

### Making accordion headings navigable

The `Accordion.Heading` needs to be focused in order to respond to the `select` action.

To enable gamepad/keyboard navigation for accordion headings, add them to a navigation area by assigning a CSS class and registering them with `Navigation.Area` or `navigationRef.registerArea()`.

#### Steps

1. Wrap your `Accordion` inside a `Navigation` component
2. Assign a CSS class to each `Accordion.Heading` you want to be navigable
3. Register the headings as a navigation area using either:
- `navigationRef.registerArea()` with the class selector
- Or wrap the accordion in `Navigation.Area` with the appropriate selector

```tsx
import Navigation, { NavigationRef } from '@components/Utility/Navigation/Navigation';
import Accordion from '@components/Basic/Accordion/Accordion';

const App = () => {
let navigationRef: NavigationRef | undefined;

// Register accordion headings as a navigation area
onMount(() => {
navigationRef?.registerArea('accordion-headers', ['.accordion-heading']);
});

return (
<Navigation ref={navigationRef}>
<Accordion>
<Accordion.Panel title="panel-1">
<Accordion.Heading class="accordion-heading">
Settings
</Accordion.Heading>
<Accordion.Body>Settings content</Accordion.Body>
</Accordion.Panel>
<Accordion.Panel title="panel-2">
<Accordion.Heading class="accordion-heading">
Audio
</Accordion.Heading>
<Accordion.Body>Audio content</Accordion.Body>
</Accordion.Panel>
</Accordion>
</Navigation>
);
};

export default App;
```

### Navigating accordion body content

<video src="/video/accordion/header-navigation.webm" autoplay loop muted></video>

For more complex accordion implementations where the body contains navigable elements (like form inputs, buttons, or lists), you can create a `Navigation.Area` inside the `Accordion.Body` and automatically focus it when the panel expands.

#### Steps

1. Wrap the elements you wish to be navigable inside `Accordion.Body` with `Navigation.Area`
2. Use the `onChange` callback to detect when the panel expands
3. Call `navigationRef.focusFirst()` or `navigationRef.switchArea()` to focus the body's navigation area
4. Optionally set up a `back` action to return focus to the accordion headings

```tsx
import Navigation, { NavigationRef } from '@components/Utility/Navigation/Navigation';
import Accordion from '@components/Basic/Accordion/Accordion';

const App = () => {
let navigationRef: NavigationRef | undefined;

// Handle accordion panel changes
const handleAccordionChange = (title: string) => {
if (title === 'settings-panel') {
// Focus the navigation area inside the expanded panel
navigationRef?.focusFirst('settings-area');
}
};

onMount(() => {
// Register accordion headings as an area and focus it.
navigationRef?.registerArea('accordion-headers', ['.accordion-heading'], true);

// Configure back action to return to accordion headers
navigationRef?.updateAction('back', {
key: { binds: ['ESC'], type: ['press'] },
button: { binds: ['face-button-right'] },
callback: (scope) => {
if (scope === 'settings-area') {
navigationRef?.focusFirst('accordion-headers');
}
}
});
});

return (
<Navigation ref={navigationRef}>
<Accordion onChange={handleAccordionChange}>
<Accordion.Panel title="settings-panel">
<Accordion.Heading class="accordion-heading">
Settings
</Accordion.Heading>
<Accordion.Body>
<Navigation.Area name="settings-area">
<div class="menu-item">Graphics Quality</div>
<div class="menu-item">Resolution</div>
<div class="menu-item">V-Sync</div>
</Navigation.Area>
</Accordion.Body>
</Accordion.Panel>
<Accordion.Panel title="audio-panel">
<Accordion.Heading class="accordion-heading">
Audio
</Accordion.Heading>
<Accordion.Body>Audio settings content</Accordion.Body>
</Accordion.Panel>
</Accordion>
</Navigation>
);
};

export default App;
```

CSS for `.menu-item`:

```css
.menu-item {
padding: 1vmax;
border-bottom: 1px solid #232323;
background-color: #cecece;
}

.menu-item:focus {
background-color: #b1b1b1;
}
```
2 changes: 2 additions & 0 deletions docs/src/content/docs/components/Basic/button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export default App;
| `disabled` | `boolean` | `false` | Specify if the button is disabled |
| `size` | `'large' \| 'middle' \| 'small'` | `''` | Specify the size of the button. If an empty string is passed, the button won't have any size. In that case, please specify the size through the `class` or `style` properties. |
| `textFit` | `boolean` | `true` | Specify if the text inside the button should be fitted. By default, this option is **enabled**. |
| `onAction` | `Record<string, (scope?: string, ...args: any[]) => void>` | `undefined` | Allows you to add custom navigation action handlers to the button. See the [Navigation component documentation](/components/utility/navigation#extending-component-navigation-actions) for details. |
| `anchor` | `string \| HTMLElement` | `undefined` | Links navigation to another element. When the anchor element is focused, the button's actions will execute. Can be a CSS selector or HTMLElement. |

## Guide

Expand Down
28 changes: 28 additions & 0 deletions docs/src/content/docs/components/Basic/checkbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export default App;
| `value` | `any` | `''` | The value associated with the checkbox component. |
| `checked` | `boolean` | `false` | Specify if the checkbox is checked initially. |
| `onChange` | `(checked: boolean) => void` | `undefined` | A function that is called every time the checkbox is toggled. It can be used to retrieve whether the checkbox is checked. |
| `onAction` | `Record<string, (scope?: string, ...args: any[]) => void>` | `undefined` | Extends or overrides the component's default navigation action handlers. See [Implemented Navigation Actions](#implemented-navigation-actions) for details. |
| `anchor` | `string \| HTMLElement` | `undefined` | Links navigation to another element. When the anchor element is focused, the checkbox's actions will execute. Can be a CSS selector or HTMLElement. |

## Ref API

Expand Down Expand Up @@ -176,6 +178,32 @@ const App = () => {
export default App;
```

## Implemented Navigation Actions

The `Checkbox` component implements the following navigation actions by default:

| Action Name | Behavior |
|-------------|-----------------------------------|
| `select` | Toggles the checkbox on/off |

You can extend the Checkbox with additional navigation actions or override the default behavior using the `onAction` prop:

```tsx
import Checkbox from '@components/Basic/Checkbox/Checkbox';

<Checkbox
value="v-sync"
onAction={{
'back': () => console.log('Going back from checkbox'),
'select': () => console.log('Custom toggle logic') // Overrides default
}}
>
Enable V-Sync
</Checkbox>
```

For more information about navigation actions, see the [Navigation component documentation](/components/utility/navigation#extending-component-navigation-actions).

## Guide

### Retrive the Checkbox Value
Expand Down
36 changes: 36 additions & 0 deletions docs/src/content/docs/components/Basic/dropdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export default App;
| `disabled` | `boolean` | `false` | Disables the dropdown when set to `true`. |
| `class-disabled` | `string` | `""` | Additional CSS classes to apply when the dropdown is disabled. |
| `onChange` | `(value: string) => void` | `undefined` | Callback function triggered whenever the selected option changes, providing the selected option's value. |
| `onAction` | `Record<string, (scope?: string, ...args: any[]) => void>` | `undefined` | Extends or overrides the component's default navigation action handlers. See [Implemented Navigation Actions](#implemented-navigation-actions) for details. |
| `anchor` | `string \| HTMLElement` | `undefined` | Links navigation to another element. When the anchor element is focused, the dropdown's actions will execute. Can be a CSS selector or HTMLElement. |

## Ref API

Expand Down Expand Up @@ -268,6 +270,40 @@ To hide the `Dropdown.Handle`, set its `display` style to `none`:

:::

## Implemented Navigation Actions

The `Dropdown` component implements the following navigation actions by default:

| Action Name | Behavior |
|--------------|-----------------------------------------------------------------------------------------------|
| `select` | Opens the dropdown when closed. When open, collapses the dropdown (options handle selection) |
| `back` | Closes the dropdown and returns focus to the trigger or anchor element |

You can extend the `Dropdown` with additional navigation actions or override the default behavior using the `onAction` prop:

```tsx
import Dropdown from '@components/Basic/Dropdown/Dropdown';

<Dropdown
onAction={{
'select': () => console.log('Custom select behavior!'),
'back': () => console.log('Custom back behavior!')
}}
>
<Dropdown.Options>
<Dropdown.Option value="red">red</Dropdown.Option>
<Dropdown.Option value="green">green</Dropdown.Option>
<Dropdown.Option value="blue">blue</Dropdown.Option>
</Dropdown.Options>
</Dropdown>
```

:::caution
Every `Dropdown.Option` component is listening for the `select` action to handle option selection. This action cannot be overriden.
:::

For more information about navigation actions, check the [Navigation component documentation](/components/utility/navigation#extending-component-navigation-actions).

## Guide

### Retrieve the selected option's value on change
Expand Down
Loading