Commit 3ced879c authored by Kent Tamura's avatar Kent Tamura Committed by Commit Bot

Custom state: Add ':state()' CSS pseudo class

The new behavior is behind the 'CustomStatePseudoClass' runtime flag.

Explainer: https://github.com/w3c/webcomponents/blob/gh-pages/proposals/custom-states-and-state-pseudo-class.md

Bug: 1012098
Change-Id: Ia0d1e145cf6589868c7c39d3b1c51fdf2b41bc68
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1800138
Commit-Queue: Kent Tamura <tkent@chromium.org>
Reviewed-by: default avatarRune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/master@{#704556}
parent 9fd91410
......@@ -278,6 +278,7 @@ PseudoId CSSSelector::GetPseudoId(PseudoType type) {
case kPseudoHost:
case kPseudoHostContext:
case kPseudoPart:
case kPseudoState:
case kPseudoShadow:
case kPseudoFullScreen:
case kPseudoFullScreenAncestor:
......@@ -418,6 +419,7 @@ const static NameToPseudoStruct kPseudoTypeWithArgumentsMap[] = {
{"nth-of-type", CSSSelector::kPseudoNthOfType},
{"part", CSSSelector::kPseudoPart},
{"slotted", CSSSelector::kPseudoSlotted},
{"state", CSSSelector::kPseudoState},
{"where", CSSSelector::kPseudoWhere},
};
......@@ -459,6 +461,11 @@ static CSSSelector::PseudoType NameToPseudoType(const AtomicString& name,
!RuntimeEnabledFeatures::CSSPictureInPictureEnabled())
return CSSSelector::kPseudoUnknown;
if (match->type == CSSSelector::kPseudoState &&
!RuntimeEnabledFeatures::CustomStatePseudoClassEnabled()) {
return CSSSelector::kPseudoUnknown;
}
return static_cast<CSSSelector::PseudoType>(match->type);
}
......@@ -649,6 +656,7 @@ void CSSSelector::UpdatePseudoType(const AtomicString& value,
case kPseudoScope:
case kPseudoSingleButton:
case kPseudoStart:
case kPseudoState:
case kPseudoTarget:
case kPseudoUnknown:
case kPseudoValid:
......@@ -768,6 +776,7 @@ const CSSSelector* CSSSelector::SerializeCompound(
break;
}
case kPseudoLang:
case kPseudoState:
builder.Append('(');
builder.Append(simple_selector->Argument());
builder.Append(')');
......
......@@ -160,6 +160,7 @@ class CORE_EXPORT CSSSelector {
kPseudoNthLastChild,
kPseudoNthLastOfType,
kPseudoPart,
kPseudoState,
kPseudoLink,
kPseudoVisited,
kPseudoAny,
......
......@@ -252,6 +252,8 @@ bool IsPseudoClassValidAfterPseudoElement(
case CSSSelector::kPseudoSelection:
return pseudo_class == CSSSelector::kPseudoWindowInactive;
case CSSSelector::kPseudoPart:
return IsUserActionPseudoClass(pseudo_class) ||
pseudo_class == CSSSelector::kPseudoState;
case CSSSelector::kPseudoWebKitCustomElement:
case CSSSelector::kPseudoBlinkInternalElement:
return IsUserActionPseudoClass(pseudo_class);
......@@ -599,6 +601,7 @@ std::unique_ptr<CSSParserSelector> CSSSelectorParser::ConsumePseudo(
selector->AdoptSelectorVector(selector_vector);
return selector;
}
case CSSSelector::kPseudoState:
case CSSSelector::kPseudoPart: {
const CSSParserToken& ident = block.ConsumeIncludingWhitespace();
if (ident.GetType() != kIdentToken || !block.AtEnd())
......
......@@ -89,6 +89,7 @@ bool SupportsInvalidation(CSSSelector::PseudoType type) {
case CSSSelector::kPseudoNthLastChild:
case CSSSelector::kPseudoNthLastOfType:
case CSSSelector::kPseudoPart:
case CSSSelector::kPseudoState:
case CSSSelector::kPseudoLink:
case CSSSelector::kPseudoVisited:
case CSSSelector::kPseudoAny:
......@@ -534,6 +535,7 @@ InvalidationSet* RuleFeatureSet::InvalidationSetForSimpleSelector(
case CSSSelector::kPseudoRequired:
case CSSSelector::kPseudoReadOnly:
case CSSSelector::kPseudoReadWrite:
case CSSSelector::kPseudoState:
case CSSSelector::kPseudoValid:
case CSSSelector::kPseudoInvalid:
case CSSSelector::kPseudoIndeterminate:
......
......@@ -46,6 +46,7 @@
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/picture_in_picture_controller.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_option_element.h"
......@@ -1109,6 +1110,10 @@ bool SelectorChecker::CheckPseudoClass(const SelectorCheckingContext& context,
if (!context.has_selection_pseudo)
return false;
return !element.GetDocument().GetPage()->GetFocusController().IsActive();
case CSSSelector::kPseudoState: {
return element.DidAttachInternals() &&
element.EnsureElementInternals().HasState(selector.Argument());
}
case CSSSelector::kPseudoHorizontal:
case CSSSelector::kPseudoVertical:
case CSSSelector::kPseudoDecrement:
......
......@@ -41,8 +41,8 @@ class CustomStatesTokenList : public DOMTokenList {
void setValue(const AtomicString& new_value) override {
DidUpdateAttributeValue(value(), new_value);
// TODO(crbug.com/1012098): Calls GetElement().PseudoStateChanged() for
// ':state()'
// Should we have invalidation set for each of state tokens?
GetElement().PseudoStateChanged(CSSSelector::kPseudoState);
}
};
......@@ -235,6 +235,10 @@ DOMTokenList* ElementInternals::states() {
return custom_states_;
}
bool ElementInternals::HasState(const AtomicString& state) const {
return custom_states_ && custom_states_->contains(state);
}
const AtomicString& ElementInternals::FastGetAttribute(
const QualifiedName& attribute) const {
return accessibility_semantics_map_.at(attribute);
......
......@@ -53,6 +53,8 @@ class CORE_EXPORT ElementInternals : public ScriptWrappable,
LabelsNodeList* labels(ExceptionState& exception_state);
DOMTokenList* states();
bool HasState(const AtomicString& state) const;
// We need these functions because we are reflecting ARIA attributes.
// See dom/aria_attributes.idl.
const AtomicString& FastGetAttribute(const QualifiedName&) const;
......
......@@ -258,6 +258,7 @@ const char* PseudoTypeToString(CSSSelector::PseudoType pseudo_type) {
DEFINE_STRING_MAPPING(PseudoNthLastChild)
DEFINE_STRING_MAPPING(PseudoNthLastOfType)
DEFINE_STRING_MAPPING(PseudoPart)
DEFINE_STRING_MAPPING(PseudoState)
DEFINE_STRING_MAPPING(PseudoLink)
DEFINE_STRING_MAPPING(PseudoVisited)
DEFINE_STRING_MAPPING(PseudoAny)
......
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<style>
#state-and-part::part(inner) {
opacity: 0;
}
#state-and-part::part(inner):state(innerFoo) {
opacity: 0.5;
}
#state-and-part:state(outerFoo)::part(inner) {
opacity: 0.25;
}
</style>
<body>
<script>
class TestElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
get internals() {
return this._internals;
}
}
customElements.define('test-element', TestElement);
class ContainerElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
this._shadow = this.attachShadow({mode:'open'});
this._shadow.innerHTML = `
<style>
:host {
border-style: solid;
}
:host(:state(dotted)) {
border-style: dotted;
}
</style>
<test-element part="inner"></test-element>`;
}
get internals() {
return this._internals;
}
get innerElement() {
return this._shadow.querySelector('test-element');
}
}
customElements.define('container-element', ContainerElement);
test(() => {
assert_throws(new SyntaxError(), () => { document.querySelector(':state'); });
assert_throws(new SyntaxError(), () => { document.querySelector(':state('); });
assert_throws(new SyntaxError(), () => { document.querySelector(':state()'); });
assert_throws(new SyntaxError(), () => { document.querySelector(':state(=)'); });
assert_throws(new SyntaxError(), () => { document.querySelector(':state(name=value)'); });
assert_throws(new SyntaxError(), () => { document.querySelector(':state( foo bar)'); });
assert_throws(new SyntaxError(), () => { document.querySelector(':state(16px)'); });
}, ':state() parsing failures');
test(() => {
assert_equals(document.styleSheets[0].cssRules[1].cssText,
'#state-and-part::part(inner):state(innerFoo) { opacity: 0.5; }');
}, ':state() serialization');
test(() => {
let element = new TestElement();
let states = element.internals.states;
assert_false(element.matches(':state(foo)'));
assert_true(element.matches(':not(:state(foo))'));
states.add('foo');
assert_true(element.matches(':state(foo)'));
assert_true(element.matches(':is(:state(foo))'));
element.classList.add('c1', 'c2');
assert_true(element.matches('.c1:state(foo)'));
assert_true(element.matches(':state(foo).c1'));
assert_true(element.matches('.c2:state(foo).c1'));
}, ':state() in simple cases');
test(() => {
let element = new TestElement();
element.tabIndex = 0;
document.body.appendChild(element);
element.focus();
let states = element.internals.states;
states.value = 'foo';
assert_true(element.matches(':focus:state(foo)'));
assert_true(element.matches(':state(foo):focus'));
}, ':state() and other pseudo classes');
test(() => {
let outer = new ContainerElement();
outer.id = 'state-and-part';
document.body.appendChild(outer);
let inner = outer.innerElement;
let innerStates = inner.internals.states;
innerStates.add('innerFoo');
assert_equals(getComputedStyle(inner).opacity, '0.5',
'::part() followed by :state()');
innerStates.replace('innerFoo', 'innerfoo');
assert_equals(getComputedStyle(inner).opacity, '0',
':state() matching should be case-sensitive');
innerStates.remove('innerfoo');
outer.internals.states.add('outerFoo');
assert_equals(getComputedStyle(inner).opacity, '0.25',
':state() followed by ::part()');
}, ':state() and ::part()');
test(() => {
let outer = new ContainerElement();
document.body.appendChild(outer);
assert_equals(getComputedStyle(outer).borderStyle, 'solid');
outer.internals.states.toggle('dotted');
assert_equals(getComputedStyle(outer).borderStyle, 'dotted');
}, ':state() and :host()');
</script>
</body>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment