Bug 1617600 - Prototype :focus-visible behind a flag. r=smaug

The heuristic is that we show focus outlines for unknown or key focus, and not
for mouse / touch.

This is probably not the final heuristic we take, but this allows people to play
with it and file bugs.

Once this is mature enough we should remove :-moz-focusring in favor of
:focus-visible.

Differential Revision: https://phabricator.services.mozilla.com/D63861

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Emilio Cobos Álvarez 2020-02-26 14:16:20 +00:00
parent cec1932e5b
commit de54b68ce7
17 changed files with 90 additions and 43 deletions

View File

@ -872,7 +872,8 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument,
}
}
NotifyFocusStateChange(content, nullptr, shouldShowFocusRing, false);
NotifyFocusStateChange(content, nullptr, shouldShowFocusRing, 0,
/* aGettingFocus = */ false);
}
return NS_OK;
@ -975,7 +976,7 @@ nsFocusManager::WindowHidden(mozIDOMWindowProxy* aWindow) {
if (oldFocusedElement && oldFocusedElement->IsInComposedDoc()) {
NotifyFocusStateChange(oldFocusedElement, nullptr,
mFocusedWindow->ShouldShowFocusRing(), false);
mFocusedWindow->ShouldShowFocusRing(), 0, false);
window->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
if (presShell) {
@ -1088,10 +1089,41 @@ nsFocusManager::ParentActivated(mozIDOMWindowProxy* aWindow, bool aActive) {
return NS_OK;
}
static bool ShouldMatchFocusVisible(const Element& aElement,
int32_t aFocusFlags) {
if (StaticPrefs::browser_display_show_focus_rings()) {
// FIXME: Spec is ambiguous about whether we should take platform
// conventions into account. This branch does make us account for them.
return true;
}
switch (nsFocusManager::GetFocusMoveActionCause(aFocusFlags)) {
case InputContextAction::CAUSE_UNKNOWN:
case InputContextAction::CAUSE_KEY:
return true;
case InputContextAction::CAUSE_MOUSE:
case InputContextAction::CAUSE_TOUCH:
case InputContextAction::CAUSE_LONGPRESS:
break;
case InputContextAction::CAUSE_UNKNOWN_CHROME:
case InputContextAction::CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT:
case InputContextAction::CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT:
// TODO(emilio): We could return some of these though, looking at
// UserActivation. We may want to suppress focus rings for unknown /
// programatic focus if the user is interacting with the page but not
// during keyboard input, or such.
MOZ_ASSERT_UNREACHABLE(
"These don't get returned by GetFocusMoveActionCause");
break;
}
return false;
}
/* static */
void nsFocusManager::NotifyFocusStateChange(nsIContent* aContent,
nsIContent* aContentToFocus,
bool aWindowShouldShowFocusRing,
int32_t aFlags,
bool aGettingFocus) {
MOZ_ASSERT_IF(aContentToFocus, !aGettingFocus);
if (!aContent->IsElement()) {
@ -1104,24 +1136,29 @@ void nsFocusManager::NotifyFocusStateChange(nsIContent* aContent,
aContent, aContentToFocus);
}
EventStates eventState = NS_EVENT_STATE_FOCUS;
if (aWindowShouldShowFocusRing) {
eventState |= NS_EVENT_STATE_FOCUSRING;
}
if (aGettingFocus) {
aContent->AsElement()->AddStates(eventState);
EventStates eventStateToAdd = NS_EVENT_STATE_FOCUS;
if (aWindowShouldShowFocusRing) {
eventStateToAdd |= NS_EVENT_STATE_FOCUSRING;
}
if (ShouldMatchFocusVisible(*aContent->AsElement(), aFlags)) {
eventStateToAdd |= NS_EVENT_STATE_FOCUS_VISIBLE;
}
aContent->AsElement()->AddStates(eventStateToAdd);
} else {
aContent->AsElement()->RemoveStates(eventState);
EventStates eventStateToRemove = NS_EVENT_STATE_FOCUS |
NS_EVENT_STATE_FOCUSRING |
NS_EVENT_STATE_FOCUS_VISIBLE;
aContent->AsElement()->RemoveStates(eventStateToRemove);
}
for (nsIContent* content = aContent; content && content != commonAncestor;
content = content->GetFlattenedTreeParent()) {
if (!content->IsElement()) {
Element* element = Element::FromNode(content);
if (!element) {
continue;
}
Element* element = content->AsElement();
if (aGettingFocus) {
if (element->State().HasState(NS_EVENT_STATE_FOCUS_WITHIN)) {
break;
@ -1950,7 +1987,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear,
element && element->IsInComposedDoc() && !IsNonFocusableRoot(element);
if (element) {
if (sendBlurEvent) {
NotifyFocusStateChange(element, aContentToFocus, shouldShowFocusRing,
NotifyFocusStateChange(element, aContentToFocus, shouldShowFocusRing, 0,
false);
}
@ -2231,7 +2268,7 @@ void nsFocusManager::Focus(nsPIDOMWindowOuter* aWindow, Element* aElement,
nsPresContext* presContext = presShell->GetPresContext();
if (sendFocusEvent) {
NotifyFocusStateChange(aElement, nullptr, aWindow->ShouldShowFocusRing(),
true);
aFlags, /* aGettingFocus = */ true);
// if this is an object/plug-in/remote browser, focus its widget. Note
// that we might no longer be in the same document, due to the events we

View File

@ -671,7 +671,7 @@ class nsFocusManager final : public nsIFocusManager,
static void NotifyFocusStateChange(nsIContent* aContent,
nsIContent* aContentToFocus,
bool aWindowShouldShowFocusRing,
bool aGettingFocus);
int32_t aFlags, bool aGettingFocus);
void SetFocusedWindowInternal(nsPIDOMWindowOuter* aWindow);

View File

@ -2914,8 +2914,7 @@ bool CanvasRenderingContext2D::DrawCustomFocusRing(
return false;
}
nsFocusManager* fm = nsFocusManager::GetFocusManager();
if (fm) {
if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) {
// check that the element is focused
if (&aElement == fm->GetFocusedElement()) {
if (nsPIDOMWindowOuter* window = aElement.OwnerDoc()->GetWindow()) {

View File

@ -287,9 +287,10 @@ class EventStates {
#define NS_EVENT_STATE_AUTOFILL NS_DEFINE_EVENT_STATE_MACRO(50)
// Element is filled with preview data by Autofill feature.
#define NS_EVENT_STATE_AUTOFILL_PREVIEW NS_DEFINE_EVENT_STATE_MACRO(51)
// Event state that is used for values that need to be parsed but do nothing.
#define NS_EVENT_STATE_IGNORE NS_DEFINE_EVENT_STATE_MACRO(63)
// Element matches the :focus-visible pseudo-class.
//
// TODO(emilio): We should eventually unify this and FOCUSRING.
#define NS_EVENT_STATE_FOCUS_VISIBLE NS_DEFINE_EVENT_STATE_MACRO(52)
/**
* NOTE: do not go over 63 without updating EventStates::InternalType!
@ -324,7 +325,8 @@ class EventStates {
REQUIRED_STATES | NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_DEFINED | \
NS_EVENT_STATE_DRAGOVER | NS_EVENT_STATE_FOCUS | NS_EVENT_STATE_FOCUSRING | \
NS_EVENT_STATE_FOCUS_WITHIN | NS_EVENT_STATE_FULLSCREEN | \
NS_EVENT_STATE_HOVER | NS_EVENT_STATE_URLTARGET)
NS_EVENT_STATE_HOVER | NS_EVENT_STATE_URLTARGET | \
NS_EVENT_STATE_FOCUS_VISIBLE)
#define INTRINSIC_STATES (~EXTERNALLY_MANAGED_STATES)

View File

@ -2172,7 +2172,8 @@ void HTMLInputElement::SetFocusState(bool aIsFocused) {
return;
}
EventStates focusStates = NS_EVENT_STATE_FOCUS | NS_EVENT_STATE_FOCUSRING;
EventStates focusStates = NS_EVENT_STATE_FOCUS | NS_EVENT_STATE_FOCUSRING |
NS_EVENT_STATE_FOCUS_VISIBLE;
if (aIsFocused) {
AddStates(focusStates);
} else {

View File

@ -170,14 +170,8 @@ bool nsIConstraintValidation::ReportValidity() {
}
}
if (element->IsHTMLElement(nsGkAtoms::input) &&
// We don't use nsContentUtils::IsFocusedContent here, because it doesn't
// really do what we want for number controls: it's true for the
// anonymous textnode inside, but not the number control itself. We can
// use the focus state, though, because that gets synced to the number
// control by the anonymous text control.
element->State().HasState(NS_EVENT_STATE_FOCUS)) {
HTMLInputElement* inputElement = HTMLInputElement::FromNode(element);
auto* inputElement = HTMLInputElement::FromNode(element);
if (inputElement && inputElement->State().HasState(NS_EVENT_STATE_FOCUS)) {
inputElement->UpdateValidityUIBits(true);
}

View File

@ -5155,6 +5155,16 @@
mirror: always
rust: true
# Whether the `:focus-visible` pseudo-class is enabled.
#
# TODO: There are various ambiguities in the spec about this, so we shouldn't
# ship this until those are sorted out.
- name: layout.css.focus-visible.enabled
type: RelaxedAtomicBool
value: @IS_NIGHTLY_BUILD@
mirror: always
rust: true
# Allow <number> and <number>/<number> both for <aspect-ratio>
# https://github.com/w3c/csswg-drafts/issues/3757
- name: layout.css.aspect-ratio-number.enabled

View File

@ -137,6 +137,10 @@ bitflags! {
const IN_AUTOFILL_STATE = 1 << 50;
/// Non-standard & undocumented.
const IN_AUTOFILL_PREVIEW_STATE = 1 << 51;
/// :focus-visible
///
/// https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo
const IN_FOCUS_VISIBLE_STATE = 1 << 52;
}
}

View File

@ -42,6 +42,7 @@ macro_rules! apply_non_ts_list {
("enabled", Enabled, enabled, IN_ENABLED_STATE, _),
("focus", Focus, focus, IN_FOCUS_STATE, _),
("focus-within", FocusWithin, focusWithin, IN_FOCUS_WITHIN_STATE, _),
("focus-visible", FocusVisible, focusVisible, IN_FOCUS_VISIBLE_STATE, _),
("hover", Hover, hover, IN_HOVER_STATE, _),
("-moz-drag-over", MozDragOver, mozDragOver, IN_DRAGOVER_STATE, _),
("target", Target, target, IN_TARGET_STATE, _),

View File

@ -172,6 +172,9 @@ impl NonTSPseudoClass {
/// Returns whether the pseudo-class is enabled in content sheets.
#[inline]
fn is_enabled_in_content(&self) -> bool {
if matches!(*self, NonTSPseudoClass::FocusVisible) {
return static_prefs::pref!("layout.css.focus-visible.enabled")
}
!self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME)
}

View File

@ -2049,6 +2049,7 @@ impl<'le> ::selectors::Element for GeckoElement<'le> {
NonTSPseudoClass::MozReadOnly |
NonTSPseudoClass::MozReadWrite |
NonTSPseudoClass::FocusWithin |
NonTSPseudoClass::FocusVisible |
NonTSPseudoClass::MozDragOver |
NonTSPseudoClass::MozDevtoolsHighlighted |
NonTSPseudoClass::MozStyleeditorTransitioning |

View File

@ -0,0 +1 @@
prefs: [layout.css.focus-visible.enabled:true]

View File

@ -1,7 +1,3 @@
[focus-visible-005.html]
[Programmatic focus after click should not match :focus-visible]
expected:
if os == "android": FAIL
if os == "mac": FAIL
if os == "linux": FAIL
expected: FAIL

View File

@ -1,4 +1,5 @@
[focus-visible-009.html]
[Autofocus should match :focus-visible]
expected: FAIL
expected:
if os == "android": FAIL
bug: 1618195

View File

@ -1,4 +0,0 @@
[focus-visible-010.html]
[Programmatic focus on page load bshould match :focus-visible]
expected: FAIL

View File

@ -1,4 +1,5 @@
[focus-visible-011.html]
[:focus-visible matches even if preventDefault() is called]
expected: FAIL
expected:
if os == "android": FAIL
bug: 1618195

View File

@ -35,7 +35,7 @@
assert_equals(getComputedStyle(el).outlineColor, "rgb(0, 100, 0)");
t.done();
}));
}, "Programmatic focus on page load bshould match :focus-visible");
}, "Programmatic focus on page load should match :focus-visible");
</script>
</body>
</html>