Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom state pseudo class #8467

Merged
merged 13 commits into from
Dec 24, 2023
180 changes: 180 additions & 0 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -2835,6 +2835,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 @@ -70723,6 +70724,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
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
element author can use the <code data-x="dom-ElementInternals-states">states</code> property of
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<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 @@ -72055,6 +72148,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 @@ -72383,6 +72479,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
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -73350,6 +73522,14 @@ 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()</code> pseudo-class takes an argument and must
match all <span>custom element</span>s whose <span>states set</span>'s <span>set entries</span>
contains a string matching the argument passed to <code
data-x="selector-custom">:state()</code>.</p>
</dd>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need to say anything more here as :state() is probably not passed an actual string but rather a CSS identifier. cc @tabatkins

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So instead of saying "takes an argument" i should say "takes a css identifier" and link to the css spec for what a css identifier is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mainly wondering about the comparison operation, but it's probably fine as-is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the drive-by review, but is this the right formatting for a :state(arg) pseudo-class? I see why it happened while the pseudo-class was spelled :--arg, but once it's formatted as a function call like :dir(ltr), I'd expect something like <dfn selector noexport><code data-x="selector-state">:state(<var>identifier</var>)</code></dfn></dt>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much easier to read, so I did it. Thanks!

</dl>

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