Skip to content

Commit

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

Here are some spec issues where this feature has been discussed:
w3ctag/design-reviews#428
w3c/csswg-drafts#4805
  • Loading branch information
josepharhar committed Nov 2, 2022
1 parent 7d8951c commit 17f9aae
Showing 1 changed file with 159 additions and 0 deletions.
159 changes: 159 additions & 0 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,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>
</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 +3903,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>
<li>The <dfn data-x-href="https://drafts.csswg.org/css-values-4/#typedef-dashed-ident">dashed ident</dfn> identifier</li>
</ul>

<p>The term <dfn data-x="css-styling-attribute"
Expand Down Expand Up @@ -70681,6 +70683,8 @@ 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>;

[SameObject] readonly attribute <span>CustomStateSet</span> <dfn data-x="dom-elementinternals-states">states</dfn>;
};

// <a href="#accessibility-semantics">Accessibility semantics</a>
Expand Down Expand Up @@ -71047,6 +71051,153 @@ dictionary <dfn dictionary>ValidityStateFlags</dfn> {

</div>

<h4>Custom state pseudo class</h4>

<!-- 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>

<!-- The next two paragraphs are "motivation" from the wicg spec. should they be included...? -->

<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>

<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>

<!-- Works even on ::part()s -->
&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>

<!-- TODO should i keep this heading? its in the wicg draft spec -->
<h5>Exposing custom element states</h5>

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

<span data-x="concept-element-dom">DOM interface</span>:
<pre><code class="idl">[Exposed=Window]
interface <dfn>CustomStateSet</dfn> {
setlike&lt;DOMString>;
undefined add(DOMString value);
};</code></pre>

<!-- TODO should this go next to the IDL code of ElementInternals instead? -->
<p>The <dfn for="HTMLElement"><code data-x="dom-htmlelement-states">states</code></dfn> IDL
attribute must return the <span>states set</span>.</p>

<p>The <dfn for="CustomStateSet"><code
data-x="dom-customstateset-add">add(<var>value</var>)</code></dfn> method must run the following
steps:</p>

<ol>
<!-- TODO the draft spec doesn't say what "match" means. Should it be defined? Should I just
say "starts with" instead? -->
<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>Invoke the <span>default add operation</span>, which the <code
data-x="">setlike&lt;DOMString></code> would have if <code>CustomStateSet</code> interface had no
<code data-x="dom-customstateset-add">add</code> operation, given <var>value</var>.</p></li>
</ol>

<div class="example">
<p><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>.

<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 +72127,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 <span data-x="selector-custom">custom state pseudo class</span> is any selector which
begins with <span data-x="dashed ident">&lt;dashed-ident></span>. It must match any element that
is an <span>autonomous custom element</span> and whose <span>states set</span> contains a 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

0 comments on commit 17f9aae

Please sign in to comment.