Skip to content

Commit

Permalink
feat: add CustomStateSet
Browse files Browse the repository at this point in the history
  • Loading branch information
calebdwilliams committed May 23, 2021
1 parent 2f7dba0 commit 2e4c1ad
Show file tree
Hide file tree
Showing 8 changed files with 6,017 additions and 5,511 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ yarn:
yarn add element-internals-polyfill
```

skypack:
```javascript
import 'https://cdn.skypack.dev/element-internals-polyfill';
```

unpkg:
```javascript
import 'https://unpkg.com/element-internals-polyfill';
Expand Down Expand Up @@ -127,6 +132,38 @@ In addition to form controls, `ElementInternals` will also surface several acces
- `ariaValueNow`: 'aria-valuenow'
- `ariaValueText`: 'aria-valuetext'

### State API

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

In order to properly select these elements in CSS, you will need to duplicate your rule as follows:

```css
/** Supporting browsers */
:--foo {
color: rebeccapurple;
}

/** Polyfilled browsers */
[state--foo] {
color: rebeccapurple;
}
```

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:

```css
@supports selector(:--foo) {
/** Native supporting code here */
}

@supports not selector([state--foo]) {
/** Code for polyfilled browsers here */
}
```

Be sure to understand how your supported browsers work with CSS `@supports` before using the above strategy.

## Current limitations

- Right now providing a cross-browser compliant version of `ElementInternals.reportValidity` is not supported. The method essentially behaves as a proxy for `ElementInternals.checkValidity`.
Expand Down
11,401 changes: 5,892 additions & 5,509 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@open-wc/testing-helpers": "^1.7.1",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-typescript": "^6.0.0",
"@web/dev-server-esbuild": "^0.2.12",
"@web/test-runner": "^0.12.16",
"@web/test-runner-playwright": "^0.8.4",
"babel-plugin-istanbul": "^6.0.0",
Expand Down
36 changes: 36 additions & 0 deletions src/CustomStateSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ICustomElement } from "./types";

/** Save a reference to the ref for teh CustomStateSet */
const customStateMap = new WeakMap<CustomStateSet, ICustomElement>();

export class CustomStateSet extends Set<string> {
constructor(ref: ICustomElement) {
super();

customStateMap.set(this, ref);
}

add(state: string) {
if (!/^--/.exec(state) || typeof state !== 'string') {
throw new DOMException(`Failed to execute 'add' on 'CustomStateSet': The specified value ${state} must start with '--'.`);
}
const result = super.add(state);
const ref = customStateMap.get(this);
ref.toggleAttribute(`state${state}`, true);
return result;
}

clear() {
for (let [entry] of this.entries()) {
this.delete(entry);
}
super.clear();
}

delete(state: string) {
const result = super.delete(state);
const ref = customStateMap.get(this);
ref.toggleAttribute(`state${state}`, false);
return result;
}
}
4 changes: 4 additions & 0 deletions src/element-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { initAom } from './aom';
import { ValidityState, reconcileValidty, setValid } from './ValidityState';
import { deferUpgrade, observerCallback, observerConfig } from './mutation-observers';
import { IElementInternals, ICustomElement, LabelsList } from './types';
import { CustomStateSet } from './CustomStateSet';

export class ElementInternals implements IElementInternals {
ariaAtomic: string;
Expand Down Expand Up @@ -61,6 +62,8 @@ export class ElementInternals implements IElementInternals {
ariaValueNow: string;
ariaValueText: string;

states: CustomStateSet;

static get isPolyfilled() {
return true;
}
Expand All @@ -71,6 +74,7 @@ export class ElementInternals implements IElementInternals {
}
const rootNode = ref.getRootNode();
const validity = new ValidityState();
this.states = new CustomStateSet(ref);
refMap.set(this, ref);
validityMap.set(this, validity);
internalsMap.set(ref, this);
Expand Down
42 changes: 42 additions & 0 deletions test/CustomStateSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { fixture, html, expect, fixtureCleanup } from '@open-wc/testing';
import { ICustomElement } from '../dist';
import { CustomStateSet } from '../src/CustomStateSet';

describe('CustomStateSet polyfill', () => {
let el: HTMLElement;
let set: CustomStateSet;

beforeEach(async () => {
el = await fixture(html`<div></div>`);
set = new CustomStateSet(el as ICustomElement);
});

afterEach(() => {
fixtureCleanup();
});

describe('it will add attributes', async () => {
set.add('--foo');
expect(el.hasAttribute('state--foo')).to.be.true;
});

describe('it will remove attributes', async () => {
set.add('--foo');
expect(el.hasAttribute('state--foo')).to.be.true;

set.delete('--foo');
expect(el.hasAttribute('state--foo')).to.be.false;
});

describe('it will clear all attributes', async () => {
set.add('--foo');
set.add('--bar');

expect(el.hasAttribute('state--foo')).to.be.true;
expect(el.hasAttribute('state--bar')).to.be.true;

set.clear();
expect(el.hasAttribute('state--foo')).to.be.false;
expect(el.hasAttribute('state--bar')).to.be.false;
});
});
2 changes: 0 additions & 2 deletions test/polyfilledBrowsers.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
aTimeout,
expect,
fixture,
fixtureCleanup,
html,
} from '@open-wc/testing';
import { spy } from 'sinon';
import '../dist/index.js';

describe('ElementInternals polyfill behavior', () => {
Expand Down
5 changes: 5 additions & 0 deletions web-test-runner.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { esbuildPlugin } from '@web/dev-server-esbuild';

export default {
plugins: [esbuildPlugin({ ts: true, target: 'auto' })],
};

0 comments on commit 2e4c1ad

Please sign in to comment.