Merge branch 'master' into a11y-no-noninteractive-element-interactions

pull/8391/head
Nguyen Tran 4 years ago
commit 546ebbbe76

@ -1,5 +1,9 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))
## 3.57.0 ## 3.57.0
* Add `<svelte:document>` ([#3310](https://github.com/sveltejs/svelte/issues/3310)) * Add `<svelte:document>` ([#3310](https://github.com/sveltejs/svelte/issues/3310))
@ -11,6 +15,7 @@
* Prevent derived store callbacks after store is unsubscribed from ([#8364](https://github.com/sveltejs/svelte/issues/8364)) * Prevent derived store callbacks after store is unsubscribed from ([#8364](https://github.com/sveltejs/svelte/issues/8364))
* Account for `bind:group` members being spread across multiple control flow blocks ([#8372](https://github.com/sveltejs/svelte/issues/8372)) * Account for `bind:group` members being spread across multiple control flow blocks ([#8372](https://github.com/sveltejs/svelte/issues/8372))
* Revert buggy reactive statement optimization ([#8374](https://github.com/sveltejs/svelte/issues/8374)) * Revert buggy reactive statement optimization ([#8374](https://github.com/sveltejs/svelte/issues/8374))
* Support CSS units in the `fly` and `blur` transitions ([#7623](https://github.com/sveltejs/svelte/pull/7623))
## 3.56.0 ## 3.56.0

@ -1586,6 +1586,7 @@ export interface SvelteHTMLElements {
// Svelte specific // Svelte specific
'svelte:window': SvelteWindowAttributes; 'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:body': HTMLAttributes<HTMLElement>; 'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string }; 'svelte:fragment': { slot?: string };
'svelte:options': { [name: string]: any }; 'svelte:options': { [name: string]: any };

@ -137,6 +137,17 @@ Enforce that attributes important for accessibility have a valid value. For exam
--- ---
### `a11y-interactive-supports-focus`
Enforce that elements with an interactive role and interactive handlers (mouse or key press) must be focusable or tabbable.
```sv
<!-- A11y: Elements with the 'button' interactive role must have a tabindex value. -->
<div role="button" on:keypress={() => {}} />
```
---
### `a11y-label-has-associated-control` ### `a11y-label-has-associated-control`
Enforce that a label tag has a text label and an associated control. Enforce that a label tag has a text label and an associated control.

@ -4,7 +4,7 @@
const handleSelectionChange = (e) => selection = document.getSelection(); const handleSelectionChange = (e) => selection = document.getSelection();
</script> </script>
<svelte:body /> <svelte:document />
<p>Select this text to fire events</p> <p>Select this text to fire events</p>
<p>Selection: {selection}</p> <p>Selection: {selection}</p>

@ -170,6 +170,10 @@ export default {
code: 'a11y-img-redundant-alt', code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.' message: 'A11y: Screenreaders already announce <img> elements as an image.'
}, },
a11y_interactive_supports_focus: (role: string) => ({
code: 'a11y-interactive-supports-focus',
message: `A11y: Elements with the '${role}' interactive role must have a tabindex value.`
}),
a11y_label_has_associated_control: { a11y_label_has_associated_control: {
code: 'a11y-label-has-associated-control', code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.' message: 'A11y: A form label must be associated with a control.'

@ -25,7 +25,7 @@ import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { ARIARoleDefinitionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role } from '../utils/a11y'; import { is_interactive_element, is_non_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element, is_abstract_role, is_static_element, has_disabled_attribute } from '../utils/a11y';
const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const aria_attribute_set = new Set(aria_attributes); const aria_attribute_set = new Set(aria_attributes);
@ -76,6 +76,33 @@ const a11y_labelable = new Set([
]); ]);
const a11y_interactive_handlers = new Set([ const a11y_interactive_handlers = new Set([
// Keyboard events
'keypress',
'keydown',
'keyup',
// Click events
'click',
'contextmenu',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragexit',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup'
]);
const a11y_recommended_interactive_handlers = new Set([
'click', 'click',
'mousedown', 'mousedown',
'mouseup', 'mouseup',
@ -612,6 +639,21 @@ export default class Element extends Node {
} }
} }
// interactive-supports-focus
if (
!has_disabled_attribute(attribute_map) &&
!is_hidden_from_screen_reader(this.name, attribute_map) &&
!is_presentation_role(current_role) &&
is_interactive_roles(current_role) &&
is_static_element(this.name, attribute_map) &&
!attribute_map.get('tabindex')
) {
const has_interactive_handlers = handlers.some((handler) => a11y_interactive_handlers.has(handler.name));
if (has_interactive_handlers) {
component.warn(this, compiler_warnings.a11y_interactive_supports_focus(current_role));
}
}
// no-interactive-element-to-noninteractive-role // no-interactive-element-to-noninteractive-role
if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) { if (is_interactive_element(this.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role))) {
component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name)); component.warn(this, compiler_warnings.a11y_no_interactive_element_to_noninteractive_role(current_role, this.name));
@ -711,7 +753,7 @@ export default class Element extends Node {
is_non_interactive_roles(role_static_value)) || is_non_interactive_roles(role_static_value)) ||
(is_non_interactive_element(this.name, attribute_map) && !role)) (is_non_interactive_element(this.name, attribute_map) && !role))
) { ) {
const has_interactive_handlers = handlers.some((handler) => a11y_interactive_handlers.has(handler.name)); const has_interactive_handlers = handlers.some((handler) => a11y_recommended_interactive_handlers.has(handler.name));
if (has_interactive_handlers) { if (has_interactive_handlers) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_element_interactions(this.name)); component.warn(this, compiler_warnings.a11y_no_noninteractive_element_interactions(this.name));
} }

@ -68,6 +68,24 @@ export function is_hidden_from_screen_reader(tag_name: string, attribute_map: Ma
return aria_hidden_value === true || aria_hidden_value === 'true'; return aria_hidden_value === true || aria_hidden_value === 'true';
} }
export function has_disabled_attribute(attribute_map: Map<string, Attribute>) {
const disabled_attr = attribute_map.get('disabled');
const disabled_attr_value = disabled_attr && disabled_attr.get_static_value();
if (disabled_attr_value) {
return true;
}
const aria_disabled_attr = attribute_map.get('aria-disabled');
if (aria_disabled_attr) {
const aria_disabled_attr_value = aria_disabled_attr.get_static_value();
if (aria_disabled_attr_value === true) {
return true;
}
}
return false;
}
const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = []; const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = [];
elementRoles.entries().forEach(([schema, roles]) => { elementRoles.entries().forEach(([schema, roles]) => {

@ -0,0 +1,32 @@
<!-- VALID -->
<div aria-hidden role="button" on:keypress={() => {}} />
<div aria-disabled role="button" on:keypress={() => {}} />
<div disabled role="button" on:keypress={() => {}} />
<div role="presentation" on:keypress={() => {}} />
<button on:click={() => {}} />
<div role="menuitem" tabindex="0" on:click={() => {}} on:keypress={() => {}} />
<div role="button" tabindex="-1" on:click={() => {}} on:keypress={() => {}} />
<!-- INVALID -->
<div role="button" on:keypress={() => {}} />
<span role="menuitem" on:keydown={() => {}} />
<div role="button" on:keyup={() => {}} />
<span role="menuitem" on:click={() => {}} on:keypress={() => {}} />
<div role="button" on:contextmenu={() => {}} />
<span role="menuitem" on:dblclick={() => {}} />
<div role="button" on:drag={() => {}} />
<span role="menuitem" on:dragend={() => {}} />
<div role="button" on:dragenter={() => {}} />
<span role="menuitem" on:dragexit={() => {}} />
<div role="button" on:dragleave={() => {}} />
<span role="menuitem" on:dragover={() => {}} />
<div role="button" on:dragstart={() => {}} />
<span role="menuitem" on:drop={() => {}} />
<div role="button" on:mousedown={() => {}} />
<span role="menuitem" on:mouseenter={() => {}} />
<div role="button" on:mouseleave={() => {}} />
<span role="menuitem" on:mousemove={() => {}} />
<div role="button" on:mouseout={() => {}} />
<span role="menuitem" on:mouseover={() => {}} />
<div role="button" on:mouseup={() => {}} />

@ -0,0 +1,278 @@
[
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 44,
"line": 11
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 11
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 46,
"line": 12
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 12
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 41,
"line": 13
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 13
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 67,
"line": 14
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 14
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 47,
"line": 15
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 15
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 47,
"line": 16
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 16
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 40,
"line": 17
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 17
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 46,
"line": 18
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 18
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 45,
"line": 19
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 19
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 47,
"line": 20
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 20
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 45,
"line": 21
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 21
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 47,
"line": 22
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 22
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 45,
"line": 23
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 23
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 43,
"line": 24
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 24
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 45,
"line": 25
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 25
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 49,
"line": 26
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 26
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 46,
"line": 27
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 27
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 48,
"line": 28
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 28
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 44,
"line": 29
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 29
}
},
{
"code": "a11y-mouse-events-have-key-events",
"end": {
"column": 44,
"line": 29
},
"message": "A11y: on:mouseout must be accompanied by on:blur",
"start": {
"column": 0,
"line": 29
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 48,
"line": 30
},
"message": "A11y: Elements with the 'menuitem' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 30
}
},
{
"code": "a11y-mouse-events-have-key-events",
"end": {
"column": 48,
"line": 30
},
"message": "A11y: on:mouseover must be accompanied by on:focus",
"start": {
"column": 0,
"line": 30
}
},
{
"code": "a11y-interactive-supports-focus",
"end": {
"column": 43,
"line": 31
},
"message": "A11y: Elements with the 'button' interactive role must have a tabindex value.",
"start": {
"column": 0,
"line": 31
}
}
]
Loading…
Cancel
Save