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
228 changes: 226 additions & 2 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -2277,8 +2277,9 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
<dfn data-x-href="https://infra.spec.whatwg.org/#queue-enqueue">enqueue</dfn> and
<dfn data-x-href="https://infra.spec.whatwg.org/#queue-dequeue">dequeue</dfn></li>
<li>The <dfn data-x="set" data-x-href="https://infra.spec.whatwg.org/#ordered-set">ordered set</dfn> data structure and the associated definition for
<dfn data-x="set append" data-x-href="https://infra.spec.whatwg.org/#set-append">append</dfn> and
<dfn data-x="set union" data-x-href="https://infra.spec.whatwg.org/#set-union">union</dfn></li>
<dfn data-x="set append" data-x-href="https://infra.spec.whatwg.org/#set-append">append</dfn>,
<dfn data-x="set union" data-x-href="https://infra.spec.whatwg.org/#set-union">union</dfn>, and
<dfn data-x="set entries" data-x-href="https://infra.spec.whatwg.org/#dfn-set-entries">entries</dfn></li>
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<li>The <dfn data-x-href="https://infra.spec.whatwg.org/#struct">struct</dfn> specification type and the associated definition for
<dfn data-x="struct item" data-x-href="https://infra.spec.whatwg.org/#struct-item">item</dfn></li>
<li>The <dfn data-x-href="https://infra.spec.whatwg.org/#forgiving-base64-encode">forgiving-base64 encode</dfn> and
Expand Down Expand Up @@ -2761,6 +2762,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/#es-add-delete">Default add operation</dfn></li>
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
</ul>

<p><cite>Web IDL</cite> also defines the following types that are used in Web IDL fragments in
Expand Down Expand Up @@ -3902,6 +3904,7 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
<li>The <dfn data-x-href="https://drafts.csswg.org/css-values/#pt">'pt'</dfn> unit</li>
<li>The <dfn data-x-href="https://drafts.csswg.org/css-values/#funcdef-attr">'attr()'</dfn> function</li>
<li>The <dfn data-x-href="https://drafts.csswg.org/css-values/#math-function">math functions</dfn></li>
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<li>The <dfn data-x-href="https://drafts.csswg.org/css-values-4/#typedef-dashed-ident">dashed ident</dfn> production</li>
</ul>

<p>The term <dfn data-x="css-styling-attribute"
Expand Down Expand Up @@ -69370,6 +69373,117 @@ console.log(plasticButton.outerHTML); // will output '&lt;button is="plastic-but
console.assert(outOfDocument instanceof ExampleElement);
&lt;/script></code></pre>

<h5>Custom state pseudo-classes</h5>
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

<!-- The next two paragraphs are "motivation" from the wicg spec. should they be included...? -->
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

<p>Built-in elements provided by user agents have certain “states” that can change over time
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
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>

<!-- domenic's intro -->
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

<p>This is done via <dfn>custom state pseudo-classes</dfn>. 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 pseudo-classes with corresponding
names prefixed by <span data-x="">"--"</span>.

<!-- my own attempt at an intro/definition -->

<p>The <dfn>custom state pseudo-class</dfn> allows <span data-x="custom element">custom
elements</span> to set and remove <span data-x="pseudo-class">pseudo-classes</span> with custom
names starting with <span data-x="">"--"</span>.</p>

<!-- the wicg spec's intro/definition. which is better? -->

<p>The <span>custom state pseudo-class</span> allows <span data-x="custom element">custom
elements</span> to inform custom element's states to the user agent, and a
<span>pseudo-class</span> to select elements with specific states. The former is the <span
data-x="dom-ElementInternals-states">states</span> IDL attribute of <code>ElementInternals</code>,
and the latter is the <span>custom state pseudo-class</span>.</p>

<div class="example">
<p>The following shows how a <span>custom state pseudo-class</span> 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(:--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:--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;!-- Works even on ::part()s -->
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
&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):--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 @@ -70681,6 +70795,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-classes">Custom state pseudo-classes</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 @@ -71047,6 +71164,105 @@ dictionary <dfn dictionary>ValidityStateFlags</dfn> {

</div>

josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<h5>Exposing custom element states</h5>

<dl class="domintro">
<dt><code data-x=""><var>internals</var>.<span
data-x="dom-ElementInternals-states">states</span>.<span
data-x="dom-CustomStateSet-add">add</span>(<var>value</var>)</code></dt>
<dd>
<p>Adds the string <var>value</var> to this element's <span>states set</span> to be exposed as a
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
pseudo-class. If <var>value</var> doesn't start with <span data-x="">"--"</span> then an
exception will be thrown.</p>
domenic marked this conversation as resolved.
Show resolved Hide resolved
</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
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
false.</p>
</dd>

<dt><code data-x=""><var>internals</var>.<span
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
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>

josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<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>
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<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>

<p>Each <span>autonomous custom element</span> has a <dfn>states set</dfn>, which is a
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<code>CustomStateSet</code>, initially empty.</p>
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, potentially-significant issue: what about customized built-in (i.e., non-autonomous) custom elements? I don't see anything prohibiting them from working, so I believe right now the spec will crash when you do element.internals.states.

We might need to make the getter steps more complicated? Or we could just expand this to work on customized built-ins? I'm not sure why that restriction was added in the first place...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is because we can't use ElementInternals on custom builtins: #5166

But yeah I don't see any reason why that means we also have to exclude them here. I removed "autonomous", what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

How is this observable if the states set isn't exposed? I'd rather stay aligned with ElementInternals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How is this observable if the states set isn't exposed?

Yeah I agree, you can't get ElementInternals on a custom builtin so I guess it doesn't matter what we say here


<pre><code class="idl">[Exposed=Window]
interface <dfn>CustomStateSet</dfn> {
setlike&lt;DOMString>;
undefined <span data-x="dom-CustomStateSet-add">add</span>(DOMString value);
};</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>states set</span>.</p>
josepharhar marked this conversation as resolved.
Show resolved Hide resolved

<p>The <dfn for="CustomStateSet"><code
data-x="dom-CustomStateSet-add">add(<var>value</var>)</code></dfn> method steps are:</p>

<ol>
<li><p>If <var>value</var> does not match <span data-x="dashed ident">&lt;dashed-ident></span>,
then throw a <span>"<code>SyntaxError</code>"</span> <code>DOMException</code>.</p></li>

<li><p><span data-x="set append">Append</span> <var>value</var> to <span>this</span>'s <span>set
entries</span>.</p></li>

<li><p>Return <var>value</var>.</p></li>
</ol>

<div class="example">
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
<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 @@ -71976,6 +72192,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-classes</dfn></dt>
<dd>
<p>The <span data-x="selector-custom">custom state pseudo-classes</span> are any selector which
begins with <span data-x="dashed ident">&lt;dashed-ident></span>. They must match any element
that is an <span>autonomous custom element</span> and whose <span>states set</span> contains a
josepharhar marked this conversation as resolved.
Show resolved Hide resolved
string matching the name of the pseudo-class.</p>
</dd>
</dl>

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