diff --git a/CHANGELOG.md b/CHANGELOG.md index 097cfd51ba..b78413123a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## Unreleased + +* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311)) + ## 3.57.0 * Add `` ([#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)) * 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)) +* Support CSS units in the `fly` and `blur` transitions ([#7623](https://github.com/sveltejs/svelte/pull/7623)) ## 3.56.0 diff --git a/elements/index.d.ts b/elements/index.d.ts index 4ad2f2880f..7595d767bf 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -1586,6 +1586,7 @@ export interface SvelteHTMLElements { // Svelte specific 'svelte:window': SvelteWindowAttributes; + 'svelte:document': HTMLAttributes; 'svelte:body': HTMLAttributes; 'svelte:fragment': { slot?: string }; 'svelte:options': { [name: string]: any }; diff --git a/site/content/docs/06-accessibility-warnings.md b/site/content/docs/06-accessibility-warnings.md index 9babea95ae..934cf638e6 100644 --- a/site/content/docs/06-accessibility-warnings.md +++ b/site/content/docs/06-accessibility-warnings.md @@ -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-label-has-associated-control` Enforce that a label tag has a text label and an associated control. diff --git a/site/content/tutorial/16-special-elements/06-svelte-document/app-a/App.svelte b/site/content/tutorial/16-special-elements/06-svelte-document/app-a/App.svelte index cea052ec88..09cc596e8c 100644 --- a/site/content/tutorial/16-special-elements/06-svelte-document/app-a/App.svelte +++ b/site/content/tutorial/16-special-elements/06-svelte-document/app-a/App.svelte @@ -4,7 +4,7 @@ const handleSelectionChange = (e) => selection = document.getSelection(); - +

Select this text to fire events

Selection: {selection}

diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index d41348779a..86ee6f4927 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -170,6 +170,10 @@ export default { code: 'a11y-img-redundant-alt', message: 'A11y: Screenreaders already announce 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: { code: 'a11y-label-has-associated-control', message: 'A11y: A form label must be associated with a control.' diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index f105da83f8..513ad4ba18 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -25,7 +25,7 @@ import { Literal } from 'estree'; import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; 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_attribute_set = new Set(aria_attributes); @@ -76,6 +76,33 @@ const a11y_labelable = 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', 'mousedown', '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 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)); @@ -711,7 +753,7 @@ export default class Element extends Node { is_non_interactive_roles(role_static_value)) || (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) { component.warn(this, compiler_warnings.a11y_no_noninteractive_element_interactions(this.name)); } diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts index d0564b419e..4409f80262 100644 --- a/src/compiler/compile/utils/a11y.ts +++ b/src/compiler/compile/utils/a11y.ts @@ -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'; } +export function has_disabled_attribute(attribute_map: Map) { + 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[] = []; elementRoles.entries().forEach(([schema, roles]) => { diff --git a/test/validator/samples/a11y-interactive-supports-focus/input.svelte b/test/validator/samples/a11y-interactive-supports-focus/input.svelte new file mode 100644 index 0000000000..9caa9ccfaf --- /dev/null +++ b/test/validator/samples/a11y-interactive-supports-focus/input.svelte @@ -0,0 +1,32 @@ + +
{}} /> +
{}} /> +
{}} /> +
{}} /> +