2 ポイント 投稿者 GN⁺ 2024-12-06 | 1件のコメント | WhatsAppで共有
  • @stanko/dual-range-input は 2 つの ネイティブ HTML range input を組み合わせて二重範囲スライダーを作成し、ブラウザー標準のインタラクションとアクセシビリティをそのまま活用する
  • 実装は入力値を同期する約 50行の JavaScript と CSS が中心で、ドラッグやアクセシビリティを自前で再実装する従来方式よりシンプル
  • 2 つの入力は選択値の 中間点 に合わせて minmax、幅が更新され、トラックをクリックすると近い方の thumb がその値へ移動する
  • range input のトラックは実際の入力幅より短いため、thumb 幅を計算に加え、wrapper の padding で超過幅を吸収する
  • テーマは CSS 変数と gradient で調整でき、選択範囲の色・thumb サイズ・track 色などをオーバーライド可能

ネイティブ range input で作る二重スライダー

  • @stanko/dual-range-input は 2 つの HTML range input を横に並べて二重範囲入力を実現する
  • ここでいう「ネイティブ」は単一のブラウザーコントロールという意味ではなく、2 つの range input を使ってブラウザーの標準インタラクションと アクセシビリティ機能 を保持するという意味
  • 実装は入力を同期する約 50行の JavaScript と、見た目を整えるための CSS で構成される
  • 出発点は、生成系ドローイングツールでパラメーターの最小値と最大値を調整する UI が必要だったこと
  • 要件は 2 つだった
    • ネイティブ HTML range input を使うこと
    • トラックをクリックしたら、2 つの thumb のうちクリック位置により近い方がその値へ移動すること

値の同期とクリック処理

  • 2 つの入力のどちらかが変わると、ライブラリーは選択された 2 つの値の 中間点 を計算する
    • 各入力の minmax 属性を中間点に合わせて設定する
    • 2 つの入力の幅も中間点基準で更新する
  • 入力が中間点で合流するようにサイズ調整されるため、2 つの thumb の間をクリックするとクリック位置に近い thumb が移動する
    • 2 つの thumb の間の step 数が奇数なら、最後に使った入力が優先される

CSS の計算とスタイリング

  • range input のトラックは入力全体の幅より短く、ブラウザーは thumb が外にはみ出さないよう両端に余白を残す
    • 入力幅を単純に 25% と 75% のように分けると、実際のトラック比率が合わない
    • 実装では各入力幅に --dri-thumb-width を加え、wrapper に padding を追加して超過幅を収める
  • スタイリングにはブラウザーの range input CSS スタイリング機能を使う
    • track と thumb を装飾し、2 つの track が接する中央部分には border radius が付かないようにしている
  • テーマは CSS 変数のオーバーライド で作れる
    • 主な変数には --dri-height--dri-thumb-width--dri-thumb-color--dri-track-color--dri-track-filled-color--dri-track-border-radius などがある
  • 選択された範囲は CSS gradient で塗る
    • gradient の位置は --dri-gradient-position 変数で指定する
    • コードで入力幅を更新するとき、この変数も一緒に更新する
  • この実装は、カスタムライブラリーでドラッグやアクセシビリティを再実装する代わりに、可能な限り ネイティブ要素 をまず活用するアプローチ

1件のコメント

 
GN⁺ 2024-12-06
Hacker News のコメント
  • この種のスライダーでは、中央の領域も常にドラッグ可能であるべきだと思う。つまり中央をつかむと、両側のハンドルが同時に動く方式であるべき
    参考までに、Unity はこう処理している: https://docs.unity3d.com/StaticFiles/ScriptRefImages/EditorG...

    • 逆に、自分なら中央の領域をクリックしてみようとはまったく思わなかったはず。頭の中では、これはまったく新しいウィジェットではなく、変形したスライダーに見えたから
      「常に」は少し言い過ぎかもしれない
  • 作者です。HN に載ってうれしい
    ここで issue やバグ報告が上がり始めているのを見ました。こちらはもう遅いので、明日確認しようと思います
    生成画像の作業で必要になって作りました。数値を入力するより、スライダーをドラッグしながら画像がどう変わるかを見るほうが好みです
    できるだけミニマルに保つよう努めました

    • この記事は本当に良かった。朝のコーヒーを飲みながら、計算過程とデバッグモードの説明を楽しく読めたし、ネイティブコンポーネントを賢く活用したやり方だと感じた
      今は SwiftUI を使っているけれど、独自コンポーネントを作るのがほとんど簡単すぎるくらいになっている。ただ作者が説明しているように、それが常に最善とは限らず、ネイティブコンポーネントは機能がかなり充実していることが多い
      iOS の Firefox というやや珍しいブラウザでも記事を確認してみたが、ちゃんと動作した。とてもよくできている
  • 直接使ってみたらすぐにバグに遭遇した。スライダーを 100 - 100 にドラッグしたあと、99 - 99 にしようとしてもできない
    99 - 100 からも行けず、98 - 99 からも行けない。99 - 99 に行く唯一の方法は、下限を 98 よりさらに下まで引き下げること
    98 - 99 の状態から 97 - 99 に下げると、そのときは 99 - 99 に行ける
    このバグはどこでも起こり得て、99 - 99 がスライダーの端に近いので最も再現しやすいだけ。一般的には、ある X - X の状態から片方の端を 1 段階動かすと、もう片方の端をその隙間の中へ引き込めない。その間隔を閉じるには、まずもう片方の端をさらに遠くへドラッグする必要がある

    • よく見つけた! 実はこのケースは考えていて、Chrome と Firefox では動くのだが、Safari はマウスダウン時にfocus イベントを発火せず、スライダーを動かし始めてから発火するようだ
      報告ありがとう。必ず修正する
    • デスクトップ Linux の Chromium では 100-100 → 99-100 → 99-99 が問題なくできる
      代わりに別の視覚的なバグがある。23-25 から始めて 24-25 にドラッグすると、25 にある右のハンドルが数ピクセル動く。左のハンドルを 23 に戻すと、右のハンドルも戻る
    • だから自分は開発者ツールを作るのが好きなんだ
  • すばらしい! HTML input にはずっと2 値の範囲入力の実装が必要だと思っていた。かなり筋が通っているから
    たとえば「ウィジェットが少なくとも X 個以上、最大 Y 個以下の製品をフィルタしたい」といったユースケースがある
    ただし Firefox で強調しておきたいバグを見つけた。最小または最大ハンドルをクリックするたびに、値が 1 段階上下に変わる
    ハンドルの正確な中央をクリックしようとすると値は変わらないが、少し左をクリックすると選択値が 1 段階減り、少し右をクリックすると増える
    ボタンを押したままマウスポインタを実際に動かすという明示的な変更操作をしたときだけ値が変わるのが期待される動作だ

    • 正直、それはバグではないと思う。どの例もかなり細かく刻まれていて、ハンドルがトラック上の複数ステップを覆っている
      Firefox はそうしたクリックを、トラックを直接クリックしたものとして解釈する。このライブラリはそこにはまったく介入しておらず、単にFirefox の動作方式
  • キーボードで動作する点がすばらしい。きちんと動いていて、Shift-Tab を押し続ければ後ろにも移動できる
    本当によくできているし、とても小さな点ではあるが、そのインタラクションを正確に実装してくれたことに感謝している

  • うちの会社のデザインシステムでも、インターンがこのように横に並べた概念実証を作ったが、片方の input のピクセルが少しずつ跳ねる現象がずっとあり、結局きちんと作れなかった
    そこで 2 人目のインターンが、重ねた 2 つの input range を使う解決策を出した

  • 範囲入力が必要になるたびに jQuery UI スライダーを使っていて、スライダー 1 つのためにページへ jQuery と jQuery UI を追加しなければならなかった。これは本当に良い
    追記: ESM モジュールではなく、すぐ使える通常の JS ファイルを事前に用意してもらえる可能性はあるだろうか?

  • バグをここで報告してよいのかわからない。最初の 2 つの例のハンドルはタッチ操作を横取りしてスクロールを妨げる。後ろの 3 つの例はそうではない
    その副作用として、指の下にあるラベルを見えるように上へドラッグできず、ページがスクロールされてラベルがずっと指の下に残る

    • 再現できるなら GitHub issue を開いてくれると助かる [1]
      どのブラウザと OS を使っているのか気になる。このライブラリはネイティブ input を使っているので、スクロールのブロックに関しては何もしていない
      自分の iOS の携帯では、スタイル適用の有無に関係なく、すべての input[type=range] 要素でスクロールがブロックされる
      [1] https://github.com/Stanko/dual-range-input/issues
  • 「ネイティブ」という部分には議論の余地がある。ライブラリが 2 つのネイティブ HTML range input を使うのでネイティブなインタラクションとアクセシビリティ機能が保たれる、という意味だそうだが、自分の基準は明確だ
    意図したとおりに動作するのにJavaScript が必要なら、もはやネイティブではない

    • 同意する。普通もそういう意味で使うのではないかと思う
      それでも作者の意図は理解できる。JS で動く div の塊を自前実装する代わりに通常のスライダーを組み合わせたので、ネイティブな解法にはずっと近づいている。賢い CSS で JavaScript を置き換えられないなら、おそらく可能な限り最も近い方式だろう
      ネイティブかどうかとは別に、この種のウィジェットは使えれば非常に有用で、その領域には実装が不足していた
    • 普通はそうだが、完全に明確に分かれるわけではない。要素はネイティブでも、モーダル状態で開くには JavaScript を使う必要がある場合がある
      popover 属性が出る前は、JavaScript なしではそもそも開けなかった
      またフォーム検証のように、ブラウザ内部の検証ツールをまったく使わないライブラリとは区別して見る価値のあるネイティブなバニラ JavaScript メソッドもある
  • https://spodj.intermer.net/ を作ったとき、こういうものを探していた
    必要な機能を得るために React を雑に使わなければならなかったが、それは自分がフロントエンド開発にあまり強くないからだった