Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
autoreleaser committed Apr 3, 2024
1 parent 5c85d6c commit f000a18
Show file tree
Hide file tree
Showing 19 changed files with 1,735 additions and 212 deletions.
2 changes: 2 additions & 0 deletions latest/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"imports": {
"@lit/reactive-element": "./runtime/@lit/reactive-element/reactive-element.js",
"@lit/reactive-element/": "./runtime/@lit/reactive-element/",
"element-internals-polyfill": "./runtime/element-internals-polyfill/dist/index.js",
"element-internals-polyfill/": "./runtime/element-internals-polyfill/dist/",
"lit": "./runtime/lit/index.js",
"lit/": "./runtime/lit/",
"lit-element": "./runtime/lit-element/lit-element.js",
Expand Down
416 changes: 208 additions & 208 deletions latest/index.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions latest/index.js.map

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions latest/runtime/element-internals-polyfill/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2021 Caleb Williams

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
208 changes: 208 additions & 0 deletions latest/runtime/element-internals-polyfill/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Element Internals Polyfill

This package is a polyfill for the [`ElementInternals` standard](https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface). The specification is supported by current releases of Chromium and Firefox.

## Use case

The primary use case for `ElementInternals` right now is allowing custom elements full participation in HTML forms. To do this, it provides any element designated as `formAssociated` access to a handful of utilities.

The `ElementInternals` API also offers users access to increased utilities for accessibility by exposing the [Accessibility Object Model](https://wicg.github.io/aom/explainer.html) to the element.

## Installation

This package is available on `npm` under the name `element-internals-polyfill`
and can be installed with [npm](https://docs.npmjs.com/getting-started),
[yarn](https://yarnpkg.com/en/docs/getting-started), [unpkg](https://unpkg.com)
or however else you consume dependencies.

### Example commands:

npm:
```bash
npm i element-internals-polyfill
```

yarn:
```bash
yarn add element-internals-polyfill
```

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

unpkg:
```javascript
import 'https://unpkg.com/element-internals-polyfill';
```

## How it works

To do this, add the `static get formAssociated` to a custom element and call the `attachInternals` method to return a new instance of the `ElementInternals` interface:

```javascript
class MyElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
}
```

This works by doing several things under the hood. First, there is a feature check for the `ElementInternals` object on the window. If that does not exist, the polyfill wires up a global [`MutationObserver`](https://developer.mozilla.org/en/docs/Web/API/MutationObserver) on the document to watch for additions to the DOM that the polyfill might need.

It also monkey-patches `HTMLElement.prototype.attachShadow` to wire up a similar listener on any created shadow roots and to remove the watcher if the shadow root is removed.

The polyfill will also monkey-patch `window.FormData` to attach any custom elements to that feature as well.

The currently-supported features of the polyfill are:

### Form-associated custom elements

To create a form-associated custom element using `ElementInternals`, the element's class must have a static `formAssociated` member that returns `true`.

```javascript
class MyFormControl extends HTMLElement {
static get formAssociated() {
return true;
}

constructor() {
super();
this.internals = this.attachInternals();
}
}
```

In the above example, the form control will now have access to several unique APIs for participating in a form:

- Labels will be wired up properly as they would with any built-in input. The polyfill achieves this by applying an `aria-labelledby` attribute to the host element and referencing any labels with a `for` attribute corresponding to the host's `id`. A reference to these labels can be found under `this.internals.labels`.
- The internals interface will have access to the host element's form if one exists under `this.internals.form`.
- If the element has a name, a refernce to the host element will be saved on the form object.

In addition to the above the `ElementInternals` prototype has access to several form-specific methods including:

- `checkValidity`: Will return the validity state of the form control.
- `reportValidity`: Will trigger an `invalid` event if the form control is invalid. For the polyfill this method will not trigger the `validationMessage` to show to the user, that is a task left to the consumer.
- `setFormValue`: Sets the form control's value on the form. This value will be attached to the form's `FormData` method.
- `setValidity`: Takes two arguments, the first being a partial validity object that will update the control's validity object and the second being an optional validation message (required if the form is invalid). If this object is missing the method will throw an error. If the first argument is an object literal the form will be marked as valid.
- `validationMessage`: The element's validation message as set by callse to `ElementInternals.prototype.setValidity`.
- `validity`: The validity controller which is identical to the interface of `HTMLInputElement.prototype.validity`.
- `willValidate`: Will be `true` if the control is set to participate in a form.

### Accessibility controls

In addition to form controls, `ElementInternals` will also surface several accessibility methods for any element with internals attached. A list of supported properties (and their associated attributes) follows:

- `ariaAtomic`: 'aria-atomic'
- `ariaAutoComplete`: 'aria-autocomplete'
- `ariaBusy`: 'aria-busy'
- `ariaChecked`: 'aria-checked'
- `ariaColCount`: 'aria-colcount'
- `ariaConIndex`: 'aria-colindex'
- `ariaColSpan`: 'aria-colspan'
- `ariaCurrent`: 'aria-current'
- `ariaDisabled`: 'aria-disabled'
- `ariaExpanded`: 'aria-expanded'
- `ariaHasPopup`: 'aria-haspopup'
- `ariaHidden`: 'aria-hidden'
- `ariaKeyShortcuts`: 'aria-keyshortcuts'
- `ariaLabel`: 'aria-label'
- `ariaLevel`: 'aria-level'
- `ariaLive`: 'aria-live'
- `ariaModal`: 'aria-modal'
- `ariaMultiLine`: 'aria-multiline'
- `ariaMultiSelectable`: 'aria-multiselectable'
- `ariaOrientation`: 'aria-orientation'
- `ariaPlaceholder`: 'aria-placeholder'
- `ariaPosInSet`: 'aria-posinset'
- `ariaPressed`: 'aria-pressed'
- `ariaReadOnly`: 'aria-readonly'
- `ariaRelevant`: 'aria-relevant'
- `ariaRequired`: 'aria-required'
- `ariaRoleDescription`: 'aria-roledescription'
- `ariaRowCount`: 'aria-rowcount'
- `ariaRowIndex`: 'aria-rowindex'
- `ariaRowSpan`: 'aria-rowspan'
- `ariaSelected`: 'aria-selected'
- `ariaSort`: 'aria-sort'
- `ariaValueMax`: 'aria-valuemax'
- `ariaValueMin`: 'aria-valuemin'
- `ariaValueNow`: 'aria-valuenow'
- `ariaValueText`: 'aria-valuetext'

For example, if you are creating a control that has a checked property, you will likely could set the `internals.ariaChecked` property to `'true'`. In polyfilled browsers, this will result in adding `aria-checked="true"` to the host's attributes. In fully-supported browsers, this attribute will not be reflected although the checked property will be reflected in the accessibility object model.

```javascript
class CheckedControl extends HTMLElement {
#checked = false;
#internals = this.attachInternals();

get checked() {
return this.#checked;
}

set checked(isChecked) {
this.#checked = isChecked;
this.#internals.ariaChecked = isChecked.toString();
}
}
```

### 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.add('--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, as well as a `state--foo` shadow part in supporting browsers.

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;
}
```

The shadow part allows matching the custom state from _outside_ a shadow tree, with similarly duplicated rules:

```css
/** Supporting browsers */
::part(bar):--foo {
color: rebeccapurple;
}

/** Polyfilled browsers (that however support shadow parts) */
::part(bar 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`.
- The polyfill does support the outcomes of the [Accessibility Object Model](https://wicg.github.io/aom/explainer.html#) for applying accessibility rules on the DOM object. However, the spec states that updates using AOM will not be reflected by DOM attributes, but only on the element's accesibility object. However, to emulate this behavior before it is fully supported, it is necessary to use the attributes. If you choose to use this feature, please note that behavior in polyfilled browsers and non-polyfilled browsers will be different; however, the outcome for users will be the same.
- It is currently impossible to set form states to `:invalid` and `:valid` so this polyfill replaces those with the `[internals-invalid]` and `[internals-valid]` attributes on the host element. The proper selector for invalid elements will be `:host(:invalid), :host([internals-invalid])`.
- Exposing custom states as shadow parts means that any element using custom states in a shadow tree can be matched using `::part(state--foo)` in polyfilled browsers, even if the author didn't intend to expose it. This was deemed an acceptable trade-off, compared to tracking explicitly exposed elements using a mutation observer.

## A note about versioning

This packages doesn't necessarily follow semantic versioning. As the spec is still under consideration and implementation by browser vendors, the features supported by this package will change (generally following Chrome's implementation).
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ICustomElement } from "./types.js";
export type CustomState = `--${string}`;
export declare class CustomStateSet extends Set<CustomState> {
static get isPolyfilled(): boolean;
constructor(ref: ICustomElement);
add(state: CustomState): this;
clear(): void;
delete(state: CustomState): boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export declare class HTMLFormControlsCollection implements HTMLFormControlsCollection {
#private;
constructor(elements: any);
[index: number]: Element;
get length(): number;
[Symbol.iterator](): any;
item(i: any): Element;
namedItem(name: any): Element;
}
34 changes: 34 additions & 0 deletions latest/runtime/element-internals-polyfill/dist/ValidityState.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** Emulate the browser's default ValidityState object */
export declare class ValidityState implements ValidityState {
badInput: boolean;
customError: boolean;
patternMismatch: boolean;
rangeOverflow: boolean;
rangeUnderflow: boolean;
stepMismatch: boolean;
tooLong: boolean;
tooShort: boolean;
typeMismatch: boolean;
valid: boolean;
valueMissing: boolean;
constructor();
}
/**
* Reset a ValidityState object back to valid
* @param {ValidityState} validityObject - The object to modify
* @return {ValidityState} - The modified ValidityStateObject
*/
export declare const setValid: (validityObject: ValidityState) => ValidityState;
/**
* Reconcile a ValidityState object with a new state object
* @param {ValidityState} - The base object to reconcile with new state
* @param {Object} - A partial ValidityState object to override the original
* @return {ValidityState} - The updated ValidityState object
*/
export declare const reconcileValidity: (validityObject: ValidityState, newState: Partial<ValidityState>, form: HTMLFormElement) => ValidityState;
/**
* Check if a partial ValidityState object should be valid
* @param {Object} - A partial ValidityState object
* @return {Boolean} - Should the new object be valid
*/
export declare const isValid: (validityState: Partial<ValidityState>) => boolean;
3 changes: 3 additions & 0 deletions latest/runtime/element-internals-polyfill/dist/aom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { IAom, IElementInternals } from './types.js';
export declare const aom: IAom;
export declare const initAom: (ref: Element, internals: IElementInternals) => void;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ValidityState } from './ValidityState.js';
import { IElementInternals, ICustomElement, LabelsList } from './types.js';
import { CustomStateSet } from './CustomStateSet.js';
export declare class ElementInternals implements IElementInternals {
ariaAtomic: string;
ariaAutoComplete: string;
ariaBusy: string;
ariaChecked: string;
ariaColCount: string;
ariaColIndex: string;
ariaColIndexText: string;
ariaColSpan: string;
ariaCurrent: string;
ariaDisabled: string;
ariaExpanded: string;
ariaHasPopup: string;
ariaHidden: string;
ariaInvalid: string;
ariaKeyShortcuts: string;
ariaLabel: string;
ariaLevel: string;
ariaLive: string;
ariaModal: string;
ariaMultiLine: string;
ariaMultiSelectable: string;
ariaOrientation: string;
ariaPlaceholder: string;
ariaPosInSet: string;
ariaPressed: string;
ariaReadOnly: string;
ariaRelevant: string;
ariaRequired: string;
ariaRoleDescription: string;
ariaRowCount: string;
ariaRowIndex: string;
ariaRowIndexText: string;
ariaRowSpan: string;
ariaSelected: string;
ariaSetSize: string;
ariaSort: string;
ariaValueMax: string;
ariaValueMin: string;
ariaValueNow: string;
ariaValueText: string;
role: string;
states: CustomStateSet;
static get isPolyfilled(): boolean;
constructor(ref: ICustomElement);
/**
* Will return true if the element is in a valid state
*/
checkValidity(): boolean;
/** The form element the custom element is associated with */
get form(): HTMLFormElement;
/** A list of all relative form labels for this element */
get labels(): LabelsList;
/** Will report the elements validity state */
reportValidity(): boolean;
/** Sets the element's value within the form */
setFormValue(value: string | FormData | null): void;
/**
* Sets the element's validity. The first argument is a partial ValidityState object
* reflecting the changes to be made to the element's validity. If the element is invalid,
* the second argument sets the element's validation message.
*
* If the field is valid and a message is specified, the method will throw a TypeError.
*/
setValidity(validityChanges: Partial<ValidityState>, validationMessage?: string, anchor?: HTMLElement): void;
get shadowRoot(): ShadowRoot | null;
/** The element's validation message set during a call to ElementInternals.setValidity */
get validationMessage(): string;
/** The current validity state of the object */
get validity(): ValidityState;
/** If true the element will participate in a form's constraint validation. */
get willValidate(): boolean;
}
declare global {
interface CustomElementConstructor {
formAssociated?: boolean;
}
interface Window {
ElementInternals: typeof ElementInternals;
}
}
export declare function isElementInternalsSupported(): boolean;
19 changes: 19 additions & 0 deletions latest/runtime/element-internals-polyfill/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ElementInternals } from './element-internals.js';
import { CustomStateSet } from './CustomStateSet.js';
import './element-internals.js';
import { IElementInternals } from './types.js';
export * from './types.js';
declare global {
interface Window {
CustomStateSet: typeof CustomStateSet;
ElementInternals: typeof ElementInternals;
ShadyDOM: any;
}
interface HTMLElement {
/**
* Attaches an ElementInternals instance to a custom element. Calling this method
* on a built-in element will throw an error.
*/
attachInternals(): ElementInternals & IElementInternals;
}
}
Loading

0 comments on commit f000a18

Please sign in to comment.