Chromeでボタンをクリックしたときのフォーカスリング

geckotangが「Chromeでボタンをマウスでクリックしたときにフォーカスリングが出たり出なかったりするのなんで」と言っていて、なんかお前調べろよ的な感じだったので調べることにした。

使うのはcs.chromium.org<button>なので"HTMLButtonElement"で検索して、それっぽいファイルを探す。あった。

"focus"で検索するとSupportsAutoFocus()というメソッドが見つかる……がこれはautofocus属性の対象になるかとかそういう感じがするのでこれではない。というかそっか、autofocus指定できるんだ……<input>にしか使ったことなかったからなんか使えないって思ってしまってた。

HTMLButtonElementにはなさそうなので、参照しているHTMLFormControlElementを見る。

"focus"で検索……ShouldShowFocusRingOnMouseFocus()とかShouldHaveFocusAppearance()とか、それっぽいのがある!

メソッドを選択して、Call Hierarchyを見てみる。layout_theme.ccShouldDrawDefaultFocusRing()というメソッドが見える。これかーーーー!?

フォーカスリングが出る条件

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文を読み解く。

  1. テーマがフォーカスリングを出す → false
  2. ノードじゃない → true
  3. アピアランスががない、かつリンクでない → true
  4. フォーカスされている、かつフォーカスを表示すべきじゃないと判断してる → 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.hHasAppearance()が定義されていた。

  // -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*);

スタイルが計算された、もしくはappearancenone以外になったときにコールされると。おっけ。これだ。

どんなときにアピアランスが変わるのか

もっかいさっきのコードを。

  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関連プロパティが変わってたら指定されてるという判断なのかな。

続いてHasAuthorBorder()

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だとFirefoxSafariは出さないみたいだけど、WindowsLinuxとかはどうだっただろうか。