Skip to content

Commit

Permalink
update docs to reflect new css custom state syntax (#31258)
Browse files Browse the repository at this point in the history
* update docs to reflect new css custom state syntax

* fix typos

* fix top level description of add

* Update files/en-us/web/api/customstateset/add/index.md

* Update files/en-us/web/api/customstateset/index.md

* Apply suggestions from code review

* Update files/en-us/web/api/customstateset/index.md

* Update files/en-us/web/api/customstateset/index.md

* Update files/en-us/web/api/customstateset/index.md

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update files/en-us/web/api/customstateset/index.md

* Fix bad selector

Co-authored-by: Hamish Willee <[email protected]>

* remove mention of sytnaxerrors, as these no longer occur

* remove mention of <custom-ident>

* remove mention of <custom-ident>

* Update files/en-us/web/api/customstateset/add/index.md

* Update files/en-us/web/api/customstateset/index.md

---------

Co-authored-by: Hamish Willee <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 4, 2024
1 parent 133ee87 commit c6e8c32
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 50 deletions.
15 changes: 6 additions & 9 deletions files/en-us/web/api/customstateset/add/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ browser-compat: api.CustomStateSet.add

{{APIRef("Web Components")}}{{SeeCompatTable}}

The **`add`** method of the {{domxref("CustomStateSet")}} interface adds an item to the `CustomStateSet`, after checking that the value is in the correct format.
The **`add`** method of the {{domxref("CustomStateSet")}} interface adds value representing a custom state to the `CustomStateSet`.

Custom elements with a specific state can be selected using the [`:state()`](/en-US/docs/Web/CSS/:state) pseudo-class, specifying the desired state as an argument.

## Syntax

Expand All @@ -21,26 +23,21 @@ add(value)
### Parameters

- `value`
- : A string which must be a `<dashed-ident>`, with the form `--mystate`.
- : A string that represents the custom state.

### Return value

Undefined.

### Exceptions

- `SyntaxError` {{domxref("DOMException")}}
- : Thrown if the string is not a `<dashed-ident>`.

## Examples

The following function adds the state `--checked` to a `CustomStateSet`.
The following function adds the state `checked` to a `CustomStateSet`.

```js
class MyCustomElement extends HTMLElement {
set checked(flag) {
if (flag) {
this._internals.states.add("--checked");
this._internals.states.add("checked");
}
}
}
Expand Down
127 changes: 94 additions & 33 deletions files/en-us/web/api/customstateset/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The interface can be used to expose the internal states of a custom element, all
## Instance methods

- {{domxref("CustomStateSet.add()")}} {{Experimental_Inline}}
- : Adds a value to the set, first checking that the _value_ is a `<dashed-ident>`.
- : Adds a value to the set.
- {{domxref("CustomStateSet.clear()")}} {{Experimental_Inline}}
- : Removes all elements from the `CustomStateSet` object.
- {{domxref("CustomStateSet.delete()")}} {{Experimental_Inline}}
Expand Down Expand Up @@ -53,7 +53,7 @@ To make the {{domxref("CustomStateSet")}} available, a custom element must first
Note that `ElementInternals` cannot be attached to a custom element based on a built-in element, so this feature only works for autonomous custom elements (see [github.com/whatwg/html/issues/5166](https://github.com/whatwg/html/issues/5166)).

The `CustomStateSet` instance is a [`Set`-like object](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#set-like_browser_apis) that can hold an ordered set of state values.
Each value is a dashed identifier, with the format: `--mystatename`.
Each value is a custom identifier.
Identifiers can be added to the set or deleted.
If an identifier is present in the set the particular state is `true`, while if it is removed the state is `false`.

Expand All @@ -64,23 +64,26 @@ The states can be used within the custom element but are not directly accessible
### Interaction with CSS

Developers can select a custom element with a specific state using its state _custom state pseudo-class_.
The format of this pseudo-class is `:--mystatename`, where `--mystatename` is the state as defined in the element.
The format of this pseudo-class is `:state(mystatename)`, where `mystatename` is the state as defined in the element.

The custom state pseudo-class matches the custom element only if the state is `true` (i.e. if `--mystatename` is present in the `CustomStateSet`).
The custom state pseudo-class matches the custom element only if the state is `true` (i.e. if `mystatename` is present in the `CustomStateSet`).

> **Warning:** Chrome supports a deprecated syntax that selects custom states using a CSS `<dashed-ident>` rather than the `:state()` function.
> For information about how to support both approaches see the [Compatibility with `<dashed-ident>` syntax](compability_with_dashed-ident_syntax) section below.
## Examples

### Labeled Checkbox

This example, which is adapted from the specification, demonstrates a custom checkbox element that has an internal "checked" state.
This is mapped to the `--checked` custom state, allowing styling to be applied using the `:--checked` custom state pseudo class.
This is mapped to the `checked` custom state, allowing styling to be applied using the `:state(checked)` custom state pseudo class.

#### JavaScript

First we define our class `LabeledCheckbox` which extends from `HTMLElement`.
In the constructor we call the `super()` method, leaving most of the "work" to `connectedCallback()`, which is invoked when a custom element is added to the page.
The content of the element is defined using a `<style>` element to be the text `[]` or `[x]` followed by a label.
What's noteworthy here is that the custom state pseudo class is used to select the text to display: `:host(:--checked)::`.
What's noteworthy here is that the custom state pseudo class is used to select the text to display: `:host(:state(checked))`.
After the example below, we'll cover what's happening in the snippet in more detail.

```js
Expand All @@ -104,20 +107,20 @@ class LabeledCheckbox extends HTMLElement {
white-space: pre;
font-family: monospace;
}
:host(:--checked)::before { content: '[x]'; background: grey; }
:host(:state(checked))::before { content: '[x]'; background: grey; }
</style>
<slot>Label</slot>`;
}

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

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

Expand All @@ -132,8 +135,8 @@ In the `LabeledCheckbox` class:

- The `connectedCallback()` method uses {{domxref("HTMLElement.attachInternals()", "`this.attachInternals()`")}} to attach an {{domxref("ElementInternals", "`ElementInternals`")}} object.
- In the `get checked()` and `set checked()` we use `ElementInternals.states` to get the `CustomStateSet`.
- The `set checked(flag)` method adds the `"--checked"` dashed identifier to the `CustomStateSet` if the flag is set and delete the identifier if the flag is `false`.
- The `get checked()` method just checks whether the `--checked` property is defined in the set.
- The `set checked(flag)` method adds the `"checked"` identifier to the `CustomStateSet` if the flag is set and delete the identifier if the flag is `false`.
- The `get checked()` method just checks whether the `checked` property is defined in the set.
- The property value is toggled when the element is clicked.

We then call the {{domxref("CustomElementRegistry/define", "define()")}} method on the object returned by {{domxref("Window.customElements")}} in order to register the custom element:
Expand All @@ -152,13 +155,13 @@ After registering the custom element we can use the element in HTML as shown:

#### CSS

Finally we use the `:--checked` custom state pseudo class to select CSS for when the box is checked.
Finally we use the `:state(checked)` custom state pseudo class to select CSS for when the box is checked.

```css
labeled-checkbox {
border: dashed red;
}
labeled-checkbox:--checked {
labeled-checkbox:state(checked) {
border: solid;
}
```
Expand All @@ -174,8 +177,8 @@ Click the element to see a different border being applied as the checkbox `check
This example shows how to handle the case where the custom element has an internal property with multiple possible value.

The custom element in this case has a `state` property with allowed values: "loading", "interactive" and "complete".
To make this work, we map each value to its custom state and create code to ensure that only the dashed identifier corresponding to the internal state is set.
You can see this in the implementation of the `set state()` method: we set the internal state, add the dashed identifier for the matching custom state to `CustomStateSet`, and remove the dashed identifiers associated with all the other values.
To make this work, we map each value to its custom state and create code to ensure that only the identifier corresponding to the internal state is set.
You can see this in the implementation of the `set state()` method: we set the internal state, add the identifier for the matching custom state to `CustomStateSet`, and remove the identifiers associated with all the other values.

Most of the remaining code is similar to the example that demonstrates a single boolean state (we show different text for each state as the user toggles through them).

Expand All @@ -200,9 +203,9 @@ class ManyStateElement extends HTMLElement {
font-family: monospace;
}
:host::before { content: '[ unknown ]'; white-space: pre; }
:host(:--loading)::before { content: '[ loading ]' }
:host(:--interactive)::before { content: '[ interactive ]' }
:host(:--complete)::before { content: '[ complete ]' }
:host(:state(loading))::before { content: '[ loading ]' }
:host(:state(interactive))::before { content: '[ interactive ]' }
:host(:state(complete))::before { content: '[ complete ]' }
</style>
<slot>Click me</slot>`;
}
Expand All @@ -213,22 +216,22 @@ class ManyStateElement extends HTMLElement {

set state(stateName) {
// Set internal state to passed value
// Add dashed identifier matching state and delete others
// Add identifier matching state and delete others
if (stateName == "loading") {
this._state = "loading";
this._internals.states.add("--loading");
this._internals.states.delete("--interactive");
this._internals.states.delete("--complete");
this._internals.states.add("loading");
this._internals.states.delete("interactive");
this._internals.states.delete("complete");
} else if (stateName == "interactive") {
this._state = "interactive";
this._internals.states.delete("--loading");
this._internals.states.add("--interactive");
this._internals.states.delete("--complete");
this._internals.states.delete("loading");
this._internals.states.add("interactive");
this._internals.states.delete("complete");
} else if (stateName == "complete") {
this._state = "complete";
this._internals.states.delete("--loading");
this._internals.states.delete("--interactive");
this._internals.states.add("--complete");
this._internals.states.delete("loading");
this._internals.states.delete("interactive");
this._internals.states.add("complete");
}
}

Expand Down Expand Up @@ -258,17 +261,17 @@ This is similar to the example that demonstrates a single boolean state, except

#### CSS

In the CSS we use the three custom state pseudo classes to select CSS for each of the internal state values: `:--loading`, `:--interactive`, `:--complete`.
In the CSS we use the three custom state pseudo classes to select CSS for each of the internal state values: `:state(loading)`, `:state(interactive)`, `:state(complete)`.
Note that the custom element code ensures that only one of these custom states can be defined at a time.

```css
many-state-element:--loading {
many-state-element:state(loading) {
border: dotted grey;
}
many-state-element:--interactive {
many-state-element:state(interactive) {
border: dashed blue;
}
many-state-element:--complete {
many-state-element:state(complete) {
border: solid green;
}
```
Expand All @@ -279,6 +282,64 @@ Click the element to see a different border being applied as the state changes.

{{EmbedLiveSample("Non-boolean internal states", "100%", 50)}}

## Compability with `<dashed-ident>` syntax

Previously custom elements with custom states were selected using a `<dashed-ident>` instead of the [`:state()`](/en-US/docs/Web/CSS/:state) function.
Browsers that don't support `:state()`, including versions of Chrome, will throw an error when supplied with an ident that is not prefixed with the double dash.
If support for these browsers is required, it is possible to use a `<dashed-ident>` as the state's value, and select it with both the `:--mystate` and `:state(--mystate)` CSS selector:

### Using double dash prefixed idents

#### JavaScript

```js
class CompatibleStateElement extends HTMLElement {
connectedCallback() {
const internals = this.attachInternals();
// The double dash is required in browsers with the
// legacy syntax, but works with the modern syntax
internals.states.set("--loaded");
}
}
```

#### CSS

```css
compatible-state-element:is(:--loaded, :state(--loaded)) {
border: solid green;
}
```

### Using a try catch block

An alternative solution can be to use a `try` `catch` block to fall back to the legacy syntax:

#### JavaScript

```js
class CompatibleStateElement extends HTMLElement {
connectedCallback() {
const internals = this.attachInternals();
// The double dash is required in browsers with the
// legacy syntax, not supplying it will throw
try {
internals.states.set("loaded");
} catch {
internals.states.set("--loaded");
}
}
}
```

#### CSS

```css
compatible-state-element:is(:--loaded, :state(loaded)) {
border: solid green;
}
```

## Specifications

{{Specifications}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ The code below shows how this works using the example of an autonomous custom el

The `collapsed` state is represented as a boolean property (with setter and getter methods) that is not visible outside of the element.
To make this state selectable in CSS the custom element first calls {{domxref("HTMLElement.attachInternals()")}} in its constructor in order to attach an {{domxref("ElementInternals")}} object, which in turn provides access to a {{domxref("CustomStateSet")}} through the {{domxref("ElementInternals.states")}} property.
The setter for the (internal) collapsed state adds the _dashed identifier_ `--hidden` to the `CustomStateSet` when the state is `true`, and removes it when the state is `false`.
The dashed identifier is just a string preceded by two dashes: in this case we called it `--hidden`, but we could have just as easily called it `--collapsed`.
The setter for the (internal) collapsed state adds the _identifier_ `hidden` to the `CustomStateSet` when the state is `true`, and removes it when the state is `false`.
The identifier is just a string: in this case we called it `hidden`, but we could have just as easily called it `collapsed`.

```js
class MyCustomElement extends HTMLElement {
Expand All @@ -197,16 +197,16 @@ class MyCustomElement extends HTMLElement {
}

get collapsed() {
return this._internals.states.has("--hidden");
return this._internals.states.has("hidden");
}

set collapsed(flag) {
if (flag) {
// Existence of identifier corresponds to "true"
this._internals.states.add("--hidden");
this._internals.states.add("hidden");
} else {
// Absence of identifier corresponds to "false"
this._internals.states.delete("--hidden");
this._internals.states.delete("hidden");
}
}
}
Expand All @@ -215,14 +215,14 @@ class MyCustomElement extends HTMLElement {
customElements.define("my-custom-element", MyCustomElement);
```

After adding `<my-custom-element>` to the HTML we can use the dashed identifier added to the `CustomStateSet`, prefixed with `:`, as a custom state pseudo-class for selecting the element state.
For example, below we select on the `--hidden` state being true (and hence the element's `collapsed` state) using the `:--hidden` selector, and remove the border.
After adding `<my-custom-element>` to the HTML we can use the identifier added to the `CustomStateSet`, passed to the `:state()` function, as a custom state pseudo-class for selecting the element state.
For example, below we select on the `hidden` state being true (and hence the element's `collapsed` state) using the `:hidden` selector, and remove the border.

```css
my-custom-element {
border: dashed red;
}
my-custom-element:--hidden {
my-custom-element:state(hidden) {
border: none;
}
```
Expand Down

0 comments on commit c6e8c32

Please sign in to comment.