Skip to content

Commit 2e4c1ad

Browse files
feat: add CustomStateSet
1 parent 2f7dba0 commit 2e4c1ad

8 files changed

+6017
-5511
lines changed

Diff for: README.md

+37
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ yarn:
2727
yarn add element-internals-polyfill
2828
```
2929

30+
skypack:
31+
```javascript
32+
import 'https://cdn.skypack.dev/element-internals-polyfill';
33+
```
34+
3035
unpkg:
3136
```javascript
3237
import 'https://unpkg.com/element-internals-polyfill';
@@ -127,6 +132,38 @@ In addition to form controls, `ElementInternals` will also surface several acces
127132
- `ariaValueNow`: 'aria-valuenow'
128133
- `ariaValueText`: 'aria-valuetext'
129134

135+
### State API
136+
137+
`ElementInternals` exposes an API for creating custom states on an element. For instance if a developer wanted to signify to users that an element was in state `foo`, they could call `internals.states.set('--foo')`. This would make the element match the selector `:--foo`. Unfortunately in non-supporting browsers this is an invalid selector and will throw an error in JS and would cause the parsing of a CSS rule to fail. As a result, this polyfill will add states using the `state--foo` attribute to the host element.
138+
139+
In order to properly select these elements in CSS, you will need to duplicate your rule as follows:
140+
141+
```css
142+
/** Supporting browsers */
143+
:--foo {
144+
color: rebeccapurple;
145+
}
146+
147+
/** Polyfilled browsers */
148+
[state--foo] {
149+
color: rebeccapurple;
150+
}
151+
```
152+
153+
Trying to combine selectors like `:--foo, [state--foo]` will cause the parsing of the rule to fail because `:--foo` is an invalid selector. As a potential optimization, you can use CSS `@supports` as follows:
154+
155+
```css
156+
@supports selector(:--foo) {
157+
/** Native supporting code here */
158+
}
159+
160+
@supports not selector([state--foo]) {
161+
/** Code for polyfilled browsers here */
162+
}
163+
```
164+
165+
Be sure to understand how your supported browsers work with CSS `@supports` before using the above strategy.
166+
130167
## Current limitations
131168

132169
- Right now providing a cross-browser compliant version of `ElementInternals.reportValidity` is not supported. The method essentially behaves as a proxy for `ElementInternals.checkValidity`.

Diff for: package-lock.json

+5,892-5,509
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@open-wc/testing-helpers": "^1.7.1",
4949
"@rollup/plugin-node-resolve": "^7.1.3",
5050
"@rollup/plugin-typescript": "^6.0.0",
51+
"@web/dev-server-esbuild": "^0.2.12",
5152
"@web/test-runner": "^0.12.16",
5253
"@web/test-runner-playwright": "^0.8.4",
5354
"babel-plugin-istanbul": "^6.0.0",

Diff for: src/CustomStateSet.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ICustomElement } from "./types";
2+
3+
/** Save a reference to the ref for teh CustomStateSet */
4+
const customStateMap = new WeakMap<CustomStateSet, ICustomElement>();
5+
6+
export class CustomStateSet extends Set<string> {
7+
constructor(ref: ICustomElement) {
8+
super();
9+
10+
customStateMap.set(this, ref);
11+
}
12+
13+
add(state: string) {
14+
if (!/^--/.exec(state) || typeof state !== 'string') {
15+
throw new DOMException(`Failed to execute 'add' on 'CustomStateSet': The specified value ${state} must start with '--'.`);
16+
}
17+
const result = super.add(state);
18+
const ref = customStateMap.get(this);
19+
ref.toggleAttribute(`state${state}`, true);
20+
return result;
21+
}
22+
23+
clear() {
24+
for (let [entry] of this.entries()) {
25+
this.delete(entry);
26+
}
27+
super.clear();
28+
}
29+
30+
delete(state: string) {
31+
const result = super.delete(state);
32+
const ref = customStateMap.get(this);
33+
ref.toggleAttribute(`state${state}`, false);
34+
return result;
35+
}
36+
}

Diff for: src/element-internals.ts

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { initAom } from './aom';
2222
import { ValidityState, reconcileValidty, setValid } from './ValidityState';
2323
import { deferUpgrade, observerCallback, observerConfig } from './mutation-observers';
2424
import { IElementInternals, ICustomElement, LabelsList } from './types';
25+
import { CustomStateSet } from './CustomStateSet';
2526

2627
export class ElementInternals implements IElementInternals {
2728
ariaAtomic: string;
@@ -61,6 +62,8 @@ export class ElementInternals implements IElementInternals {
6162
ariaValueNow: string;
6263
ariaValueText: string;
6364

65+
states: CustomStateSet;
66+
6467
static get isPolyfilled() {
6568
return true;
6669
}
@@ -71,6 +74,7 @@ export class ElementInternals implements IElementInternals {
7174
}
7275
const rootNode = ref.getRootNode();
7376
const validity = new ValidityState();
77+
this.states = new CustomStateSet(ref);
7478
refMap.set(this, ref);
7579
validityMap.set(this, validity);
7680
internalsMap.set(ref, this);

Diff for: test/CustomStateSet.test.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { fixture, html, expect, fixtureCleanup } from '@open-wc/testing';
2+
import { ICustomElement } from '../dist';
3+
import { CustomStateSet } from '../src/CustomStateSet';
4+
5+
describe('CustomStateSet polyfill', () => {
6+
let el: HTMLElement;
7+
let set: CustomStateSet;
8+
9+
beforeEach(async () => {
10+
el = await fixture(html`<div></div>`);
11+
set = new CustomStateSet(el as ICustomElement);
12+
});
13+
14+
afterEach(() => {
15+
fixtureCleanup();
16+
});
17+
18+
describe('it will add attributes', async () => {
19+
set.add('--foo');
20+
expect(el.hasAttribute('state--foo')).to.be.true;
21+
});
22+
23+
describe('it will remove attributes', async () => {
24+
set.add('--foo');
25+
expect(el.hasAttribute('state--foo')).to.be.true;
26+
27+
set.delete('--foo');
28+
expect(el.hasAttribute('state--foo')).to.be.false;
29+
});
30+
31+
describe('it will clear all attributes', async () => {
32+
set.add('--foo');
33+
set.add('--bar');
34+
35+
expect(el.hasAttribute('state--foo')).to.be.true;
36+
expect(el.hasAttribute('state--bar')).to.be.true;
37+
38+
set.clear();
39+
expect(el.hasAttribute('state--foo')).to.be.false;
40+
expect(el.hasAttribute('state--bar')).to.be.false;
41+
});
42+
});

Diff for: test/polyfilledBrowsers.test.js

-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import {
2-
aTimeout,
32
expect,
43
fixture,
54
fixtureCleanup,
65
html,
76
} from '@open-wc/testing';
8-
import { spy } from 'sinon';
97
import '../dist/index.js';
108

119
describe('ElementInternals polyfill behavior', () => {

Diff for: web-test-runner.config.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { esbuildPlugin } from '@web/dev-server-esbuild';
2+
3+
export default {
4+
plugins: [esbuildPlugin({ ts: true, target: 'auto' })],
5+
};

0 commit comments

Comments
 (0)