Chromeでボタンをクリックしたときのフォーカスリング
geckotangが「Chromeでボタンをマウスでクリックしたときにフォーカスリングが出たり出なかったりするのなんで」と言っていて、なんかお前調べろよ的な感じだったので調べることにした。
使うのはcs.chromium.org。<button>
なので"HTMLButtonElement"で検索して、それっぽいファイルを探す。あった。
"focus"で検索するとSupportsAutoFocus()
というメソッドが見つかる……がこれはautofocus
属性の対象になるかとかそういう感じがするのでこれではない。というかそっか、autofocus
指定できるんだ……<input>
にしか使ったことなかったからなんか使えないって思ってしまってた。
HTMLButtonElement
にはなさそうなので、参照しているHTMLFormControlElement
を見る。
"focus"で検索……ShouldShowFocusRingOnMouseFocus()
とかShouldHaveFocusAppearance()
とか、それっぽいのがある!
メソッドを選択して、Call Hierarchyを見てみる。layout_theme.ccにShouldDrawDefaultFocusRing()
というメソッドが見える。これかーーーー!?
フォーカスリングが出る条件
ShouldDrawDefaultFocusRing()
はこうなっていた。
bool LayoutTheme::ShouldDrawDefaultFocusRing(const Node* node, const ComputedStyle& style) const { if (ThemeDrawsFocusRing(style)) return false; if (!node) return true; if (!style.HasAppearance() && !node->IsLink()) return true; // We can't use LayoutTheme::isFocused because outline:auto might be // specified to non-:focus rulesets. if (node->IsFocused() && !node->ShouldHaveFocusAppearance()) return false; return true; }
if文を読み解く。
- テーマがフォーカスリングを出す →
false
- ノードじゃない →
true
- アピアランスががない、かつリンクでない →
true
- フォーカスされている、かつフォーカスを表示すべきじゃないと判断してる →
false
ふんふん(わかってない)
テーマのフォーカスリング
テーマがフォーカスリングを出すのかは、ThemeDrawsFocusRing()
を見ればいいらしい。メソッドをクリックするとlayout_theme.hに飛んだ。
virtual bool ThemeDrawsFocusRing(const ComputedStyle&) const = 0;
さらにクリック。layout_theme_default.ccなるものに飛ぶ。
bool LayoutThemeDefault::ThemeDrawsFocusRing(const ComputedStyle& style) const { if (UseMockTheme()) { // Don't use focus rings for buttons when mocking controls. return style.Appearance() == kButtonPart || style.Appearance() == kPushButtonPart || style.Appearance() == kSquareButtonPart; } // This causes Blink to draw the focus rings for us. return false; }
基本的にはfalse
らしい。true
になりそうなのもテストで使う系なのかな。
プラットフォームによって上書きされてるかもしれないなーと思ったけど、layout_theme.ccの上の方にThe methods in this file are shared by all themes on every platform.
なんてコメントがあった。しかしこのメソッド、テスト以外になにか使い所あるのかなあ。
アピアランスがあるかないか
アピアランスがない、というのは-webkit-appearance
プロパティの話らしい。computed_style.hにHasAppearance()
が定義されていた。
// -webkit-appearance utility functions. bool HasAppearance() const { return Appearance() != kNoControlPart; }
kNoControlPart
とは、theme_types.hのControlPart
というenumに定義されていた。-webkit-appearance: none
に相当する定数っぽい。
CSSProperties.json5(CSSプロパティの定義を生成するもとのファイル)を見ると、-webkit-appearance
の初期値がkNoControlPart
と定義されてたので、認識はあってそう。
フォーカスを見せるのか
フォーカスすべきでないという判断は、ShouldHaveFocusAppearance()
を見ればいい。クリックしたらhtml_form_control_element.ccに定義されていた。
bool HTMLFormControlElement::ShouldHaveFocusAppearance() const { return !was_focused_by_mouse_ || ShouldShowFocusRingOnMouseFocus(); }
ShouldShowFocusRingOnMouseFocus()
はすぐ上にあった。
bool HTMLFormControlElement::ShouldShowFocusRingOnMouseFocus() const { return false; }
マウスでフォーカスされていない場合にのみ、フォーカスすべきという話らしい。
以上をふまえると、基本的にbutton要素は以下の場合にフォーカスリングを出すらしい。
-webkit-appearance
がない場合- マウス以外でフォーカスした場合
なので、通常のボタンをマウスでクリックした場合はフォーカスリング出ない。
appearanceプロパティのcomputed styleが変わるとき
geckotangによるとborder
やらbackground-color
などを指定すると、マウスクリック時にもフォーカスリングが出るとのこと。
先程のShouldDrawDefaultFocusRing()
がtrue
になる条件をふまえると、-webkit-appearance
がない場合にフォーカスリングが出るということなので、-webkit-appearance
が変わる条件を探せばよい。
さてどうしたもんかなーと思っていたのだけど、layout_theme.ccの上の方にこんなコードがあった。
void LayoutTheme::AdjustStyle(ComputedStyle& style, Element* e) { DCHECK(style.HasAppearance()); ControlPart part = style.Appearance(); // ... if (IsControlStyled(style)) { if (part == kMenulistPart) { style.SetAppearance(kMenulistButtonPart); part = kMenulistButtonPart; } else { style.SetAppearance(kNoControlPart); return; } } // ... }
アピアランスを見て、特定のアピアランスでない場合はアピアランスを変更するというコード。
これどこで使われるのかなーと思ったのだけど、layout_theme.hに関数の説明が書いてあった。
// This method is called whenever style has been computed for an element and // the appearance property has been set to a value other than "none". // The theme should map in all of the appropriate metrics and defaults given // the contents of the style. This includes sophisticated operations like // selection of control size based off the font, the disabling of appearance // when certain other properties like "border" are set, or if the appearance // is not supported by the theme. void AdjustStyle(ComputedStyle&, Element*);
スタイルが計算された、もしくはappearance
がnone
以外になったときにコールされると。おっけ。これだ。
どんなときにアピアランスが変わるのか
もっかいさっきのコードを。
if (IsControlStyled(style)) { if (part == kMenulistPart) { style.SetAppearance(kMenulistButtonPart); part = kMenulistButtonPart; } else { style.SetAppearance(kNoControlPart); return; } }
IsControlStyled
という状態の場合でかつ、アピアランスがメニューリストでないものはkNoControlPart
に切り替えるらしい。
IsControlStyled()
の定義を見てみる。
bool LayoutTheme::IsControlStyled(const ComputedStyle& style) const { switch (style.Appearance()) { case kPushButtonPart: case kSquareButtonPart: case kButtonPart: case kProgressBarPart: return style.HasAuthorBackground() || style.HasAuthorBorder(); case kMenulistPart: case kSearchFieldPart: case kTextAreaPart: case kTextFieldPart: return style.HasAuthorBackground() || style.HasAuthorBorder() || style.BoxShadow(); default: return false; } }
<button>
のアピアランスはkButtonPart
なので、style.HasAuthorBackground() || style.HasAuthorBorder()
を満たす場合、そのコントロールにはスタイルがついていると認識される。
HasAuthorBackground()
の定義を見てみる。
bool StyleResolver::HasAuthorBackground(const StyleResolverState& state) { const CachedUAStyle* cached_ua_style = state.GetCachedUAStyle(); if (!cached_ua_style) return false; FillLayer old_fill = cached_ua_style->background_layers; FillLayer new_fill = state.Style()->BackgroundLayers(); // Exclude background-repeat from comparison by resetting it. old_fill.SetRepeatX(EFillRepeat::kNoRepeatFill); old_fill.SetRepeatY(EFillRepeat::kNoRepeatFill); new_fill.SetRepeatX(EFillRepeat::kNoRepeatFill); new_fill.SetRepeatY(EFillRepeat::kNoRepeatFill); return (old_fill != new_fill || cached_ua_style->background_color != state.Style()->BackgroundColor()); }
だるくなってきたので深く追ってないけど、background-repeat
以外のbackground関連プロパティが変わってたら指定されてるという判断なのかな。
bool StyleResolver::HasAuthorBorder(const StyleResolverState& state) { const CachedUAStyle* cached_ua_style = state.GetCachedUAStyle(); return cached_ua_style && (cached_ua_style->border_image != state.Style()->BorderImage() || !cached_ua_style->BorderColorEquals(*state.Style()) || !cached_ua_style->BorderWidthEquals(*state.Style()) || !cached_ua_style->BorderRadiiEquals(*state.Style()) || !cached_ua_style->BorderStyleEquals(*state.Style())); }
border関連が変わってたらだめと。
なので、背景とborder関連プロパティ(border-image
, border-radius
含む)が変更されたら、そのコントロールにスタイルがあたってると認識され、マウスクリック時でもフォーカスリングが当たるようになる。
フォーカスリングを出したくない?
出したくない場合、borderとbackground関連のプロパティは使えない……ちょっと現実的でない。とはいえ:focus { outline: 0 }
はダメゼッタイ。ジレンマ。
というわけで:focus-visible
なんて擬似クラスができた。Chromeで実装中なのでもうちょいでよさげな感じになる。
そういえば、Chrome以外でマウスクリック時にフォーカスリングを出すブラウザってあったっけ…?macOSだとFirefoxとSafariは出さないみたいだけど、WindowsやLinuxとかはどうだっただろうか。