kappyこのサイトのロゴ。
一覧に戻る

2024-12-15

React AriaにおけるSlotsについて

Tech

React AriaはAdobeが提供している、HeadlessなUIコンポーネントやHooksを提供するライブラリです。アクセシビリティや様々なデバイスの動作が考慮されており、開発者はアプリケーションの開発やデザインシステムの構築に注力しやすくなります。React Ariaのドキュメントを見ていくとコンポーネント内でslotというプロパティが使われていることがありますが、私が最初React Ariaを使い始めた時はどのようなものかよくわかりませんでした。改めて調べてみたので、React AriaにおけるSlotsについてまとめてみます。

Slotsとは

React AriaにおいてSlotsはコンポーネントに対して識別可能なプロパティを与えることで、同じコンポーネントでも各々に異なる動作やスタイルを適用することを可能にする仕組みです。公式のドキュメントでは以下のような例が挙げられています。

function Stepper({ children }) {
  let [value, setValue] = React.useState(0);

  return (
    <ButtonContext.Provider
      value={{
        slots: {
          increment: {
            onPress: () => setValue(value + 1),
          },
          decrement: {
            onPress: () => setValue(value - 1),
          },
        },
      }}
    >
      {children}
    </ButtonContext.Provider>
  );
}

<Stepper>
  <Button slot="increment"></Button>
  <Button slot="decrement"></Button>
</Stepper>;

このようにすることで、Stepperコンポーネント内でslotに応じて異なる動作を実装することができます。Buttonコンポーネントはslotプロパティを持っており、Stepperコンポーネント内でslotに応じた動作を実装することができます。ProviderにはButtonコンポーネントが受け取れるpropsを指定できるので、classNameやstyleをslotに応じて変更することも可能です。

例えば、別のページではボタンの中身を+や-に変更することも容易です。(この例だとあまり恩恵は感じにくいかもしれませんが...。)

また、React Ariaの開発者はコンポーネントがどのイベントを識別するかに利用するし、セマンティックを目的として識別するとも話しています。

これはMenuItemコンポーネント内にlabelやdescriptionといったslotを持たせることで、aria-labelledby属性に使用するidを指定することができるため、アクセシビリティを向上させることができます。

<MenuTrigger>
  <Button>open</Button>
  <Popover>
    <Menu>
      <MenuItem textValue="Copy">
        <Text slot="label">Copy</Text>
        <Text slot="description">Copy the selected text</Text>
        <Keyboard>⌘C</Keyboard>
      </MenuItem>
      <MenuItem textValue="Cut">
        <Text slot="label">Cut</Text>
        <Text slot="description">Cut the selected text</Text>
        <Keyboard>⌘X</Keyboard>
      </MenuItem>
      <MenuItem textValue="Paste">
        <Text slot="label">Paste</Text>
        <Text slot="description">Paste the copied text</Text>
        <Keyboard>⌘V</Keyboard>
      </MenuItem>
    </Menu>
  </Popover>
</MenuTrigger>

もし、このslotがなかった場合にはErrorが発生します。

MenuItem内のTextコンポーネントにslotがない場合のエラー。以下のようなエラー内容が表示される。Unhandled Runtime Error
Error: A slot prop is required. Valid slot names are "label"、"description".

React Aria ComponentsでのSlotsの利用

上記の通り、再利用性を高める目的で自分たちでSlotsの実装をすることもできますし、React Aria Componentsを利用するときにSlotsが求められる場面が他にもあるので、いくつか紹介します。

GridList

GridListは、アイテムのリストをグリッド形式で表示するコンポーネントです。アイテムのリストを表示する際に、dragAndDropHooksというプロパティを指定することで、アイテムのドラッグアンドドロップを実装す流ことが可能です。その際、slotにdragを指定することで、React Ariaがaria-labelを設定してくれます。

<GridList
  items={initialList}
  dragAndDropHooks={dragAndDropHooks}
  aria-label="List of items"
>
  {(item) => (
    <GridListItem textValue={item.name}>
      {(props) => (
        <>
          {props.allowsDragging && (
            <Button slot="drag">
              <GiHamburgerMenu />
            </Button>
          )}
          {item.name}
        </>
      )}
    </GridListItem>
  )}
</GridList>

もし、このslotがなかった場合にはconsoleにwarningが表示されます。

GridListItem内のButtonコンポーネントにslotがない場合の警告。以下のような警告内容が表示される。GridList.mjs:303 Draggable items in a GridList must contain a Button slot="drag" element so that keyboard and screen reader users can drag them.

Dialog

Dialogは、モーダルダイアログを表示するコンポーネントです。Dialog内のHeadingコンポーネントにはslot="title"、Buttonコンポーネントにはslot="close"を指定することが可能です。

<DialogTrigger>
  <Button>Sign up…</Button>
  <Modal>
    <Dialog>
      <form>
        <Heading slot="title">Sign up</Heading>
        <TextField autoFocus>
          <Label>First Name</Label>
          <Input />
        </TextField>
        <TextField>
          <Label>Last Name</Label>
          <Input />
        </TextField>
        <Button slot="close" style={{ marginTop: 8 }}>
          Submit
        </Button>
      </form>
    </Dialog>
  </Modal>
</DialogTrigger>

Disclosure

Disclosureは、開閉可能なセクションを表示するコンポーネントです。Disclosure内のButtonコンポーネントにはslot="trigger"を指定することで要素の開閉を制御することができます。

<Disclosure>
  <Heading>
    <Button slot="trigger">
      <svg viewBox="0 0 24 24">
        <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
      </svg>
      System Requirements
    </Button>
  </Heading>
  <DisclosurePanel>
    <p>Details about system requirements here.</p>
  </DisclosurePanel>
</Disclosure>

他にも、RadioGroupやDateFieldのようなformに用いるコンポーネントでは内部のTextコンポーネントのslotにerrorMessagedescriptionを指定することで、エラーメッセージや補助テキストを表示することができます。

スタイリング

Slotsを利用することで、コンポーネントのスタイリングを柔軟に行うことができます。例えば、slotに応じてスタイルを変更することができます。

<NumberField>
  <Label>Width</Label>
  <Group>
    <Input />
    <Button slot="increment">+</Button>
    <Button slot="decrement">-</Button>
  </Group>
</NumberField>
.react-aria-NumberField {
  [slot="increment"] {
    border-radius: 4px 4px 0 0;
  }

  [slot="decrement"] {
    border-radius: 0 0 4px 4px;
  }
}

まとめ

React AriaのSlotsは、コンポーネント内で異なる動作やスタイルを適用することを可能にする仕組みです。また、アクセシビリティを向上させるためにも利用されています。React Aria Componentsを利用する際には、どのようなSlotsが使えるかを確認しておくと便利な場面が存在します。

正直自分でカスタマイズしてslotsをうまく利用するパターンが思いついていないので、良いユースケースがあれば教えていただきたいです。

余談

人生初のOSSコントリビュートはReact Ariaでした。小さい変更だけど嬉しかった。

Set default value for type parameter in SelectProps by kp047i · Pull Request #7014 · adobe/react-spectrum · GitHub

github.com

Set default value for type parameter in SelectProps by kp047i · Pull Request #7014 · adobe/react-spectrum · GitHub