Skip to content

Commit

Permalink
Add custom state pseudo class: :state(identifier)
Browse files Browse the repository at this point in the history
This is based on this WICG draft: https://wicg.github.io/custom-state-pseudo-class.

This has been discussed in these issues (among others): w3ctag/design-reviews#428 & w3c/csswg-drafts#4805.

This will be added to Selectors level 5: w3c/csswg-drafts#4805.

Tests: https://wpt.fyi/results/custom-elements/state.

Co-authored-by: Domenic Denicola <[email protected]>
  • Loading branch information
josepharhar and domenic authored Dec 24, 2023
1 parent 8027fc5 commit 9e72bf9
Showing 1 changed file with 179 additions and 0 deletions.
179 changes: 179 additions & 0 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -2834,6 +2834,7 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
<li><dfn data-x="LegacyTreatNonObjectAsNull" data-x-href="https://webidl.spec.whatwg.org/#LegacyTreatNonObjectAsNull"><code>[LegacyTreatNonObjectAsNull]</code></dfn></li>
<li><dfn data-x="LegacyUnenumerableNamedProperties" data-x-href="https://webidl.spec.whatwg.org/#LegacyUnenumerableNamedProperties"><code>[LegacyUnenumerableNamedProperties]</code></dfn></li>
<li><dfn data-x="LegacyUnforgeable" data-x-href="https://webidl.spec.whatwg.org/#LegacyUnforgeable"><code>[LegacyUnforgeable]</code></dfn></li>
<li><dfn data-x-href="https://webidl.spec.whatwg.org/#dfn-set-entries">set entries</dfn></li>
</ul>

<p><cite>Web IDL</cite> also defines the following types that are used in Web IDL fragments in
Expand Down Expand Up @@ -71347,6 +71348,98 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
console.assert(outOfDocument instanceof ExampleElement);
&lt;/script></code></pre>

<h5>Exposing custom element states</h5>

<p>Built-in elements provided by user agents have certain states that can change over time
depending on user interaction and other factors, and are exposed to web authors through <span
data-x="pseudo-class">pseudo-classes</span>. For example, some form controls have the "invalid"
state, which is exposed through the <code data-x="selector-invalid">:invalid</code>
<span>pseudo-class</span>.</p>

<p>Like built-in elements, <span data-x="custom element">custom elements</span> can have various
states to be in too, and <span>custom element</span> authors want to expose these states in a
similar fashion as the built-in elements.</p>

<p>This is done via the <code data-x="selector-custom">:state()</code> pseudo-class. A custom
element author can use the <code data-x="dom-ElementInternals-states">states</code> property of
<code>ElementInternals</code> to add and remove such custom states, which are then exposed as
arguments to the <code data-x="selector-custom">:state()</code> pseudo-class.

<div class="example">
<p>The following shows how <code data-x="selector-custom">:state()</code> can be used to style a
custom checkbox element. Assume that <code data-x="">LabeledCheckbox</code> doesn't expose its
"checked" state via a content attribute.</p>

<pre><code class="html">&lt;script>
class LabeledCheckbox extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
this.addEventListener('click', this._onClick.bind(this));

const shadowRoot = this.attachShadow({mode: 'closed'});
shadowRoot.innerHTML =
&#96;&lt;style>
:host::before {
content: '[ ]';
white-space: pre;
font-family: monospace;
}
:host(:state(checked))::before { content: '[x]' }
&lt;/style>
&lt;slot>Label&lt;/slot>&#96;;
}

get checked() { return this._internals.states.has('checked'); }

set checked(flag) {
if (flag)
this._internals.states.add('checked');
else
this._internals.states.delete('checked');
}

_onClick(event) {
this.checked = !this.checked;
}
}

customElements.define('labeled-checkbox', LabeledCheckbox);
&lt;/script>

&lt;style>
labeled-checkbox { border: dashed red; }
labeled-checkbox:state(checked) { border: solid; }
&lt;/style>

&lt;labeled-checkbox>You need to check this&lt;/labeled-checkbox></code></pre>
</div>

<div class="example">
<p>Custom pseudo-classes can even target shadow parts. An extension of the above example shows
this:</p>

<pre><code class="html">&lt;script>
class QuestionBox extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'closed'});
shadowRoot.innerHTML =
&#96;&lt;div>&lt;slot>Question&lt;/slot>&lt;/div>
&lt;labeled-checkbox part='checkbox'>Yes&lt;/labeled-checkbox>&#96;;
}
}
customElements.define('question-box', QuestionBox);
&lt;/script>

&lt;style>
question-box::part(checkbox) { color: red; }
question-box::part(checkbox):state(checked) { color: green; }
&lt;/style>

&lt;question-box>Continue?&lt;/question-box></code></pre>
</div>

<h4 id="custom-element-conformance">Requirements for custom element constructors and
reactions</h4>

Expand Down Expand Up @@ -72679,6 +72772,9 @@ interface <dfn interface>ElementInternals</dfn> {
boolean <span data-x="dom-ElementInternals-reportValidity">reportValidity</span>();

readonly attribute <span>NodeList</span> <span data-x="dom-ElementInternals-labels">labels</span>;

// <a href="#custom-state-pseudo-class">Custom state pseudo-class</a>
[SameObject] readonly attribute <span>CustomStateSet</span> <span data-x="dom-ElementInternals-states">states</span>;
};

// <a href="#accessibility-semantics">Accessibility semantics</a>
Expand Down Expand Up @@ -73007,6 +73103,82 @@ dictionary <dfn dictionary>ValidityStateFlags</dfn> {

</div>

<h5>Custom state pseudo-class</h5>

<dl class="domintro">
<dt><code data-x=""><var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.add(<var>value</var>)</code></dt>
<dd>
<p>Adds the string <var>value</var> to the element's <span>states set</span> to be exposed as a
pseudo-class.</p>
</dd>

<dt><code data-x=""><var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.has(<var>value</var>)</code></dt>
<dd>
<p>Returns true if <var>value</var> is in the element's <span>states set</span>, otherwise
false.</p>
</dd>

<dt><code data-x=""><var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.delete(<var>value</var>)</code></dt>
<dd>
<p>If the element's <span>states set</span> has <var>value</var>, then it will be removed and
true will be returned. Otherwise, false will be returned.</p>
</dd>

<dt><code data-x=""><var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.clear()</code></dt>
<dd>
<p>Removes all values from the element's <span>states set</span>.</p>
</dd>

<dt><code data-x="">for (const <var>stateName</var> of <var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>)</code></dt>
<dt><code data-x="">for (const <var>stateName</var> of <var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.entries())</code></dt>
<dt><code data-x="">for (const <var>stateName</var> of <var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.keys())</code></dt>
<dt><code data-x="">for (const <var>stateName</var> of <var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.values())</code></dt>
<dd>
<p>Iterates over all values in the element's <span>states set</span>.</p>
</dd>

<dt><code data-x=""><var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.forEach(<var>callback</var>)</code></dt>
<dd>
<p>Iterates over all values in the element's <span>states set</span> by calling
<var>callback</var> once for each value.</p>
</dd>

<dt><code data-x=""><var>internals</var>.<span data-x="dom-ElementInternals-states">states</span>.size</code></dt>
<dd>
<p>Returns the number of values in the element's <span>states set</span>.</p>
</dd>
</dl>

<div w-nodev>
<p>Each <span>custom element</span> has a <dfn>states set</dfn>, which is a
<code>CustomStateSet</code>, initially empty.</p>

<pre><code class="idl">[Exposed=Window]
interface <dfn>CustomStateSet</dfn> {
setlike&lt;DOMString>;
};</code></pre>

<p>The <dfn for="HTMLElement"><code data-x="dom-ElementInternals-states">states</code></dfn>
getter steps are to return <span>this</span>'s <span data-x="internals-target">target
element</span>'s <span>states set</span>.</p>
</div>

<div class="example">
<p>The <span>states set</span> can expose boolean states represented by existence/non-existence
of string values. If an author wants to expose a state which can have three values, it can be
converted to three exclusive boolean states. For example, a state called <code
data-x="">readyState</code> with <code data-x="">"loading"</code>, <code
data-x="">"interactive"</code>, and <code data-x="">"complete"</code> values can be mapped to
three exclusive boolean states, <code data-x="">"loading"</code>, <code
data-x="">"interactive"</code>, and <code data-x="">"complete"</code>:</p>

<pre><code class="js">// Change the readyState from anything to "complete".
this._readyState = "complete";
this._internals.states.delete("loading");
this._internals.states.delete("interactive");
this._internals.states.add("complete");</code></pre>
</div>

<h3 split-filename="semantics-other" id="common-idioms">Common idioms without dedicated elements</h3>

<h4 id="rel-up">Breadcrumb navigation</h4>
Expand Down Expand Up @@ -73974,6 +74146,13 @@ Demos:
elements whose <span data-x="the directionality">directionality</span> is '<span
data-x="concept-rtl">rtl</span>'.</p>
</dd>

<dt><dfn selector noexport data-x="selector-custom">Custom state pseudo-class</dfn></dt>
<dd>
<p>The <code data-x="selector-custom">:state(<var>identifier</var>)</code> pseudo-class must
match all <span>custom element</span>s whose <span>states set</span>'s <span>set entries</span>
contains <var>identifier</var>.</p>
</dd>
</dl>

<p class="note">This specification does not define when an element matches the <code undefined
Expand Down

0 comments on commit 9e72bf9

Please sign in to comment.