彼がいたジム

腹筋を鍛えることにおいて、BIG3に取り組むことは安全かつ必要十分なトレーニング効果をもたらす最良の方法である。腹筋は背骨を安定させるための筋肉だ。スクワットやデッドリフトで大きな重量を扱うことによって、姿勢を安定させるため腹筋には大きな負荷がかかる。その重量が増えるにつれ、腹筋のための個別種目では得られないだけの効果をもたらすことができる。

反して、世に広く知られるシットアップにおいては、背骨を曲げることが直接的に腰痛を発症・再発させる原因になる可能性がある。また、重量を伸ばすことにも限界があり、BIG3などの種目に比べて取り組む意義が少ない。

あくまで僕はそう学んできたので、腹筋のために別の種目を取り入れようという気分になることはなかった。毎日BIG3のことだけを意識して、ジムで別のトレーニングに取り組む人が目に入ってもこれっぽっちの影響も受けたことがない。周囲からなんと意見されようとも、僕は自分が正しいということがわかっていた。確実に正しい道の上を歩んでいて、よそ見をする意味なんてどこにもない。その自信は一切の不確定性を含んでいなかった。

腹筋ローラーの力を信じろ。その言葉を目にしたとき、僕はまたいつものように無視をすればいいのだと思っていた。もちろん僕はそれを真に受けない。それでも彼はずっとその言葉を繰り返していた。

僕はそれからも以前と同じようにBIG3だけを続けた。仕事が忙しくてもジムへ行く日は確保できるように努力したので、伸び悩んでいた記録も少しずつ右肩上がりになってきていた。学生のころはもっと重いバーベルを引いていたので、まずはそのころまで体組成を逆戻りさせる必要がある。トレーニーとしてのピークを過去にしてしまうには僕はあまりに若すぎる。

その間も彼はずっと主張を曲げなかった。腹筋しろよ。事あるごとにそう言ってみんなを煽り立てた。発破をかけていたと言った方が正しいのかもしれない。気づけば誰もが腹筋ローラーを転がしていた。気づけば誰もが腹筋をしていた。そして彼がいなくても、誰もが腹筋ローラーの力を信じるようになっていた。

いつからか彼はあまり腹筋ローラーの話をしないようになった。今さらになって、あえて自分がその主張をする意味が見出せなくなってしまったのかもしれない。僕自身も彼が腹筋ローラーの話をしていたことを忘れかけていた。そこにはただ過去に何かをやり遂げた男がいて、また別の方向を向いてなぜか焦燥感を抱えた男がいた。

しばらく会えなくなるかもしれないね。彼はあまりに唐突にそういったので、僕はどうせ冗談か何かだろうと思った。数日経つと、彼は元いた場所から綺麗さっぱりいなくなっていた。なぜだか理由はわからない。きっと彼には彼なりの理由があるのだろう。友達になれたと思っていたけど残念だった。

僕は今まで通りジムには行ってるし、種目はBIG3しかしてない。もちろん他の種目に手を出してみようかと気持ちが揺れることもない。それでもジムにある腹筋ローラーが目に入ると、どうしても彼の姿を思い出してしまう。このジムに来たことはないけど、腹筋ローラーがそこにあるだけで、それを転がしている彼の姿が僕の目の前に浮かぶ。しかし彼は去った。いつか戻ってくるかもしれないけど、今ここにはいない。僕にできることは、この現状を理解した上で彼の帰りを待ち続けることだけだ。

参考文献

状態遷移時にアニメーションを伴うUIのアクセシビリティ周りの実装について

ディスクロージャーの開閉時やモーダルダイアログが表示される瞬間など、あらゆるUIは状態遷移のたびにアニメーションを伴う。にも関わらず、アクセシブルなUIを実装するための手法について書かれた文献で、アニメーションを伴う状態遷移時におけるWAI-ARIAの利用方法というようなテーマが取り上げられているものは見たことがない。UIにおけるアニメーションの役割を踏まえて、それをどのようにして実装すべきだと考えているかについて述べたい。

結論としては、セマンティクス上はアニメーションの存在を意識させないように実装すべきである。多くの場合、アニメーションはUIの状態遷移を視覚的に表現するために存在する。例えばディスクロージャーにおいては、閉じた状態と開いた状態のを擬似的にアニメーションによって表現することで、ユーザーが状態変化前後のビュー(View)の繋がりを理解する手がかりになる。これは視覚を用いてGUIを操作するユーザーのための実装だ。

対して、スクリーンリーダーなどの支援技術を通してウェブサイトを利用するユーザーにとってはこれらのアニメーションに意味はない。UIの状態が変化したときには、音声読み上げなどの方法によって随時通知されるというインターフェイスになっているからだ。そのため、ユーザーの動作を起点とした状態遷移時には、アニメーションを無視して即座に状態変化後のセマンティクスに変更されることが望ましい。でなければ動作と状態変化の関係性が結びつかなくなってしまう可能性があるからだ。

前述の例として取り上げたディスクロージャーを実装に落とし込みながら、より具体的に説明する。次のようなマークアップを基に実装することにする。

<section>
  <h2>
    <button type="button" class="trigger" aria-expanded="true">Content</button>
  </h2>
  <div class="body">
    <p>Lorem ipsum, dolor sit amet consectetur <a href="#">some link</a> adipisicing elit.</p>
    <p>Magni, <button type="button">some button</button> quod minima? Harum, consequatur esse?</p>
  </div>
</section>

.triggerをクリックすると.bodyが開閉する。初期状態では開いているという仕様。aria-controlsは一部のUAでしか実装されていないことに加えて、IDでの指定が運用上難しいため利用しないことが個人的に多い。

その前提の上で、ディスクロージャーを閉じるときに必要な処理は次のようになる。

  • .bodyをスライドアップするアニメーションを開始する
  • .triggeraria-expanded="false"を指定する
  • .bodyaria-hidden="true"を指定する
  • .bodyの子孫要素でTabbable(Tabキーでフォーカスできる)なものにtabindex="-1"を指定する

これによってアニメーションが開始すると同時に、セマンティクス上は閉じている状態になる。アニメーションの完了後もその状態は変化しない。ユーザーの動作後に支援技術には閉じていることが即座に通知されるということだ。

また、タブキーによってフォーカスを移動させるユーザー(支援技術及び一般ユーザー)のために、これから閉じようとしているコンテンツにはフォーカスさせないようにする。もちろん、アニメーションが終了して完全に閉じられた後も同様だ(終了時の.bodydisplay: nonevisibility: hiddenになる場合は不要)。

ディスクロージャーを開くときは先ほどの逆になる。

  • .bodyをスライドダウンするアニメーションを開始する
  • .triggeraria-expanded="true"を指定する
  • .bodyからaria-hidden="true"を取り除く
  • .bodyの子孫要素でTabbableなものからtabindex="-1"を取り除く

コードとしては次のようになるイメージだ。外部ライブラリとしてVelocity.jstabbableを利用している。実際に動くデモも用意した。

import Velocity from 'velocity-animate'
import tabbable from 'tabbable'

const triggerEl = document.querySelector('.trigger')
const bodyEl = document.querySelector('.body')
const tabbableEls = tabbable(bodyEl)
let isExpanded = true

const open = () => {
  Velocity(bodyEl, 'slideDown')

  tabbableEls.forEach(el => {
    el.removeAttribute('tabindex')
  })

  bodyEl.removeAttribute('aria-hidden')
  triggerEl.setAttribute('aria-expanded', 'true')
}

const close = () => {
  Velocity(bodyEl, 'slideUp')

  tabbableEls.forEach(el => {
    el.setAttribute('tabindex', -1)
  })

  bodyEl.setAttribute('aria-hidden', 'true')
  triggerEl.setAttribute('aria-expanded', 'false')
}

const toggle = () => {
  if (isExpanded) {
    close()
  } else {
    open()
  }

  isExpanded = !isExpanded
}

triggerEl.addEventListener('click', toggle)

モーダルダイアログなどの実装もこれと同じ考え方になる。間の状態を視覚的に表現するためのアニメーションは、支援技術及びキーボード操作をするユーザーにとっては存在しないように実装する。慣れれば難しくはない。


UIのユーザビリティを向上させるためにアニメーションは重要な要素だ。しかしそれはある特定のユーザーに対する個別最適化であるが故に、一部のユーザーにとっては使えない要因となってしまう危険性も孕んでいる。アクセシブルな実装が担保できた上で個別最適化を考えることはもっともであるが、その間口を広げる工夫をすることで、より多くのウェブサイトをアクセシブルにするということに前向きに取り組んでいけるはずだ。


この記事は『ライブラリなしで実装する定番UI - ドロワーナビの基本』に触発されて書かれました。素晴らしい記事を届けていただき、ありがとうございます。

【お蕎麦】富士そばへ行く!【安い・早い・うまい】

どうもー! フロントエンドエンジニアのゆうへいでーす! 僕実はですね、今日まだ何も食べてなくて、すごくお腹が減ってるんですよ。ということで今回はね、行きつけの近所の富士そばに行ってみたいと思います!

いやー、それにしても冬は寒くてついつい出不精になってしまいますね。富士そばに行こうって思ってから実際に外に出るまで30時間くらいかかってしまいました(笑)。本当はこれ、富士そば Advent Calendar 2017の23日の記事だったんですが、1日遅れてしまいました、すいませんほんとに、ヘヘッ、フヘヘッ。

ということでね、外に出たんですがね、富士そばは最寄駅のすぐ近くにあるんですが、今住んでる場所から駅までが微妙に遠いんですよね。つまりいつも出勤するたびに微妙に遠い距離を歩いてるんですが、これを蓄積していくとすごい人生の時間の無駄だなーと。まあ最寄駅まで歩いて移動するくらいなら運動にもなるしいいかなと思えるんですが、今毎日通勤するのにDoor to Doorで50分くらいかかってるんですよね。都心の割には短い方みたいなんですが、それでも毎日移動で2時間近く使ってるって考えたら大変なことですよね。今住んでる部屋はそんなに気に入ってるわけじゃないんで、会社の近くに引っ越したいなとは思いつつ、引っ越すほどお金もないし、家賃上がっちゃってもなーという感じなんですよねー。東京じゃないとやりたいお仕事するのも難しそうだし世知辛い

そんなことを考えながら歩いているうちに富士そばに到着です! 期間限定メニューで「あさりそば」とか「合鴨ミニ丼」とかがあるみたいですね! えーっとどれにしようかな……、じゃあ僕このそばと合鴨ミニ丼のセットに決めました!

食券の写真

あさりとも迷ったんですが、肉にとにかく惹きつけられてしまって、そんなもんどうせしょぼいのにと思いつつこれにしてしまいました。よく行くからわかってるんですが、ミニ丼ってだいたいほんとにしょぼいんですよね。でもそんなこと言いつつも選んでしまう僕。正直飯とかだいたい食えればまあなんでもいいんですよね。

そばと合鴨ミニ丼の写真

ということですぐ僕の食券の番号が呼ばれてこれを食べました、味はいつもの富士そば味って感じです。

それで食べ終わったんですが、なんせ30時間ぶりくらいの食事でまだお腹が空いていたのでマックでチキンナゲット及びその他を買いました。

チキンナゲット15ピースとマックフライポテトの写真

このツイートをして以来ずっと久しぶりに食べたいなと思ってたんですが、近所のマックがずっと休業中だったんですね。今日確認してみると営業再開してたので、満を持してというところでした! 味はいつものマック味って感じです。ポテトも買ったのは完全に蛇足でした! 消化試合感! いぇい!

Reactコンポーネントを単独で使うための細かいテク

前回のReactコンポーネントを単独で使うに書いたようにしばらくやってみて、細かいところのいい感じのやり方がわかってきた。

ディレクトリ構成

└── src/
    ├── components/
    │   ├── react/
    │   │   ├── AwesomeApp.js
    │   │   └── Disclosure.js
    │   ├── AwesomeApp.js
    │   ├── Disclosure.js
    │   └── GlobalNavigation.js
    └── main.js

初期化を担うファイルはcomponents/の直下に、Reactコンポーネントcomponents/react/に配置する。Reactコンポーネントを初期化する処理は、components/にファイルを作成した上でそこに書く。Reactじゃないコンポーネントも同じディレクトリに配置する。

stateを制御するコンポーネントでラップする

プレゼンテーショナルなReactコンポーネントには、トップダウンで状態を渡すことが多い。

const Disclosure = ({ isExpanded, onToggle }) => (
  <div className="Disclosure">
    <button
      className="Disclosure__toggle"
      type="button"
      aria-expanded={String(isExpanded)}
      onClick={() => onToggle(!isExpanded)}
    >
      toggle
    </button>
    <div
      className="Disclosure__content"
      hidden={!isExpanded}
    >
      {children}
    </div>
  </div>
)

これを単独で機能させるというのをやりやすくするために、簡単にstateを管理できるコンポーネントを作る。あるいはreact-valueがちょうど良い。

import React from 'react'
import ReactDOM from 'react-dom'
import { Value } from 'react-value'
import Disclosure from './react/Disclosure'

export const init = () => {
  document.querySelectorAll('.react-Disclosure').forEach((containerEl) => {
    const initialExpanded = containerEl.dataset.open === 'true'
    ReactDOM.render(
      <Value
        defaultValue={initialExpanded}
        render={(value, onChange) => (
          <Disclosure
            isExpanded={value}
            onToggle={onChange}
          />
        )}
      />
      containerEl,
    )
  })
}

クラス名の命名規則

ReactコンポーネントReactDOM.render()で描画する以上、マウントする対象の要素が必要になる。そのため、単独で利用するReactコンポーネントには必ずラッパー要素ができる。

コンポーネントのクラス名が.Disclosureの場合、そのラッパー要素のクラス名は.react-Disclosureにする。このラッパー要素は、利用方法によって存在したりしなかったりするので、あってもなくても同じ振る舞いをするように実装する。

ラッパー要素のスタイル宣言は、内包するコンポーネントと同じファイルに記述する。

_Disclosure.scss:

.react-Disclosure {
  ...
}

.Disclosure {
  ...
}

data-*属性をコンポーネントに渡すpropsとして利用する

上記の例でもそうしたけど、ラッパー要素のdata-*属性にReactコンポーネントへ渡すpropsを設定しておくと便利。

<div class="react-Disclosure" data-open="true"></div>

propsへHTMLを渡したいときは、例えばPugなら次のようにすればいい。

div.react-Disclosure(hidden)
  ul
    li foo
    li bar

innerHTMLをpropsに渡しつつ、描画が終わったらhidden属性を取り除く。あまりきれいじゃないけど。

配列やオブジェクトを渡したいときは次のようにする。

- const array = JSON.stringify(['foo', 'bar', 'baz'])
- const object = JSON.stringify({k: 'val'})
div.react-MyComponent(data-array=array data-object=object)

data-*属性の値をJSON.parse()してpropsに渡す。無駄感はあるけど、テンプレートの管理の楽さとかを考えるとまあこれでいいかなという感想。

Reactコンポーネントを単独で使う

普通の静的なHTMLのサイトの中で、限定的に複雑になる部分だけをReactの小さいアプリとして実装するというパターンを個人的によくやる。その際に、Reactの中でもそれ以外の部分でも使うコンポーネントがあって、どう実装すればいいかと悩んだ。

というのも、普通のJSでの実装をReactで再利用できるように作るのは、初期化とかライフサイクル的に大変だ。あるいはもし、同じコンポーネントをそれぞれの専用に実装すると、お互いの挙動を一貫させるのが大変だし、UIの仕様が変わったときには追従に倍の手間が掛かる。

どうするもんかなと考えると、共有するコンポーネントはReactで実装して、利用する箇所ごとにReactDOM.render()すればいいことに気づいた。単純。

例えば、次のようなコンポーネントを共有したいとする。

import React from 'react'

export default class Disclosure extends React.PureComponent {
  render() {
    return ...
  }
}

Reactアプリの外で利用するには次のようにする。

<div class="js-react-disclosure" data-heading="Section heading">
  <p>Lorem ipsum dolor...</p>
</div>
import React from 'react'
import ReactDOM from 'react-dom'
import Disclosure from '../react/Disclosure'

document.querySelectorAll('.js-react-disclosure').forEach((el) => {
  const heading = el.dataset.heading
  const children = el.innerHTML
  ReactDOM.render(
    <Disclosure heading={heading}>{children}</Disclosure>,
    el,
  )
})

簡単な例はGitHubに上げた

propsの渡し方については、PugでJSONのデータをHTMLにJSON.stringify()して埋め込んでJS側でパースするとかでも良さそう。

全部Reactで(別にVue.jsでもいい)できたらなーとは思うものの、要件によってはそれが効率的でないことも多い。Custom ElementsとShadow DOMが使えれば完璧に解決するんだけど、ポリフィルもバグがあったり欲しい機能が実装できなかったりで厳しそうだし、10年後かなー、あー、みたいなことを最近は永遠に考えてる。コールドスリープしたい。

フォーカスリングの役割とマウスユーザーに向けた対応について

ブラウザは、フォーカスされた要素を可視化するためにフォーカスリングを実装している。青や黒のフォーカスされた要素を囲う枠線のことだ。outlineプロパティで表現される。これは主に、ウェブページをキーボードで操作可能にするためにある。

例として、ウェブページ上のボタンをクリックするという場面を想定する。キーボードによる操作では、まず、Tabキーなどによってフォーカスの位置を移動させながら、フォーカスを目的の要素まで辿り着かせる必要がある。その上で、スペースキーやエンターキーによってクリックに相当するアクションを実行する、というような流れになる。このような操作を行うためには、どの要素がフォーカスされているかを、ユーザーが視覚的に認識できることが前提になる。フォーカスリングが実装されているおかげで、開発者は適切なマークアップを行うだけでフォーカスを可視化することができる。

しかしながら、マウス操作を行うユーザーにとってはフォーカスの可視化は重要でないと言えそうだ。キーボードユーザーは、フォーカスを経てアクションを実行することが多い。だが、マウスユーザーは、要素をフォーカスさせるという行動を経由する必要が無いため、フォーカスを意識する必要性が低い。むしろ、フォーカスを意識していないため、フォーカスが可視化されているために操作上での混乱を呼ぶという可能性もある。

ブラウザはフォーカスリングを必要に応じて表示する。<button>要素の場合、マウスでのクリックによってフォーカスされた際にはフォーカスリングは表示されない。だが、キーボード操作によってフォーカスされた際にはフォーカスリングが表示される。これは前述したように、マウスユーザーにとってはフォーカスが可視化されている必要性が低いという考えに基づいている。とは言うものの、これは開発者によってスタイルが設定されてない場合の<button>要素の挙動であり、任意のスタイルを設定した途端、入力デバイスに関わらずフォーカスリングが表示されるようになってしまう。
一方、テキストフィールド(textタイプの<input>要素など)の場合、フォーカスリングは入力デバイスに関わらず表示される。これは、マウスによってフォーカスされようとも、キーボードによる入力が続く可能性が高いためだと考えられる。キーボード入力を行う以上、ユーザーはマウス操作時ほど明確に入力対象の要素を認識できない。そのため、キーボード入力が主になるUIの場合、フォーカスするために用いられた入力デバイスに関わらず、フォーカスリングは表示すべきだろう。また、項目数の多いフォームへ入力を行う際は、どのフィールドに対して入力途中であるかが明確になり、一時的にフォームから注意がそれてしまっても容易に入力を再開できるという利点もある。

以上の事情を考慮すると、ボタンはキーボード入力時のみ、テキストフィールドは常にフォーカスリングが表示されて欲しい。幸いにも現在のところ、このようにフォーカスリングを表示すべき場合にだけマッチする:focus-ring疑似クラスが提案されている:focus-visibleにリネームされた。これが実装されると、次のような宣言によって求めている挙動を実現できる。

:focus {
  outline: none;
}

:focus-ring {
  outline: 2px dotted dimgray;
}

マウス入力の際にどの要素がマッチしないかという判断に関しては、Firefox:-moz-focusring疑似クラスという独自実装や、focus-ringポリフィルを参考にできる。

残念ながら、:focus-ring疑似クラスはまだ実装されていない仕様であるため、代わりにJavaScriptを用いてそれに相当するような実装をすることになる。What Input?というライブラリを利用すれば、入力デバイスを検出してCSSから参照することができる。検出した入力デバイス<html>要素のdata-*属性に反映されるため、セレクタとして次のように利用するパターンが考えられる。

@mixin focus-with-keyboard {
  html[data-whatinput="keyboard"] &:focus {
    @content;
  }
}

@mixin focus-without-keyboard {
  html[data-whatinput="initial"] &:focus,
  html[data-whatinput="mouse"] &:focus,
  html[data-whatinput="touch"] &:focus {
    @content;
  }
}

@mixin focus-clear {
  outline: none;
}

@mixin focus-clear-without-keyboard {
  @include focus-without-keyboard {
    @include focus-clear;
  }
}
// キーボード操作時以外はフォーカスリングを非表示
.my-button {
  @include focus-clear-without-keyboard;
}
// デフォルトのフォーカスリングを非表示、キーボード操作時のみ独自のフォーカススタイルを適用
.my-button {
  @include focus-clear;

  @include focus-with-keyboard {
    background-color: lightgray;
    outline: 2px solid red;
  }
}
// デフォルトのフォーカスリングを非表示、常に独自のフォーカススタイルを適用
.my-text-field {
  @include focus-clear;

  &:focus {
    outline: 3px solid green;
  }
}

少なくないウェブサイトからフォーカスリングが取り除かれてしまっているのは、マウスユーザーに対する個別最適化の結果だろう。そもそも開発者でさえフォーカスを意識していないことすら考えられる。これらは、フォーカスの明示がマウスユーザーに混乱を与えているということの結果ではないか。とすると、フォーカスリングの見た目が悪いから代替のスタイルを設定する、というような話は論点がズレていて、フォーカスを意識する必要のないユーザーに対してもフォーカスを明示していたことが間違いだったと考えるべきではないか。

キーボードユーザーにのみフォーカスリングを表示するという実装方法が、これからのデファクトになっていくべきだと僕は思う。

cssnextを使うべきか

cssnextは、未来のCSS構文を今のブラウザでも解釈できるようにトランスパイルするPostCSSプラグインだ。そう聞くとさも、将来のCSSの書き方をそのまま先取りできる素晴らしいツールであるような印象を抱く。だが実際は、cssnextで表現できる形と標準の仕様は大きく異なっていることがある。cssnextを前提にして書いたコードは、未来のブラウザで違う挙動をする可能性があるということだ。

cssnextは、単一の機能を持ついくつかのPostCSSプラグインをまとめたプラグインセットだ。それぞれのプラグインは単に、ある構文を現在のブラウザでそれっぽく動くコードに変換することしかできない。対象とされている構文の多くは、現状の実装でフォールバックすることが不可能であるため、プラグイン作者の主観に基づいたなんちゃって実装に置き換えるしかないのだ。
そのため、cssnextを通して利用できる構文のいくつかは、実際の仕様と異なった形で利用する必要があり、仕様と異なった挙動をするコードを生成する。

標準の仕様と異なった利用方法をするプラグインとして、postcss-custom-propertiesは顕著な例である。カスタムプロパティは、動的でDOMスコープのプロパティであるという仕様だ。対してこのプラグインでは、プリプロセッサ上でのみ有効な変数として解釈されて、Sassのように静的な値に置き換えられる。これではカスタムプロパティを利用する意味が変わってしまうので、本来の仕様の意図から外れたコードを書かざるを得ない。

ほとんどのプラグインは、仕様通りの挙動を実現できないにも関わらず、無理やり標準の構文を利用することを目的にしてしまっている。それによってもたらされる弊害は、標準の構文を利用できるという利点よりも大きいだろう。
ユーザーが標準の仕様を誤解してしまうことも考えられるし、コードが仕様通りに動かないことを理解するためにプラグインの知識が必要になることもある。

また、標準と言えども破棄されてしまう仕様もある。仕様を追い続けた上で、利用していた構文が使えなくなったと決まれば、既存のコードを書き直す必要もあるだろう。

だったら無理に未来の構文に憧れを抱くのではなく、プリプロセッサの独自構文に身を委ねてしまったほうが安心できるはずだ。幸いSassは市民権を得た当たり前の言語になった。極端なコードを書かない限り、Sassに依存していることは大きな問題にならない。プリプロセッサ無しでみんなが書きたいCSSを書けるようになるのは、どうせかなり先の未来だろうし。


蛇足ながら、妥当に利用できそうなプラグインについても考えた。基準は次のふたつ。

  • 未来の仕様と全く同じ挙動を実現できる(単に糖衣構文である)
  • 仕様自体が破棄される可能性が高くない(Working Draft以降)

これに基づいて次のプラグインを選択できた。

列挙した全てのプラグインがベースにしている仕様は、現在Working Draftだ。慎重に考えるとどのプラグインも利用すべきではないのかもしれない。とは言え、僕にも未来の構文を使いたいという願望はある。ちょっとした個人プロジェクトでは利用してみたい。