インストール

アプリに Stimeo UI を追加

Stimeo UI は Rails 向けに 挙動だけ を提供する Stimulus コントローラ群です。 ARIA 状態、キーボード操作、フォーカス管理を data-* 属性で駆動し、CSS は一切同梱しません。見た目は完全にあなたのものです。

ステータス: プレリリース版(0.x)です。 stimeo--* の属性 API は 1.0 までに変わる可能性があります。

Rails + importmap(推奨)

bundle add stimeo-ui
bin/rails generate stimeo:install

このコマンド(stimeo:install ジェネレータ)は、次のセットアップを自動で行います:

  • ビルド済みのライブラリ本体を vendor/javascript/stimeo/ に配置(同梱)し、
  • config/importmap.rb に stimeo-ui の読み込み設定(importmap のエントリ)を追加し、
  • コアのコントローラを Stimulus に登録し(app/javascript/controllers/stimeo.js)、
  • オプトインの positioning と cable を、この 2 つのファイルにコメントアウトした形で書き込みます。使うものの行のコメントを外して有効にします。positioning には、その節に載せた @floating-ui/dom の pin も要ります。この行はジェネレータが書きません(下の各節を参照)。

何度実行しても安全です。ライブラリ本体は最新版に置き換わり、既にある stimeo-ui の読み込み設定と controllers/stimeo.js はそのまま残ります。そのため、再実行しても既存の構成にオプトインの行は足されません。必要な行は下の各節に載せています。

Inspector は npm 側で配布しています。 stimeo インスペクタ CLI は Node 製のツールのため、gem には同梱されていません。gem で導入したアプリでも、プロジェクトに npm を追加せずに npx stimeo check app/views でその場で取得・実行できます。必要なのは Node の実行環境だけで、GitHub Actions の GitHub ホステッドランナーには最初から入っています。

あとは HTML に属性を書くだけでコンポーネントが動きます:

<div data-controller="stimeo--dropdown">
  <button data-stimeo--dropdown-target="trigger"
          data-action="click->stimeo--dropdown#toggle">Menu</button>
  <div data-stimeo--dropdown-target="menu" hidden>…</div>
</div>

npm(jsbundling など任意のバンドラ)

npm install stimeo-ui @hotwired/stimulus
import { Application } from "@hotwired/stimulus";
import { registerStimeo } from "stimeo-ui";

const application = Application.start();
registerStimeo(application); // コアのエントリポイントのコントローラをすべて登録

オプトインの stimeo-ui/positioning と stimeo-ui/cable は、それぞれ registerPositioning / registerCable で自分のコントローラを登録します(下記)。

必要なコントローラだけを使いたい場合は、stimeo-ui/controllers/* から個別に import して、好きな識別子で登録することもできます:

import { Application } from "@hotwired/stimulus";
import { DialogController } from "stimeo-ui/controllers/dialog_controller";

const application = Application.start();
application.register("stimeo--dialog", DialogController);

オプトインの positioning(フローティング配置)

popover / tooltip / hover-card のような「何かに沿って浮かぶ UI」では、開いたり閉じたりといった動きはコントローラが担います。一方でどこに表示するか(位置)は、通常はあなたの CSS で決めます。画面の端に来たときに自動で向きを反転させて収める、といった「動的な位置合わせ」が欲しいときだけ、@floating-ui/dom を使う positioning モジュール(stimeo-ui/positioning)を任意で追加します。必要なければ追加は不要で、本体(コアの stimeo-ui)には positioning モジュールも @floating-ui/dom も含まれないため、追加の依存はゼロのままです。使う場合は config/importmap.rb に次の 2 行を足して、サブパスと依存ライブラリも読み込めるようにします(1 行目はジェネレータがコメントアウトした形で書き込んでいます):

pin "stimeo-ui/positioning", to: "stimeo/positioning/index.js"
pin "@floating-ui/dom", to: "floating-ui-dom.js"

宣言的に位置を合わせる stimeo--anchored コントローラを使うときは、このモジュールのコントローラも登録します(この 2 行もジェネレータが controllers/stimeo.js にコメントアウトした形で書き込んでいます):

import { registerPositioning } from "stimeo-ui/positioning";
registerPositioning(application);

オプトインの cable(リアルタイム)

ライブカウンター、在席表示、入力中の表示は、状態をページの中ではなく Action Cable のチャンネルに持ちます。これらのコントローラ(stimeo--live-counter / stimeo--presence / stimeo--typing-indicator)は、@rails/actioncable を使うオプトインの cable モジュール(stimeo-ui/cable)に入っています。本体(コアの stimeo-ui)にはモジュールも @rails/actioncable も含まれないため、使うときだけ追加します。importmap では、サブパスと依存ライブラリを pin します(2 行ともジェネレータがコメントアウトした形で書き込んでいます):

pin "stimeo-ui/cable", to: "stimeo/cable/index.js"
pin "@rails/actioncable", to: "actioncable.esm.js"

そのうえで、コアのコントローラと並べて登録します(registerCable の行もジェネレータが controllers/stimeo.js にコメントアウトした形で書き込んでいます)。アプリが既に Action Cable の consumer を持っているなら、先に setCableConsumer で渡すと WebSocket を 1 本で共有できます。持っていなければ、最初に使うときにモジュールが自分で作ります。

import consumer from "channels/consumer"; // アプリが consumer を持つ場合だけ
import { registerCable, setCableConsumer } from "stimeo-ui/cable";

setCableConsumer(consumer);
registerCable(application);

Lint(リント)

Stimeo UI はヘッドレス(見た目を持たない)なので、role などの WAI-ARIA 属性は あなた自身が書きます。一部のコントローラは、その属性を手がかりに要素を探します(例: data-grid は [role="row"] で行を見つけます)。そのため、あなたのマークアップには <ul role="menu"> / <div role="radio"> / <td role="gridcell"> のような、APG に沿った正しいカスタムウィジェットの ARIA が含まれます。

ところが厳格な静的 a11y リンタ(Biome の recommended プリセット(2.5 以降)や eslint-plugin-jsx-a11y)は、こうした正しい書き方をエラー扱いします。<button> のようなネイティブ要素を前提にしたチェックだからです。そこで Stimeo UI のマークアップを書く場所だけ 該当ルールを緩めます(includes のパスは自分の構成に合わせて調整します)。

Biome なら biome.json に次の overrides を追加します:

{
  "overrides": [
    {
      "includes": ["app/components/**"],
      "linter": {
        "rules": {
          "a11y": {
            "noNoninteractiveElementToInteractiveRole": "off",
            "noRedundantRoles": "off",
            "useSemanticElements": "off",
            "useFocusableInteractive": "off",
            "noNoninteractiveTabindex": "off"
          }
        }
      }
    }
  ]
}

ESLint(eslint-plugin-jsx-a11y)を使う場合も、同じ考え方で、対象のパス(.eslintrc.json など)だけ同等のルールを off にします。

次のステップ