【お蕎麦】富士そばへ行く!【安い・早い・うまい】
どうもー! フロントエンドエンジニアのゆうへいでーす! 僕実はですね、今日まだ何も食べてなくて、すごくお腹が減ってるんですよ。ということで今回はね、行きつけの近所の富士そばに行ってみたいと思います!
いやー、それにしても冬は寒くてついつい出不精になってしまいますね。富士そばに行こうって思ってから実際に外に出るまで30時間くらいかかってしまいました(笑)。本当はこれ、富士そば Advent Calendar 2017の23日の記事だったんですが、1日遅れてしまいました、すいませんほんとに、ヘヘッ、フヘヘッ。
ということでね、外に出たんですがね、富士そばは最寄駅のすぐ近くにあるんですが、今住んでる場所から駅までが微妙に遠いんですよね。つまりいつも出勤するたびに微妙に遠い距離を歩いてるんですが、これを蓄積していくとすごい人生の時間の無駄だなーと。まあ最寄駅まで歩いて移動するくらいなら運動にもなるしいいかなと思えるんですが、今毎日通勤するのにDoor to Doorで50分くらいかかってるんですよね。都心の割には短い方みたいなんですが、それでも毎日移動で2時間近く使ってるって考えたら大変なことですよね。今住んでる部屋はそんなに気に入ってるわけじゃないんで、会社の近くに引っ越したいなとは思いつつ、引っ越すほどお金もないし、家賃上がっちゃってもなーという感じなんですよねー。東京じゃないとやりたいお仕事するのも難しそうだし世知辛い。
そんなことを考えながら歩いているうちに富士そばに到着です! 期間限定メニューで「あさりそば」とか「合鴨ミニ丼」とかがあるみたいですね! えーっとどれにしようかな……、じゃあ僕このそばと合鴨ミニ丼のセットに決めました!
あさりとも迷ったんですが、肉にとにかく惹きつけられてしまって、そんなもんどうせしょぼいのにと思いつつこれにしてしまいました。よく行くからわかってるんですが、ミニ丼ってだいたいほんとにしょぼいんですよね。でもそんなこと言いつつも選んでしまう僕。正直飯とかだいたい食えればまあなんでもいいんですよね。
ということですぐ僕の食券の番号が呼ばれてこれを食べました、味はいつもの富士そば味って感じです。
それで食べ終わったんですが、なんせ30時間ぶりくらいの食事でまだお腹が空いていたのでマックでチキンナゲット及びその他を買いました。
さっきジムのテレビで見たんですが、今チキンナゲットがお得らしいです #フロントエンドゆるふわ筋トレ部
— 超一流HTMLコーダー(55) (@_yuheiy) 2017年12月12日
このツイートをして以来ずっと久しぶりに食べたいなと思ってたんですが、近所のマックがずっと休業中だったんですね。今日確認してみると営業再開してたので、満を持してというところでした! 味はいつものマック味って感じです。ポテトも買ったのは完全に蛇足でした! 消化試合感! いぇい!
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, ) })
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以降)
これに基づいて次のプラグインを選択できた。
- postcss-media-minmax(Sassでは構文エラーになるので採用不可)
- postcss-pseudo-class-any-link
- postcss-selector-matches
- postcss-selector-not
列挙した全てのプラグインがベースにしている仕様は、現在Working Draftだ。慎重に考えるとどのプラグインも利用すべきではないのかもしれない。とは言え、僕にも未来の構文を使いたいという願望はある。ちょっとした個人プロジェクトでは利用してみたい。
日本語向けフォントスタックの現状
日本語のウェブサイト向けのフォントスタックの現状と無難な設定についてまとめた。sans-serif
、serif
、system-ui
のそれぞれの総称フォントファミリーに基づいて、主要な端末(Windows、Mac、iOS、Android)のフォントの搭載状況を整理する。
sans-serif
まず、Windowsはメイリオ一択だと考えたほうが良い。游ゴシックはWindows 8.1ではかなり細く、Windows 10でも一般的なフォントと比べると少し細いのが問題だ。ハック的に回避する方法はあるものの、積極的に採用したくはない。メイリオはWindowsユーザーにとって馴染みがあり、最も問題になりにくいフォントだと考えられるため、あえて別の選択をする必要性は低いと思う。
Yu Gothic UIという選択肢もあるが、本文向きでは無さそうだ。
Macでは問題なく游ゴシックが利用できるので、ヒラギノ角ゴシックかいずれかを選択できる。
ヒラギノ角ゴシックを選択する場合、ファミリーの名称はこれまでのようにHiragino Kaku Gothic ProN
とするか、新しいHiragino Sans
とするかが悩みどころになる。前者はウェイトのバリエーションがW3とW6の2段階しかなく、後者はW1からW9までの9段階あるのが違いだ。ウェイトの多い別のフォントとの併用を考えると、Hiragino Sans
を指定しておく方が見た目の印象を一貫させやすいだろう。
注意点は、Hiragino Kaku Gothic ProN
をHiragino Sans
にそのまま置き換えると違うウェイトになることがあるということ。CSSでfont-weight: normal
が指定されていると、前者ではW3が適応され、後者ではW4になる。bold
の場合、前者ではW6、後者ではW7だ。ウェイトのバリエーションが増えたことにより、CSSの指定に対応するフォントが変わるということだ。あえてW3を利用したい場合、font-weight: 300
と指定する必要がある。どのウェイトが最適かは場合によりけりだが、これまで慣れた見た目と違うものになり得ることは頭に入れておいたほうが良い。
ただし、ウェイトのバリエーションが増えるのはMacのみで、iOSはこれまでと同じくW3とW6だ。
これらを踏まえると次のような指定になる。
$font-stack-sans-serif: "Hiragino Sans", "Meiryo", sans-serif;
Windowsの主要なブラウザでは、sans-serif
を指定するとメイリオが選択される。だが、Windows 7のIE11に限ってはMS Pゴシックで表示される。それを上書きするために指定する必要がある。
また、古いバージョンのMS Office(Office for Mac 2011?)をインストールすると、Macでもメイリオが有効になってしまう。Macではメイリオが選択されてしまわないようにするため、ヒラギノ角ゴシックを先に指定する。
Macで游ゴシックを利用したい場合は次のようになる。
$font-stack-sans-serif: "YuGothic", "Hiragino Sans", "Meiryo", sans-serif;
iOSには游ゴシックがインストールされていないため、ヒラギノ角ゴシックで表示されるように指定する。
Hiragino Sans
はOS X El Capitan及びiOS 9以降から搭載されたフォントなので、それ以前のバージョンもサポートするには、Hiragino Sans
に続いてHiragino Kaku Gothic ProN
も指定する必要がある。
serif
Windowsには游明朝を利用させる。游ゴシックと同様に細く見える問題はあるが、MS P明朝と比べるとかなりいい状態で読める。現在のChromeではserif
の既定のフォントは游明朝になっており、Firefox 57以降でも同じように変更されることになっている。
Macではヒラギノ明朝か游明朝を選択できる。ヒラギノ明朝を利用する場合は次のようになる。
$font-stack-serif: "Yu Mincho", serif;
游明朝を利用する場合は次のようになる。
$font-stack-serif: "YuMincho", "Yu Mincho", serif;
遊書体はWindows 8.1以降から搭載されたフォントであるため、Windows 7ではMS P明朝になる。
また、Android端末の場合、serif
に対応するフォントは搭載されていない。ウェブフォントを利用するか、諦めてsans-serif
等で代用するしかない。
system-ui
system-ui
はsans-serif
などと同じ総称フォントファミリーだ。CSS Fonts Module Level 4で追加された。プラットフォームのUIと同じフォントを利用できる。現状ではChromeのみで実装されており、その他のブラウザのためにはフォールバックを書く必要がある。
フォールバックを含めた指定方法はいろいろ紹介されているが、英語圏のブログに書かれているような指定は日本語向きではなさそうだ。
Chromeでsystem-ui
を指定した場合、MacとiOSではSan Franciscoになる(ヒラギノ角ゴシックは含まれていない)。Windows 10ではYu Gothic UI
で、Windows 8.1以前ではSegoe UI
とMeiryo
の混植。AndroidではRoboto
とNoto
の混植。
ブラウザをまたいでもこれと同じ結果にするためには、次のように指定する。
$font-stack-system-ui: system-ui, -apple-system, "Hiragino Sans", "Yu Gothic UI", "Segoe UI", "Meiryo", sans-serif;
それぞれの値の意味は次のようになる。
$font-stack-system-ui: // 1. OS X Chrome(欧文)、Windows Chrome(和文・欧文)、Android Chrome(和文・欧文) system-ui, // 2. OS X Safari(欧文)、iOS Safari(欧文)、OS X Firefox(欧文) -apple-system, // 3. OS XとiOS全て(和文) "Hiragino Sans", // 4. Windows 10 Chrome以外(和文・欧文) "Yu Gothic UI", // 5. Windows 8.1以前 Chrome以外(欧文) "Segoe UI", // 6. Windows 8.1以前 Chrome以外(和文)(Windows 7のIE11用の指定) "Meiryo", // 7. その他 sans-serif;
Mac及びiOSの場合、1か2の指定でSan Franciscoを利用できる。それだけだと和文の指定が無いので3を指定する。
Windows 10の場合、Chromeは1の指定、その他のブラウザは4の指定でYu Gothic UI
になる。Yu Gothic UI
自体にSegoe UI
が含まれているため、あえてSegoe UI
を指定する必要はない。
Windows 8.1以前の場合、Chromeは1の指定、その他のブラウザは5と6の指定でSegoe UI
とMeiryo
の混植になる。
Android Chromeは1の指定でRoboto
とNoto
の混植になる。
San FranciscoはOS X El Capitan及びiOS 9以降から搭載されたフォントなので、それ以前のOSもサポートすると、Hiragino Sans
の前にHelvetica Neue
の指定が必要。加えて、Hiragino Sans
も同じタイミングで搭載されたフォントなので、その後ろにHiragino Kaku Gothic ProN
の指定が必要になる。
参考
よくある「font-family
の最適な指定」系の記事を見ると、違う文脈のファミリーをひとつのフォントスタックに押し込んで、自分の好きな順番に並べただけのようなまとめ方が多いと思っていた。なのでこの記事では、総称フォントファミリーを基準にした一般的(汎用的)な分類でまとめた。自分の主観に依らない視点になっていると嬉しい。
:focus-ringの代用としてwhat-inputを試す
前回の記事で紹介した:focus-ring
のポリフィルはイマイチだった。element.focus()
で制御したときにいい感じにならないというつぶやきを見て知った。
具体的な例として、モーダルを閉じた後の挙動について考えたい。
モーダルを閉じた後は、フォーカスはモーダルを開いたボタンに戻るように実装する。そのために、button.focus()
のようにしてフォーカスをスクリプトで制御する。この際、:focus-ring
のポリフィルだとfocus-ringが有効になる処理が実行されず、キーボード操作をしていても適切なスタイルが表示されないことになる。
調べてみるとWhat Input?という似たライブラリがあった。ユーザーの入力端末を検出する機能があり、同じくfocus-ringの制御をすることが目的らしい。:focus-ring
のポリフィルとは違い、入力端末の検出結果をhtml
要素の属性などを通して公開している。フォーカスのスタイルはそれに基づいて設定すればいい。これを利用すれば、意図した通りにフォーカスのスタイルを機能させることができた。
提供される状態は、initial
、mouse
、keyboard
、touch
のいずれかだ。initial
はまだ検出できていないことを示す。マウスとタッチデバイスではoutline
を非表示にしたいので次のようにする。
[data-whatinput="mouse"] :focus, [data-whatinput="touch"] :focus { outline: none; }
また、E:focus-ring
と同じ意図を示すセレクタは次のようになる。
html:not([data-whatinput="mouse"]):not([data-whatinput="touch"]) .awesome-button:focus { // focus-ring style }
ただこれでは冗長だ。E:hover
と併記したいことも考えると、Sassで次のように抽象化できる。
@mixin focus-ring() { html:not([data-whatinput="mouse"]):not([data-whatinput="touch"]) &:focus { @content; } } .awesome-button:hover { background-color: red; } .awesome-button { @include focus-ring() { @extend .awesome-button:hover; } }
多分これで問題なく:focus-ring
風の実装ができるはずだ。将来的に未知の入力端末が登場しても、マウスとタッチデバイス以外ではフォーカスのスタイルが表示されるため、ウェブサイトが操作不能になる可能性は低いと思う。
CSSのセレクタが複雑になってしまう問題はあるが、フォーカスのスタイルを非表示にしたいという要望とのトレードオフだろう。
本当は:focus-ring
がネイティブで実装される日を待ちたいが、これが今のところの現実解なんだと思う。