diff --git a/CHANGELOG.md b/CHANGELOG.md index b78413123a..e886996a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## Unreleased * Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311)) +* Relax `a11y-no-noninteractive-element-to-interactive-role` warning ([#8402](https://github.com/sveltejs/svelte/pull/8402)) +* Add `a11y-interactive-supports-focus` warning ([#8392](https://github.com/sveltejs/svelte/pull/8392)) +* Fix equality check when updating dynamic text ([#5931](https://github.com/sveltejs/svelte/issues/5931)) ## 3.57.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 bc793d80e7..0d025d797d 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 710377374f..a851bc24c2 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -166,6 +166,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 2fd91ac495..1678ea1caa 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); @@ -75,6 +75,33 @@ const a11y_labelable = new Set([ 'textarea' ]); +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_nested_implicit_semantics = new Map([ ['header', 'banner'], ['footer', 'contentinfo'] @@ -145,6 +172,35 @@ const input_type_to_implicit_role = new Map([ ['url', 'textbox'] ]); +/** + * Exceptions to the rule which follows common A11y conventions + * TODO make this configurable by the user + */ +const a11y_non_interactive_element_to_interactive_role_exceptions = { + ul: [ + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'tablist', + 'tree', + 'treegrid' + ], + ol: [ + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'tablist', + 'tree', + 'treegrid' + ], + li: ['menuitem', 'option', 'row', 'tab', 'treeitem'], + table: ['grid'], + td: ['gridcell'], + fieldset: ['radiogroup', 'presentation'] +}; + const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']); function input_implicit_role(attribute_map: Map) { @@ -603,13 +659,28 @@ 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)); } // no-noninteractive-element-to-interactive-role - if (is_non_interactive_element(this.name, attribute_map) && is_interactive_roles(current_role)) { + if (is_non_interactive_element(this.name, attribute_map) && is_interactive_roles(current_role) && !a11y_non_interactive_element_to_interactive_role_exceptions[this.name]?.includes(current_role)) { component.warn(this, compiler_warnings.a11y_no_noninteractive_element_to_interactive_role(current_role, this.name)); } }); diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 1f80cee89c..7a0bdbaa06 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -174,6 +174,8 @@ export default class ElementWrapper extends Wrapper { child_dynamic_element_block?: Block = null; child_dynamic_element?: ElementWrapper = null; + element_data_name = null; + constructor( renderer: Renderer, block: Block, @@ -287,6 +289,8 @@ export default class ElementWrapper extends Wrapper { } this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); + + this.element_data_name = block.get_unique_name(`${this.var.name}_data`); } render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { @@ -516,7 +520,8 @@ export default class ElementWrapper extends Wrapper { child.render( block, is_template ? x`${node}.content` : node, - nodes + nodes, + { element_data_name: this.element_data_name } ); }); } @@ -824,7 +829,6 @@ export default class ElementWrapper extends Wrapper { add_spread_attributes(block: Block) { const levels = block.get_unique_name(`${this.var.name}_levels`); - const data = block.get_unique_name(`${this.var.name}_data`); const initial_props = []; const updates = []; @@ -855,9 +859,9 @@ export default class ElementWrapper extends Wrapper { block.chunks.init.push(b` let ${levels} = [${initial_props}]; - let ${data} = {}; + let ${this.element_data_name} = {}; for (let #i = 0; #i < ${levels}.length; #i += 1) { - ${data} = @assign(${data}, ${levels}[#i]); + ${this.element_data_name} = @assign(${this.element_data_name}, ${levels}[#i]); } `); @@ -869,12 +873,12 @@ export default class ElementWrapper extends Wrapper { : x`@set_attributes`; block.chunks.hydrate.push( - b`${fn}(${this.var}, ${data});` + b`${fn}(${this.var}, ${this.element_data_name});` ); if (this.has_dynamic_attribute) { block.chunks.update.push(b` - ${fn}(${this.var}, ${data} = @get_spread_update(${levels}, [ + ${fn}(${this.var}, ${this.element_data_name} = @get_spread_update(${levels}, [ ${updates} ])); `); @@ -890,23 +894,23 @@ export default class ElementWrapper extends Wrapper { } block.chunks.mount.push(b` - 'value' in ${data} && (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); + 'value' in ${this.element_data_name} && (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value); `); block.chunks.update.push(b` - if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${data}) (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); + if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${this.element_data_name}) (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value); `); } else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) { const type = this.node.get_static_attribute_value('type'); if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') { block.chunks.mount.push(b` - if ('value' in ${data}) { - ${this.var}.value = ${data}.value; + if ('value' in ${this.element_data_name}) { + ${this.var}.value = ${this.element_data_name}.value; } `); block.chunks.update.push(b` - if ('value' in ${data}) { - ${this.var}.value = ${data}.value; + if ('value' in ${this.element_data_name}) { + ${this.var}.value = ${this.element_data_name}.value; } `); } @@ -1220,8 +1224,8 @@ export default class ElementWrapper extends Wrapper { if (this.dynamic_style_dependencies.size > 0) { maybe_create_style_changed_var(); // If all dependencies are same as the style attribute dependencies, then we can skip the dirty check - condition = - all_deps.size === this.dynamic_style_dependencies.size + condition = + all_deps.size === this.dynamic_style_dependencies.size ? style_changed_var : x`${style_changed_var} || ${condition}`; } @@ -1232,7 +1236,6 @@ export default class ElementWrapper extends Wrapper { } `); } - }); } diff --git a/src/compiler/compile/render_dom/wrappers/Fragment.ts b/src/compiler/compile/render_dom/wrappers/Fragment.ts index 8c633aeb71..463c8db24a 100644 --- a/src/compiler/compile/render_dom/wrappers/Fragment.ts +++ b/src/compiler/compile/render_dom/wrappers/Fragment.ts @@ -4,7 +4,7 @@ import Body from './Body'; import DebugTag from './DebugTag'; import Document from './Document'; import EachBlock from './EachBlock'; -import Element from './Element/index'; +import Element from './Element'; import Head from './Head'; import IfBlock from './IfBlock'; import KeyBlock from './KeyBlock'; diff --git a/src/compiler/compile/render_dom/wrappers/MustacheTag.ts b/src/compiler/compile/render_dom/wrappers/MustacheTag.ts index ea6634ec80..0bfa0198ae 100644 --- a/src/compiler/compile/render_dom/wrappers/MustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/MustacheTag.ts @@ -5,7 +5,9 @@ import Wrapper from './shared/Wrapper'; import MustacheTag from '../../nodes/MustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag'; import { x } from 'code-red'; -import { Identifier } from 'estree'; +import { Identifier, Expression } from 'estree'; +import ElementWrapper from './Element'; +import AttributeWrapper from './Element/Attribute'; export default class MustacheTagWrapper extends Tag { var: Identifier = { type: 'Identifier', name: 't' }; @@ -14,10 +16,40 @@ export default class MustacheTagWrapper extends Tag { super(renderer, block, parent, node); } - render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { + render(block: Block, parent_node: Identifier, parent_nodes: Identifier, data: Record | undefined) { + const contenteditable_attributes = + this.parent instanceof ElementWrapper && + this.parent.attributes.filter((a) => a.node.name === 'contenteditable'); + + const spread_attributes = + this.parent instanceof ElementWrapper && + this.parent.attributes.filter((a) => a.node.is_spread); + + let contenteditable_attr_value: Expression | true | undefined = undefined; + if (contenteditable_attributes.length > 0) { + const attribute = contenteditable_attributes[0] as AttributeWrapper; + if ([true, 'true', ''].includes(attribute.node.get_static_value())) { + contenteditable_attr_value = true; + } else { + contenteditable_attr_value = x`${attribute.get_value(block)}`; + } + } else if (spread_attributes.length > 0 && data.element_data_name) { + contenteditable_attr_value = x`${data.element_data_name}['contenteditable']`; + } + const { init } = this.rename_this_method( block, - value => x`@set_data(${this.var}, ${value})` + value => { + if (contenteditable_attr_value) { + if (contenteditable_attr_value === true) { + return x`@set_data_contenteditable(${this.var}, ${value})`; + } else { + return x`@set_data_maybe_contenteditable(${this.var}, ${value}, ${contenteditable_attr_value})`; + } + } else { + return x`@set_data(${this.var}, ${value})`; + } + } ); block.add_element( diff --git a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts index 4bf8c20bd8..53847d8870 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts @@ -85,7 +85,7 @@ export default class Wrapper { ); } - render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { + render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier, _data: Record = undefined) { throw Error('Wrapper class is not renderable'); } } 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/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index be81a11514..cd47138fef 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -1,6 +1,7 @@ import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr } from './dom'; import { SvelteComponent } from './Component'; import { is_void } from '../../shared/utils/names'; +import { contenteditable_truthy_values } from './utils'; export function dispatch_dev(type: string, detail?: T) { document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true })); @@ -83,12 +84,26 @@ export function dataset_dev(node: HTMLElement, property: string, value?: any) { dispatch_dev('SvelteDOMSetDataset', { node, property, value }); } -export function set_data_dev(text, data) { +export function set_data_dev(text: Text, data: unknown) { data = '' + data; - if (text.wholeText === data) return; + if (text.data === data) return; + dispatch_dev('SvelteDOMSetData', { node: text, data }); + text.data = (data as string); +} +export function set_data_contenteditable_dev(text: Text, data: unknown) { + data = '' + data; + if (text.wholeText === data) return; dispatch_dev('SvelteDOMSetData', { node: text, data }); - text.data = data; + text.data = (data as string); +} + +export function set_data_maybe_contenteditable_dev(text: Text, data: unknown, attr_value: string) { + if (~contenteditable_truthy_values.indexOf(attr_value)) { + set_data_contenteditable_dev(text, data); + } else { + set_data_dev(text, data); + } } export function validate_each_argument(arg) { diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 1f786d51b5..a746613170 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,4 +1,4 @@ -import { has_prop } from './utils'; +import { contenteditable_truthy_values, has_prop } from './utils'; // Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM // at the end of hydration without touching the remaining nodes. @@ -581,9 +581,24 @@ export function claim_html_tag(nodes, is_svg: boolean) { return new HtmlTagHydration(claimed_nodes, is_svg); } -export function set_data(text, data) { +export function set_data(text: Text, data: unknown) { data = '' + data; - if (text.wholeText !== data) text.data = data; + if (text.data === data) return; + text.data = (data as string); +} + +export function set_data_contenteditable(text: Text, data: unknown) { + data = '' + data; + if (text.wholeText === data) return; + text.data = (data as string); +} + +export function set_data_maybe_contenteditable(text: Text, data: unknown, attr_value: string) { + if (~contenteditable_truthy_values.indexOf(attr_value)) { + set_data_contenteditable(text, data); + } else { + set_data(text, data); + } } export function set_input_value(input, value) { diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index b1c27a355d..192f40140e 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -194,3 +194,5 @@ export function split_css_unit(value: number | string): [number, string] { const split = typeof value === 'string' && value.match(/^\s*(-?[\d.]+)([^\s]*)\s*$/); return split ? [parseFloat(split[1]), split[2] || 'px'] : [value as number, 'px']; } + +export const contenteditable_truthy_values = ['', true, 1, 'true', 'contenteditable']; diff --git a/test/runtime-puppeteer/samples/component-event-handler-contenteditable-false/_config.js b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-false/_config.js new file mode 100644 index 0000000000..3763360fd8 --- /dev/null +++ b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-false/_config.js @@ -0,0 +1,13 @@ +// A puppeteer test because JSDOM doesn't support contenteditable +export default { + html: '
', + + async test({ assert, target, component, window }) { + const div = target.querySelector('div'); + const text = window.document.createTextNode('a'); + div.insertBefore(text, null); + assert.equal(div.textContent, 'a'); + component.text = 'bcde'; + assert.equal(div.textContent, 'bcdea'); + } +}; diff --git a/test/runtime-puppeteer/samples/component-event-handler-contenteditable-false/main.svelte b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-false/main.svelte new file mode 100644 index 0000000000..9555925487 --- /dev/null +++ b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-false/main.svelte @@ -0,0 +1,6 @@ + + +
{text}
diff --git a/test/runtime-puppeteer/samples/component-event-handler-contenteditable-spread/_config.js b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-spread/_config.js new file mode 100644 index 0000000000..de65bd6627 --- /dev/null +++ b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-spread/_config.js @@ -0,0 +1,24 @@ +// A puppeteer test because JSDOM doesn't support contenteditable +export default { + html: '
', + ssrHtml: '
', + + async test({ assert, target, window }) { + // this tests that by going from contenteditable=true to false, the + // content is correctly updated before that. This relies on the order + // of the updates: first updating the content, then setting contenteditable + // to false, which means that `set_data_maybe_contenteditable` is used and not `set_data`. + // If the order is reversed, https://github.com/sveltejs/svelte/issues/5018 + // would be happening. The caveat is that if we go from contenteditable=false to true + // then we will have the same issue. To fix this reliably we probably need to + // overhaul the way we handle text updates in general. + // If due to some refactoring this test fails, it's probably fine to ignore it since + // this is a very specific edge case and the behavior is unstable anyway. + const div = target.querySelector('div'); + const text = window.document.createTextNode('a'); + div.insertBefore(text, null); + const event = new window.InputEvent('input'); + await div.dispatchEvent(event); + assert.equal(div.textContent, 'a'); + } +}; diff --git a/test/runtime-puppeteer/samples/component-event-handler-contenteditable-spread/main.svelte b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-spread/main.svelte new file mode 100644 index 0000000000..ed301e8d20 --- /dev/null +++ b/test/runtime-puppeteer/samples/component-event-handler-contenteditable-spread/main.svelte @@ -0,0 +1,11 @@ + + +
{text}
diff --git a/test/runtime-puppeteer/samples/component-event-handler-contenteditable/_config.js b/test/runtime-puppeteer/samples/component-event-handler-contenteditable/_config.js new file mode 100644 index 0000000000..1b4b3b0271 --- /dev/null +++ b/test/runtime-puppeteer/samples/component-event-handler-contenteditable/_config.js @@ -0,0 +1,33 @@ +// A puppeteer test because JSDOM doesn't support contenteditable +export default { + html: '
', + + // Failing test for https://github.com/sveltejs/svelte/issues/5018, fix pending + // It's hard to fix this because in order to do that, we would need to change the + // way the value is compared completely. Right now it compares the value of the + // first text node, but it should compare the value of the whole content + skip: true, + + async test({ assert, target, window }) { + const div = target.querySelector('div'); + + let text = window.document.createTextNode('a'); + div.insertBefore(text, null); + let event = new window.InputEvent('input'); + await div.dispatchEvent(event); + assert.equal(div.textContent, 'a'); + + // When a user types a newline, the browser inserts a
element + const inner_div = window.document.createElement('div'); + div.insertBefore(inner_div, null); + event = new window.InputEvent('input'); + await div.dispatchEvent(event); + assert.equal(div.textContent, 'a'); + + text = window.document.createTextNode('b'); + inner_div.insertBefore(text, null); + event = new window.InputEvent('input'); + await div.dispatchEvent(event); + assert.equal(div.textContent, 'ab'); + } +}; diff --git a/test/runtime/samples/component-event-handler-contenteditable/main.svelte b/test/runtime-puppeteer/samples/component-event-handler-contenteditable/main.svelte similarity index 100% rename from test/runtime/samples/component-event-handler-contenteditable/main.svelte rename to test/runtime-puppeteer/samples/component-event-handler-contenteditable/main.svelte diff --git a/test/runtime/samples/component-event-handler-contenteditable/_config.js b/test/runtime/samples/component-event-handler-contenteditable/_config.js deleted file mode 100644 index 1628e22d01..0000000000 --- a/test/runtime/samples/component-event-handler-contenteditable/_config.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - html: ` -
- `, - - async test({ assert, target, window }) { - const div = target.querySelector('div'); - const text = window.document.createTextNode('a'); - div.insertBefore(text, null); - const event = new window.InputEvent('input'); - await div.dispatchEvent(event); - - assert.equal(div.textContent, 'a'); - } -}; diff --git a/test/runtime/samples/reactive-values-text-node/_config.js b/test/runtime/samples/reactive-values-text-node/_config.js new file mode 100644 index 0000000000..fb859285dd --- /dev/null +++ b/test/runtime/samples/reactive-values-text-node/_config.js @@ -0,0 +1,9 @@ +export default { + html:'
same text
', + async test({ assert, target }) { + await new Promise(f => setTimeout(f, 10)); + assert.htmlEqual(target.innerHTML, ` +
same text text
+ `); + } +}; diff --git a/test/runtime/samples/reactive-values-text-node/main.svelte b/test/runtime/samples/reactive-values-text-node/main.svelte new file mode 100644 index 0000000000..0982622b1a --- /dev/null +++ b/test/runtime/samples/reactive-values-text-node/main.svelte @@ -0,0 +1,8 @@ + + +
{text} text
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 @@ + +
{}} /> +
{}} /> +
{}} /> +
{}} /> +