chore: add a11y and SlotMixin docs
This commit is contained in:
parent
d7938ef6b3
commit
d6d3810f7c
7 changed files with 171 additions and 92 deletions
|
|
@ -2,4 +2,5 @@
|
||||||
'@lion/ui': patch
|
'@lion/ui': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Align with SlotMixin rerender functionality + fix interaction state synchronization
|
[input-tel] Align with SlotMixin rerender functionality
|
||||||
|
[input-tel-dropdown] Align with SlotMixin rerender functionality + fix interaction state synchronization
|
||||||
|
|
|
||||||
BIN
docs/fundamentals/rationales/_assets/theoryOfFormsLion.pdf
Normal file
BIN
docs/fundamentals/rationales/_assets/theoryOfFormsLion.pdf
Normal file
Binary file not shown.
28
docs/fundamentals/rationales/accessibility.md
Normal file
28
docs/fundamentals/rationales/accessibility.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Rationales >> Accessibility ||5
|
||||||
|
|
||||||
|
In Lion accessibility is a first class citizen. That means accessibility is never an afterthought.
|
||||||
|
|
||||||
|
Whenever we create a component, we do some thorough research first. Experience from past web component libraries learned us that when accessibility was dealt with in hindsight, a lot of concessions needed to be made. It made us want to go back to the drawing board. Lion was started with this 'fresh drawing board'.
|
||||||
|
In general, we try to leverage out of the box accessible platform solutions as much as possible. When no platform solutions are available, we try to map our components to [aria patterns and widgets as defined by W3C](https://www.w3.org/WAI/ARIA/apg/) where possible.
|
||||||
|
Building accessible components takes a lot of time, practice and knowledge about screen reader and browser implementations. Then, it takes many testing iterations to finetune a component.
|
||||||
|
|
||||||
|
More on this topic can be found in the online panel discussion [Web Components, Design Systems and Accessibility](https://www.youtube.com/watch?v=xz8yRVJMP2k&t=1190s). The discussion also contains a [dedicated section about accessible form components](https://www.youtube.com/watch?v=xz8yRVJMP2k&t=1917s)
|
||||||
|
|
||||||
|
## Shadow roots and accessibility
|
||||||
|
|
||||||
|
Since our components and applications consist of multiple shadow roots that need to be able to reference each other, designing accessible components takes extra strategy and planning.
|
||||||
|
A practical example: all form components inside a form need to be able to lay accessible relations for presenting feedback messages via the screen reader. For this, they need consistent designs so that all form components are interoperable. So, light dom needs to be leveraged until the [AOM specification](https://wicg.github.io/aom/explainer.html) is implemented.
|
||||||
|
|
||||||
|
## Some details about the form system
|
||||||
|
|
||||||
|
A huge part of Lion consists of its form system and components.
|
||||||
|
|
||||||
|
This [presentation about accessible form components](../_assets/theoryOfFormsLion.pdf) explains how accesibility is built-in in all form components.
|
||||||
|
|
||||||
|
## SlotMixin
|
||||||
|
|
||||||
|
Internally, we delegate all intricacies involved in managing light dom to SlotMixin.
|
||||||
|
SlotMixin automatically mediates between light dom provided by the user ('public slots') and light dom provided by the component author ('private slots').
|
||||||
|
Also, SlotMixin allows to hook into the reactive update loop of LitElement (automatically rerendering on property changes) and it automatically respects
|
||||||
|
the scoped registry belonging to the shadow root.
|
||||||
|
More details about SlotMixin can be found in the [SlotMixin documentation](../systems/core/SlotMixin.md)
|
||||||
136
docs/fundamentals/systems/core/SlotMixin.md
Normal file
136
docs/fundamentals/systems/core/SlotMixin.md
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# Systems >> Core >> SlotMixin ||20
|
||||||
|
|
||||||
|
The SlotMixin is made for solving accessibility challenges that inherently come with the usage of shadow dom.
|
||||||
|
Until [AOM](https://wicg.github.io/aom/explainer.html) is in place, it is not possible to create relations between different shadow doms.
|
||||||
|
The need for this can occur in the following situations:
|
||||||
|
|
||||||
|
1. A user defined slot. For instance:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<my-input>
|
||||||
|
<div slot="help-text">Input description</div>
|
||||||
|
</my-input>
|
||||||
|
```
|
||||||
|
|
||||||
|
The help text here needs to be connected to the input element that may live in shadow dom. The input needs to have `aria-describedby="help-text-id".`
|
||||||
|
|
||||||
|
2. An interplay of multiple nested web components. For instance:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<my-fieldset>
|
||||||
|
<my-input></my-input>
|
||||||
|
<my-input></my-input>
|
||||||
|
<div id="msg">Group errror message</div>
|
||||||
|
</my-fieldset>
|
||||||
|
```
|
||||||
|
|
||||||
|
In the case above, all inputs need to be able to refer the error message of their parent.
|
||||||
|
|
||||||
|
In a nutshell: SlotMixin helps you with everything related to rendering light dom (i.e. rendering to slots).
|
||||||
|
So that you can build accessible ui components with ease, while delegating all edge cases to SlotMixin.
|
||||||
|
Edge cases that it solves:
|
||||||
|
|
||||||
|
- rendering light dom in context of scoped customElementRegistries: we respect the customElementRegistry bound to your ShadowRoot
|
||||||
|
- the concept of rerendering based on property effects
|
||||||
|
- easily render lit templates
|
||||||
|
|
||||||
|
So, what does the api look like? SlotMixin can be used like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class AccessibleControl extends SlotMixin(LitElement) {
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
'public-element-slot': () => document.createElement('input'),
|
||||||
|
'_private-template-slot': () => html`<wc-rendered-to-light-dom></wc-rendered-to-light-dom>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SlotFunctionResults
|
||||||
|
|
||||||
|
The `SlotFunctionResult` is the output of the functions provided in `get slots()`. It can output the four types:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Element | TemplateResult | SlotRerenderObject | undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
Below configuration gives an example of all of them, after which we explain when each type should be used
|
||||||
|
|
||||||
|
```js
|
||||||
|
class AccessibleControl extends SlotMixin(LitElement) {
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
// Element
|
||||||
|
'public-element-slot': () => document.createElement('input'),
|
||||||
|
// TemplateResult
|
||||||
|
'_private-template-slot': () => html`<wc-rendered-to-light-dom></wc-rendered-to-light-dom>`,
|
||||||
|
// SlotRerenderObject
|
||||||
|
'rerenderable-slot': () => {
|
||||||
|
return {
|
||||||
|
template: html`<w-c>${this.litProperty}</w-c>`,
|
||||||
|
afterRender: () => { /** sync some state */ },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// undefined (conditional slot)
|
||||||
|
'' => () => {
|
||||||
|
if (conditionApplies) {
|
||||||
|
return html`<div>default slot</div>`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Element
|
||||||
|
|
||||||
|
For simple cases, an element can be returned. Use this when no web component is needed.
|
||||||
|
|
||||||
|
### TemplateResult
|
||||||
|
|
||||||
|
Return a TemplateResult when you need web components in your light dom. They will be automatically scoped correctly (to the scoped registry belonging to your shadowRoot)
|
||||||
|
If your template needs to rerender as well, use a `SlotRerenderObject`.
|
||||||
|
|
||||||
|
### SlotRerenderObject
|
||||||
|
|
||||||
|
A `SlotRerenderObject` looks like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
template: TemplateResult;
|
||||||
|
afterRender?: Function;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
It is meant for complex templates that need rerenders. Normally - when rendering into shadow dom via `LitElement.render` - we get rerenders
|
||||||
|
"for free" via [property effects](https://lit.dev/docs/components/properties/#when-properties-change).
|
||||||
|
When we configure `SlotFunctionResult` to return a `SlotRerenderObject`, we get the same behavior for light dom.
|
||||||
|
For this rerendering to work predictably (no focus and other interaction issues), the slot will be created with a wrapper div.
|
||||||
|
|
||||||
|
## Private and public slots
|
||||||
|
|
||||||
|
Some elements provide a property/attribute api with a fallback to content projection as a means to provide more advanced html.
|
||||||
|
For instance, a simple text label is provided like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<my-input label="My label"></my-input>
|
||||||
|
```
|
||||||
|
|
||||||
|
- A more advanced label (using html that can't be provided via a string) can be provided like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<my-input>
|
||||||
|
<label slot="label"><my-icon aria-hidden="true"></my-icon>My label</label>
|
||||||
|
</my-input>
|
||||||
|
```
|
||||||
|
|
||||||
|
- In the property/attribute case, SlotMixin adds the `<label slot="label">` under the hood, **unless** the developer already provided the slot.
|
||||||
|
This will make sure that the slot provided by the user always takes precedence and only one slot instance will be available in light dom per slot.
|
||||||
|
|
||||||
|
### Default slot
|
||||||
|
|
||||||
|
As can be seen in the example below, '' can be used to add content to the default slot
|
||||||
|
|
@ -15,93 +15,12 @@ const isRerenderConfig = (/** @type {SlotFunctionResult} */ o) =>
|
||||||
!Array.isArray(o) && typeof o === 'object' && 'template' in o;
|
!Array.isArray(o) && typeof o === 'object' && 'template' in o;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The SlotMixin is made for solving accessibility challenges that inherently come with the usage of shadow dom.
|
* All intricacies involved in managing light dom can be delegated to SlotMixin. Amongst others, it automatically:
|
||||||
* Until [AOM](https://wicg.github.io/aom/explainer.html) is not in place yet, it is not possible to create relations between different shadow doms.
|
* - mediates between light dom provided by the user ('public slots') and light dom provided by the component author ('private slots').
|
||||||
* The need for this can occur in the following situations:
|
* - allows to hook into the reactive update loop of LitElement (rerendering on property changes)
|
||||||
* 1. a user defined slot
|
* - respects the scoped registry belonging to the shadow root.
|
||||||
* For instance:
|
|
||||||
* `<my-input>
|
|
||||||
* <label slot="label"><></label>
|
|
||||||
* </my-input>`.
|
|
||||||
*
|
*
|
||||||
* The label here needs to be connected to the input element that may live in shadow dom. The input needs to have `aria-labelledby="label-id".`
|
* Be sure to read all details about SlotMixin in the [SlotMixin documentation](docs/fundamentals/systems/core/SlotMixin.md)
|
||||||
*
|
|
||||||
* 2. an interplay of multiple nested web components
|
|
||||||
* For instance:
|
|
||||||
* `<my-fieldset>
|
|
||||||
* <my-input></my-input>
|
|
||||||
* <my-input></my-input>
|
|
||||||
* <div id="msg">Group errror message</div>
|
|
||||||
* </my-fieldset>`
|
|
||||||
* In the case above, all inputs need to be able to refer the error message of their parent
|
|
||||||
* `
|
|
||||||
* In a nutshell: SlotMixin helps you with everything related to rendering light dom (i.e. rendering to slots).
|
|
||||||
* So that you can build accessible ui components with ease, while delegating all edge cases to SlotMixin.
|
|
||||||
* Edge cases that it solves:
|
|
||||||
* - rendering light dom in context of scoped customElementRegistries: we respect the customElementRegistry bound to your ShadowRoot
|
|
||||||
* - the concept of rerendering based on property effects
|
|
||||||
* - easily render lit templates
|
|
||||||
*
|
|
||||||
* So what does the api look like? SlotMixin can be used like this:
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```js
|
|
||||||
* class AccessibleControl extends SlotMixin(LitElement) {
|
|
||||||
* get slots() {
|
|
||||||
* return {
|
|
||||||
* ...super.slots,
|
|
||||||
* 'my-public-slot': () => document.createElement('input'),
|
|
||||||
* '_my-private-slot': () => html`<wc-rendered-to-light-dom></wc-rendered-to-light-dom>`;
|
|
||||||
* '' => () => html`<div>default slot</div>`,
|
|
||||||
* };
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## Private and public slots
|
|
||||||
* Some elements provide a property/attribute api with a fallback to content projection as a means to provide more advanced html.
|
|
||||||
* For instance, a simple text label is provided like this:
|
|
||||||
* `<my-input label="My label"></my-input>`
|
|
||||||
*
|
|
||||||
* A more advanced label can be provided like this:
|
|
||||||
* `<my-input>
|
|
||||||
* <label slot="label"><my-icon aria-hidden="true"></my-icon>My label</label>
|
|
||||||
* </my-input>`
|
|
||||||
*
|
|
||||||
* In the property/attribute case, SlotMixin adds the `<label slot="label">` under the hood. **unless** the developer already provided the slot.
|
|
||||||
* This will make sure that the slot provided by the user always takes precedence and only one slot instance will be available in light dom per slot.
|
|
||||||
*
|
|
||||||
* ### Default slot
|
|
||||||
* As can be seen in the example above, '' can be used to add content to the default slot
|
|
||||||
*
|
|
||||||
* ## SlotFunctionResult
|
|
||||||
*
|
|
||||||
* The `SlotFunctionResult` is the output of the functions provided in `get slots()`. It can output the following types:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* TemplateResult | Element | SlotRerenderObject | undefined;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ### Element
|
|
||||||
* For simple cases, an element can be returned. Use this when no web component is needed.
|
|
||||||
*
|
|
||||||
* ### TemplateResult
|
|
||||||
* Return a TemplateResult when you need web components in your light dom. They will be automatically scoped correctly (to the scoped registry belonging to your shadowRoot)
|
|
||||||
* If your template needs to be rerender, use a `SlotRerenderObject`.
|
|
||||||
*
|
|
||||||
* ### SlotRerenderObject
|
|
||||||
* A `SlotRerenderObject` looks like this:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* template: TemplateResult;
|
|
||||||
* afterRender?: Function;
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
* It is meant for complex templates that need rerenders. Normally, when rendering into shadow dom (behavior we could have when [AOM](https://wicg.github.io/aom/explainer.html) was implemented), we would get rerenders
|
|
||||||
* "for free" when a [property effect](https://lit.dev/docs/components/properties/#when-properties-change) takes place.
|
|
||||||
* When we configure `SlotFunctionResult` to return a `SlotRerenderObject`, we get the same behavior for light dom.
|
|
||||||
* For this rerendering to work predictably (no focus and other interaction issues), the slot will be created with a wrapper div.
|
|
||||||
*
|
*
|
||||||
* @type {SlotMixin}
|
* @type {SlotMixin}
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<LitElement>} superclass
|
* @param {import('@open-wc/dedupe-mixin').Constructor<LitElement>} superclass
|
||||||
|
|
|
||||||
|
|
@ -268,9 +268,6 @@ export class LionInputTelDropdown extends LionInputTel {
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
// if (changedProperties.has('_needsLightDomRender')) {
|
|
||||||
// this.__renderDropdown();
|
|
||||||
// }
|
|
||||||
if (changedProperties.has('activeRegion')) {
|
if (changedProperties.has('activeRegion')) {
|
||||||
this.__syncRegionWithDropdown();
|
this.__syncRegionWithDropdown();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,6 @@ export function runInputTelDropdownSuite({ klass } = { klass: LionInputTelDropdo
|
||||||
|
|
||||||
it('syncs value of dropdown on init if input has no value', async () => {
|
it('syncs value of dropdown on init if input has no value', async () => {
|
||||||
const el = await fixture(html` <${tag}></${tag}> `);
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
// await el.updateComplete;
|
|
||||||
expect(el.activeRegion).to.equal('GB');
|
expect(el.activeRegion).to.equal('GB');
|
||||||
expect(el.value).to.equal('+44');
|
expect(el.value).to.equal('+44');
|
||||||
expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal(
|
expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal(
|
||||||
|
|
@ -92,7 +91,6 @@ export function runInputTelDropdownSuite({ klass } = { klass: LionInputTelDropdo
|
||||||
|
|
||||||
it('syncs value of dropdown on init if input has no value does not influence interaction states', async () => {
|
it('syncs value of dropdown on init if input has no value does not influence interaction states', async () => {
|
||||||
const el = await fixture(html` <${tag}></${tag}> `);
|
const el = await fixture(html` <${tag}></${tag}> `);
|
||||||
// TODO find out why its get dirty again
|
|
||||||
expect(el.dirty).to.be.false;
|
expect(el.dirty).to.be.false;
|
||||||
expect(el.prefilled).to.be.false;
|
expect(el.prefilled).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue