# ember-aria-voyager

[![Maintainability](https://qlty.sh/gh/hokulea/projects/aria-voyager/maintainability.svg)](https://qlty.sh/gh/hokulea/projects/aria-voyager)
[![Code Coverage](https://qlty.sh/gh/hokulea/projects/aria-voyager/coverage.svg)](https://qlty.sh/gh/hokulea/projects/aria-voyager)

_Canoe vessel that navigates your aria._

Ember reactivity bindings for [`aria-voyager`](https://github.com/hokulea/aria-voyager).

## BYOM: Bring Your Own Markup

... and this library will make it interactive, according to applicable [ARIA
patterns](https://www.w3.org/WAI/ARIA/apg/patterns/). This library does not
apply styling, it will operate on the accessibility tree.

## Supported Features

See [`hokulea/aria-voyager`](https://github.com/hokulea/aria-voyager/) for a
full list of supported features.

## Installation

```sh
pnpm add ember-aria-voyager
```

## Usage

### `{{ariaGroup}}`

Basic example:

```glimmer-ts
import { ariaGroup } from 'ember-aria-voyager';

<template>
  <div role="group" {{ariaGroup}}>
    <button type="button">Cut</button>
    <button type="button">Copy</button>
    <button type="button">Paste</button>
  </div>
</template>
```

Here are the options, you can pass to `{{ariaGroup}}`

```ts
interface GroupSignature<T> {
  Element: HTMLElement;
  Args: {
    Positional: [];
    Named: {
      items?: T[];
      disabled?: boolean;
    };
  };
}
```

Full example with reactive items:

```glimmer-ts
import { ariaGroup } from 'ember-aria-voyager';
import { tracked } from '@glimmer/tracking';

const context = new class {
  @tracked items = ['Cut', 'Copy', 'Paste'];
  @tracked disabled = false;
};

<template>
  <div role="group" {{ariaGroup items=context.items disabled=context.disabled}}>
    {{#each context.items as |item|}}
      <button type="button">{{item}}</button>
    {{/each}}
  </div>
</template>
```

### `{{ariaListbox}}`

Basic example:

```glimmer-ts
import { ariaListbox } from 'ember-aria-voyager';
const options = ['apple', 'banana', 'pineapple'];

<template>
  <ul role="listbox" {{ariaListbox items=options}}>
    {{#each options as |option|}}
      <li role="option">{{option}}</li>
    {{/each}}
  </ul>
</template>
```

Here are the options, you can pass to `{{ariaListbox}}`

```ts
type ListboxSignature<T = HTMLElement> = {
  items?: T[];
  selection?: T | T[];
  activateItem?: (item: T) => void;
  check?: (selection: T[]) => void;
} & (
  | {
      multi: true;
      select?: (selection: T[]) => void;
    }
  | {
      multi?: false;
      select?: (selection: T) => void;
    }
)
```

When passing `items` the `select()` and `selection` can work off of your passed items, anyway will fall back to the HTMLElement

Full example:

```glimmer-ts
import { ariaListbox } from 'ember-aria-voyager';
const options = ['apple', 'banana', 'pineapple'];
const context = new class {
  @tracked selection = [options[0]];
  @tracked disabled = false;
  
  select: (fruits: string[]) => {
    this.selection = fruits;
  }
};

const selection = ['banana'];

<template>
  <ul role="listbox" {{ariaListbox 
    items=options
    multi=true
    disabled=context.disabled
    selection=context.selection
    select=context.select
  }}>
    {{#each options as |option|}}
      <li role="option">{{option}}</li>
    {{/each}}
  </ul>
</template>
```

### `{{ariaMenu}}`

Basic example:

```glimmer-ts
import { ariaMenu } from 'ember-aria-voyager';

<template>
  <div role="menu" {{ariaMenu}}>
    <button role="menuitem">Version Info</button>
    <a role="menuitem" href="https://github.com/hokulea/aria-voyager" target="_blank">Github</a>
    <button role="menuitem" popovertarget="authormenu">Author</button>
    <div role="menu" id="authormenu" popover {{ariaMenu}}>
      <a role="menuitem" href="https://gos.si"  target="_blank">Homepage</a>
      <a role="menuitem" href="https://github.com" target="_blank">Github</a>
    </div>
  </div>
</template>
```

Here are the options, you can pass to `{{ariaMenu}}`

```ts
interface MenuOptions<T> {
  items?: T[];
  disabled?: boolean;
  select?: (selection: HTMLElement[]) => void;
  check?: (selection: HTMLElement[]) => void;
}
```

Here is a full example:

```glimmer-ts
import { ariaMenu } from 'ember-aria-voyager';
const items = [
  {
    label: 'Version Info',
    action: () => console.log('1.2.4');
  },
  {
    label 'Github',
    link: 'https://github.com/hokulea/aria-voyager'
  }
];

<template>
  <div role="menu" {{ariaMenu items=items}}>
    {{#each items as |item|}}
      {{#if item.action}}
        <button type="button" role="menuitem" {{on "click" item.action}}>{{item.label}}</button>
      {{else if item.link}}
        <a role="menuitem" href={{item.link}} target="_blank">{{item.label}}</a>
      {{else}}
        euw, what?
      {{/if}}
    {{/each}}
  </div>
</template>
```

#### `menuitemcheckbox`

Menu supports checking items.

```glimmer-js
import { ariaMenu } from 'ember-aria-voyager';

function check(selection: HTMLElement[]) {
  console.log('checked items:', selection);
}

<template>
  <div role="menu" {{ariaMenu check=check}}>
    <span role="menuitemcheckbox">Bold</span>
    <span role="menuitemcheckbox">Italic</span>
    <span role="menuitemcheckbox">Underline</span>
    <span role="menuitemcheckbox">Strikethrough</span>
  </div>
</template>
```

#### `menuitemradio`

Menu supports radio selection.
The `role="menu"` itself acts as group within one of all radio items is checked.

Here is a minimal example:

```glimmer-js
import { ariaMenu } from 'ember-aria-voyager';

function select(selection: HTMLElement[]) {
  console.log('selected items:', selection);
}

<template>
  <div role="menu" {{ariaMenu select=select}}>
    <span role="menuitemradio">Left</span>
    <span role="menuitemradio">Center</span>
    <span role="menuitemradio">Right</span>
    <span role="menuitemradio">Justified</span>
  </div>
</template>
```

Though menus do support multiple groups of radio selection.
This is supported as well.
Here are all combinations:

```glimmer-js
import { ariaMenu } from 'ember-aria-voyager';

function select(selection: HTMLElement) {
  console.log('selected items across all groups:', selection);
}

<template>
  <div role="menu" {{ariaMenu select=select}}>
    <span role="menuitemradio">A</span>
    <div role="group">
      <span role="menuitemradio">B</span>
      <span role="menuitemradio">C</span>
    </div>
    <span role="menuitemradio">D</span>
    <div role="presentation">
      <span role="menuitemradio">E</span>
      <hr>
      <span role="menuitemradio">F</span>
    </div>
    <span role="menuitemradio">G</span>
    <span role="separator"></span>
    <span role="menuitemradio">H</span>
  </div>
</template>
```

- B+C are one group
- All others are in their own respective group
- `select()` will return the selection across all groups

### `{{ariaTablist}}`

Basic example:

```glimmer-ts
import { ariaTablist } from 'ember-aria-voyager';
const tabs = ['apple', 'banana', 'pineapple'];

<template>
  <div>
    <ul role="tablist" {{ariaTablist items=tabs}}>
      {{#each tabs as |tab id|}}
        <li role="tab" id="tab-{{id}}" aria-controls="panel-{{id}}">{{tab}}</li>
      {{/each}}
    </ul>

    {{#each tabs as |tab id|}}
      <div role="tabpanel" id="panel-{{id}}" aria-labelledby="tab-{{id}}">
        Contents Panel {{tab}}
      </div>
    {{/each}}
  <div>
</template>
```

Here are the options, you can pass to `{{ariaTablist}}`

```ts
import type { EmitStrategy, Orientation, TablistBehavior } from 'aria-voyager';

interface TablistSignature<T> {
  Element: HTMLElement;
  Args: {
    Positional: [];
    Named: {
      disabled?: boolean;
      orientation?: Orientation;
      behavior?: TablistBehavior;
    } & EmitterSignature<T>;
  };
}
```

When passing `items` the `select()` and `selection` can work off of your passed items, anyway will fall back to the HTMLElement

### `{{ariaRadioGroup}}`

Basic example:

```glimmer-ts
import { ariaRadioGroup } from 'ember-aria-voyager';

<template>
  <div role="radiogroup" {{ariaRadioGroup}}>
    <button type="button" role="radio" aria-checked="false">Top</button>
    <button type="button" role="radio" aria-checked="false">Bottom</button>
    <button type="button" role="radio" aria-checked="false">Left</button>
  </div>
</template>
```

Here are the options, you can pass to `{{ariaRadioGroup}}`

```ts
interface RadioGroupSignature<T> {
  Element: HTMLElement;
  Args: {
    Positional: [];
    Named: {
      items?: T[];
      select?: (selection: HTMLElement) => void;
      disabled?: boolean;
    };
  };
}
```

Full example with selection callback:

```glimmer-ts
import { ariaRadioGroup } from 'ember-aria-voyager';
import { tracked } from '@glimmer/tracking';

const context = new class {
  @tracked selected?: HTMLElement;

  select = (item: HTMLElement) => {
    this.selected = item;
  };
};

<template>
  <div role="radiogroup" {{ariaRadioGroup select=context.select}}>
    <button type="button" role="radio" aria-checked="false">Top</button>
    <button type="button" role="radio" aria-checked="false">Bottom</button>
    <button type="button" role="radio" aria-checked="false">Left</button>
  </div>
</template>
```

## Use in Classic Apps

`ember-aria-voyager` is primed to be used with polaris edition of ember using
imports. Classic v1 addons and v2 addons with compat automatically provide
re-exports, which this addon does not. Re-export it manually. Here is an example
for `ariaListbox`:

```ts
// app/modifiers/aria-listbox.js
export { ariaListbox as default } from 'ember-aria-voyager';
```
