Merge branch 'master' into master

pull/8022/head
Cymaera 3 years ago committed by GitHub
commit 86470b05cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,17 @@
# Svelte changelog
## 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))
* Make `preserveComments` effective in DOM renderer ([#7182](https://github.com/sveltejs/svelte/pull/7182))
* Add CSS container queries support ([#6969](https://github.com/sveltejs/svelte/issues/6969))
* Properly handle microdata attributes ([#8414](https://github.com/sveltejs/svelte/pull/8414))
* Prevent name collision when using computed destructuring variables ([#8417](https://github.com/sveltejs/svelte/issues/8417))
* Allow use of `document` for `target` in typings ([#7554](https://github.com/sveltejs/svelte/pull/7554))
## 3.57.0
* Add `<svelte:document>` ([#3310](https://github.com/sveltejs/svelte/issues/3310))
@ -11,6 +23,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

@ -1591,6 +1591,7 @@ export interface SvelteHTMLElements {
// Svelte specific
'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string };
'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`
Enforce that a label tag has a text label and an associated control.

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

@ -166,6 +166,10 @@ export default {
code: 'a11y-img-redundant-alt',
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: {
code: 'a11y-label-has-associated-control',
message: 'A11y: A form label must be associated with a control.'

@ -173,7 +173,7 @@ class Atrule {
}
apply(node: Element) {
if (this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
if (this.node.name === 'container' || this.node.name === 'media' || this.node.name === 'supports' || this.node.name === 'layer') {
this.children.forEach(child => {
child.apply(node);
});

@ -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<string, Attribute>) {
@ -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));
}
});

@ -11,7 +11,7 @@ export type Context = DestructuredVariable | ComputedProperty;
interface ComputedProperty {
type: 'ComputedProperty';
property_name: string;
property_name: Identifier;
key: Expression | PrivateIdentifier;
}
@ -30,8 +30,7 @@ export function unpack_destructuring({
default_modifier = (node) => node,
scope,
component,
context_rest_properties,
number_of_computed_props = { n: 0 }
context_rest_properties
}: {
contexts: Context[];
node: Node;
@ -40,10 +39,6 @@ export function unpack_destructuring({
scope: TemplateScope;
component: Component;
context_rest_properties: Map<string, Node>;
// we want to pass this by reference, as a sort of global variable, because
// if we pass this by value, we could get computed_property_# variable collisions
// when we deal with nested object destructuring
number_of_computed_props?: { n: number };
}) {
if (!node) return;
@ -72,8 +67,7 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties,
number_of_computed_props
context_rest_properties
});
context_rest_properties.set((element.argument as Identifier).name, element);
} else if (element && element.type === 'AssignmentPattern') {
@ -93,8 +87,7 @@ export function unpack_destructuring({
)}` as Node,
scope,
component,
context_rest_properties,
number_of_computed_props
context_rest_properties
});
} else {
unpack_destructuring({
@ -104,8 +97,7 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties,
number_of_computed_props
context_rest_properties
});
}
});
@ -124,8 +116,7 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties,
number_of_computed_props
context_rest_properties
});
context_rest_properties.set((property.argument as Identifier).name, property);
} else if (property.type === 'Property') {
@ -136,8 +127,7 @@ export function unpack_destructuring({
if (property.computed) {
// e.g { [computedProperty]: ... }
const property_name = `computed_property_${number_of_computed_props.n}`;
number_of_computed_props.n += 1;
const property_name = component.get_unique_name('computed_property');
contexts.push({
type: 'ComputedProperty',
@ -178,8 +168,7 @@ export function unpack_destructuring({
)}` as Node,
scope,
component,
context_rest_properties,
number_of_computed_props
context_rest_properties
});
} else {
// e.g. { property } or { property: newName }
@ -190,8 +179,7 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties,
number_of_computed_props
context_rest_properties
});
}
}

@ -0,0 +1,41 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Comment from '../../nodes/Comment';
import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import { Identifier } from 'estree';
export default class CommentWrapper extends Wrapper {
node: Comment;
var: Identifier;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: Comment
) {
super(renderer, block, parent, node);
this.var = x`c` as Identifier;
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (!this.renderer.options.preserveComments) return;
const string_literal = {
type: 'Literal',
value: this.node.data,
loc: {
start: this.renderer.locate(this.node.start),
end: this.renderer.locate(this.node.end)
}
};
block.add_element(
this.var,
x`@comment(${string_literal})`,
parent_nodes && x`@claim_comment(${parent_nodes}, ${string_literal})`,
parent_node
);
}
}

@ -364,7 +364,6 @@ const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & { [k
indeterminate: { applies_to: ['input'] },
inert: {},
ismap: { property_name: 'isMap', applies_to: ['img'] },
itemscope: {},
loop: { applies_to: ['audio', 'bgsound', 'video'] },
multiple: { applies_to: ['input', 'select'] },
muted: { applies_to: ['audio', 'video'] },

@ -192,6 +192,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,
@ -305,6 +307,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) {
@ -534,7 +538,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 }
);
});
}
@ -848,7 +853,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 = [];
@ -879,9 +883,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]);
}
`);
@ -893,12 +897,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}
]));
`);
@ -914,23 +918,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;
}
`);
}
@ -1244,8 +1248,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}`;
}
@ -1256,7 +1260,6 @@ export default class ElementWrapper extends Wrapper {
}
`);
}
});
}

@ -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';
@ -14,6 +14,7 @@ import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import Text from './Text';
import Comment from './Comment';
import Title from './Title';
import Window from './Window';
import { INode } from '../../nodes/interfaces';
@ -27,7 +28,7 @@ import { regex_starts_with_whitespace } from '../../../utils/patterns';
const wrappers = {
AwaitBlock,
Body,
Comment: null,
Comment,
DebugTag,
Document,
EachBlock,
@ -118,7 +119,7 @@ export default class FragmentWrapper {
link(last_child, last_child = wrapper);
} else {
const Wrapper = wrappers[child.type];
if (!Wrapper) continue;
if (!Wrapper || (child.type === 'Comment' && !renderer.options.preserveComments)) continue;
const wrapper = new Wrapper(renderer, block, parent, child, strip_whitespace, last_child || next_sibling);
this.nodes.unshift(wrapper);

@ -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<string, unknown> | 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(

@ -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<string, any> = undefined) {
throw Error('Wrapper class is not renderable');
}
}

@ -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<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[] = [];
elementRoles.entries().forEach(([schema, roles]) => {

@ -0,0 +1,43 @@
// @ts-nocheck
// Note: Must import from the `css-tree` browser bundled distribution due to `createRequire` usage if importing from
// `css-tree` Node module directly. This allows the production build of Svelte to work correctly.
import { fork } from '../../../../../node_modules/css-tree/dist/csstree.esm.js';
import * as Comparison from './node/comparison';
import * as ContainerFeature from './node/container_feature';
import * as ContainerFeatureRange from './node/container_feature_range';
import * as ContainerFeatureStyle from './node/container_feature_style';
import * as ContainerQuery from './node/container_query';
import * as QueryCSSFunction from './node/query_css_function';
/**
* Extends `css-tree` for container query support by forking and adding new nodes and at-rule support for `@container`.
*
* The new nodes are located in `./node`.
*/
const cqSyntax = fork({
atrule: { // extend or override at-rule dictionary
container: {
parse: {
prelude() {
return this.createSingleNodeList(
this.ContainerQuery()
);
},
block(isStyleBlock = false) {
return this.Block(isStyleBlock);
}
}
}
},
node: { // extend node types
Comparison,
ContainerFeature,
ContainerFeatureRange,
ContainerFeatureStyle,
ContainerQuery,
QueryCSSFunction
}
});
export const parse = cqSyntax.parse;

@ -0,0 +1,48 @@
// @ts-nocheck
import { Delim } from 'css-tree/tokenizer';
export const name = 'Comparison';
export const structure = {
value: String
};
export function parse() {
const start = this.tokenStart;
const char1 = this.consume(Delim);
// The first character in the comparison operator must match '<', '=', or '>'.
if (char1 !== '<' && char1 !== '>' && char1 !== '=') {
this.error('Malformed comparison operator');
}
let char2;
if (this.tokenType === Delim) {
char2 = this.consume(Delim);
// The second character in the comparison operator must match '='.
if (char2 !== '=') {
this.error('Malformed comparison operator');
}
}
// If the next token is also 'Delim' then it is malformed.
if (this.tokenType === Delim) {
this.error('Malformed comparison operator');
}
const value = char2 ? `${char1}${char2}` : char1;
return {
type: 'Comparison',
loc: this.getLocation(start, this.tokenStart),
value
};
}
export function generate(node) {
for (let index = 0; index < node.value.length; index++) {
this.token(Delim, node.value.charAt(index));
}
}

@ -0,0 +1,82 @@
// @ts-nocheck
import {
Ident,
Number,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';
export const name = 'ContainerFeature';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
export function parse() {
const start = this.tokenStart;
let value = null;
this.eat(LeftParenthesis);
this.skipSC();
const name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case Dimension:
value = this.Dimension();
break;
case Function:
value = this.QueryCSSFunction();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio, function, or identifier is expected');
break;
}
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'ContainerFeature',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
export function generate(node) {
this.token(LeftParenthesis, '(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
}

@ -0,0 +1,86 @@
// @ts-nocheck
import {
Ident,
Number,
Delim,
Dimension,
Function,
LeftParenthesis,
RightParenthesis,
WhiteSpace
} from 'css-tree/tokenizer';
export const name = 'ContainerFeatureRange';
export const structure = {
name: String,
value: ['Identifier', 'Number', 'Comparison', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
function lookup_non_WS_type_and_value(offset, type, referenceStr) {
let current_type;
do {
current_type = this.lookupType(offset++);
if (current_type !== WhiteSpace) {
break;
}
} while (current_type !== 0); // NULL -> 0
return current_type === type ? this.lookupValue(offset - 1, referenceStr) : false;
}
export function parse() {
const children = this.createList();
let child = null;
this.eat(LeftParenthesis);
this.skipSC();
while (!this.eof && this.tokenType !== RightParenthesis) {
switch (this.tokenType) {
case Number:
if (lookup_non_WS_type_and_value.call(this, 1, Delim, '/')) {
child = this.Ratio();
} else {
child = this.Number();
}
break;
case Delim:
child = this.Comparison();
break;
case Dimension:
child = this.Dimension();
break;
case Function:
child = this.QueryCSSFunction();
break;
case Ident:
child = this.Identifier();
break;
default:
this.error('Number, dimension, comparison, ratio, function, or identifier is expected');
break;
}
children.push(child);
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'ContainerFeatureRange',
loc: this.getLocationFromList(children),
children
};
}
export function generate(node) {
this.children(node);
}

@ -0,0 +1,85 @@
// @ts-nocheck
import {
Function,
Ident,
Number,
Dimension,
RightParenthesis,
Colon,
Delim
} from 'css-tree/tokenizer';
export const name = 'ContainerFeatureStyle';
export const structure = {
name: String,
value: ['Function', 'Identifier', 'Number', 'Dimension', 'QueryCSSFunction', 'Ratio', null]
};
export function parse() {
const start = this.tokenStart;
let value = null;
const function_name = this.consumeFunctionName();
if (function_name !== 'style') {
this.error('Unknown container style query identifier; "style" is expected');
}
this.skipSC();
const name = this.consume(Ident);
this.skipSC();
if (this.tokenType !== RightParenthesis) {
this.eat(Colon);
this.skipSC();
switch (this.tokenType) {
case Number:
if (this.lookupNonWSType(1) === Delim) {
value = this.Ratio();
} else {
value = this.Number();
}
break;
case Dimension:
value = this.Dimension();
break;
case Function:
value = this.QueryCSSFunction();
break;
case Ident:
value = this.Identifier();
break;
default:
this.error('Number, dimension, ratio, function or identifier is expected');
break;
}
this.skipSC();
}
this.eat(RightParenthesis);
return {
type: 'ContainerFeatureStyle',
loc: this.getLocation(start, this.tokenStart),
name,
value
};
}
export function generate(node) {
this.token(Function, 'style(');
this.token(Ident, node.name);
if (node.value !== null) {
this.token(Colon, ':');
this.node(node.value);
}
this.token(RightParenthesis, ')');
}

@ -0,0 +1,130 @@
// @ts-nocheck
import {
EOF,
WhiteSpace,
Comment,
Delim,
Function,
Ident,
LeftParenthesis,
RightParenthesis,
LeftCurlyBracket,
Colon
} from 'css-tree/tokenizer';
const CONTAINER_QUERY_KEYWORDS = new Set(['none', 'and', 'not', 'or']);
export const name = 'ContainerQuery';
export const structure = {
name: 'Identifier',
children: [[
'Identifier',
'ContainerFeature',
'ContainerFeatureRange',
'ContainerFeatureStyle',
'WhiteSpace'
]]
};
/**
* Looks ahead to determine if query feature is a range query. This involves locating at least one delimiter and no
* colon tokens.
*
* @returns {boolean} Is potential range query.
*/
function lookahead_is_range() {
let type;
let offset = 0;
let count = 0;
let delim_found = false;
let no_colon = true;
// A range query has maximum 5 tokens when formatted as 'mf-range' /
// '<mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>'. So only look ahead maximum of 6 non-whitespace tokens.
do {
type = this.lookupNonWSType(offset++);
if (type !== WhiteSpace) {
count++;
}
if (type === Delim) {
delim_found = true;
}
if (type === Colon) {
no_colon = false;
}
if (type === LeftCurlyBracket || type === RightParenthesis) {
break;
}
} while (type !== EOF && count <= 6);
return delim_found && no_colon;
}
export function parse() {
const start = this.tokenStart;
const children = this.createList();
let child = null;
let name = null;
// Parse potential container name.
if (this.tokenType === Ident) {
const container_name = this.substring(this.tokenStart, this.tokenEnd);
// Container name doesn't match a query keyword, so assign it as container name.
if (!CONTAINER_QUERY_KEYWORDS.has(container_name.toLowerCase())) {
name = container_name;
this.eatIdent(container_name);
}
}
this.skipSC();
scan:
while (!this.eof) {
switch (this.tokenType) {
case Comment:
case WhiteSpace:
this.next();
continue;
case Ident:
child = this.Identifier();
break;
case Function:
child = this.ContainerFeatureStyle();
break;
case LeftParenthesis:
// Lookahead to determine if range feature.
child = lookahead_is_range.call(this) ? this.ContainerFeatureRange() : this.ContainerFeature();
break;
default:
break scan;
}
children.push(child);
}
if (child === null) {
this.error('Identifier or parenthesis is expected');
}
return {
type: 'ContainerQuery',
loc: this.getLocation(start, this.tokenStart - 1),
name,
children
};
}
export function generate(node) {
if (typeof node.name === 'string') {
this.token(Ident, node.name);
}
this.children(node);
}

@ -0,0 +1,41 @@
// @ts-nocheck
import {
RightParenthesis
} from 'css-tree/tokenizer';
const QUERY_CSS_FUNCTIONS = new Set(['calc', 'clamp', 'min', 'max']);
export const name = 'QueryCSSFunction';
export const structure = {
name: String,
expression: String
};
export function parse() {
const start = this.tokenStart;
const name = this.consumeFunctionName();
if (!QUERY_CSS_FUNCTIONS.has(name)) {
this.error('Unknown query single value function; expected: "calc", "clamp", "max", min"');
}
const body = this.Raw(this.tokenIndex, null, false);
this.eat(RightParenthesis);
return {
type: 'QueryCSSFunction',
loc: this.getLocation(start, this.tokenStart),
name,
expression: body.value
};
}
export function generate(node) {
this.token(Function, `${node.name}(`);
this.node(node.expression);
this.token(RightParenthesis, ')');
}

@ -1,5 +1,6 @@
// @ts-ignore
import parse from 'css-tree/parser';
// import parse from 'css-tree/parser'; // When css-tree supports container queries uncomment.
import { parse } from './css-tree-cq/css_tree_parse'; // Use extended css-tree for container query support.
import { walk } from 'estree-walker';
import { Parser } from '../index';
import { Node } from 'estree';
@ -78,7 +79,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
});
parser.read(regex_starts_with_closing_style_tag);
const end = parser.index;
return {

@ -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<T=any>(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) {
@ -149,8 +164,9 @@ export interface SvelteComponentDev {
$destroy(): void;
[accessor: string]: any;
}
export interface ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>> {
target: Element | ShadowRoot;
target: Element | Document | ShadowRoot;
anchor?: Element;
props?: Props;
context?: Map<any, any>;

@ -1,5 +1,5 @@
import { ResizeObserverSingleton } from './ResizeObserverSingleton';
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.
@ -255,6 +255,10 @@ export function empty() {
return text('');
}
export function comment(content: string) {
return document.createComment(content);
}
export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
@ -551,6 +555,19 @@ export function claim_space(nodes) {
return claim_text(nodes, ' ');
}
export function claim_comment(nodes:ChildNodeArray, data) {
return claim_node<Comment>(
nodes,
(node: ChildNode): node is Comment => node.nodeType === 8,
(node: Comment) => {
node.data = '' + data;
return undefined;
},
() => comment(data),
true
);
}
function find_comment(nodes, text, start) {
for (let i = start; i < nodes.length; i += 1) {
const node = nodes[i];
@ -582,9 +599,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) {

@ -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'];

@ -13,7 +13,6 @@ const _boolean_attributes = [
'hidden',
'inert',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',

@ -0,0 +1 @@
div.svelte-xyz{container:test-container / inline-size}@container (min-width: 400px){div.svelte-xyz{color:red}}@container test-container (min-width: 410px){div.svelte-xyz{color:green}}@container test-container (width < 400px){div.svelte-xyz{color:blue}}@container test-container (0 <= width < 300px){div.svelte-xyz{color:purple}}@container not (width < 400px){div.svelte-xyz{color:pink}}@container (width > 400px) and (height > 400px){div.svelte-xyz{color:lightgreen}}@container (width > 400px) or (height > 400px){div.svelte-xyz{color:lightblue}}@container (width > 400px) and (width > 800px) or (orientation: portrait){div.svelte-xyz{color:salmon}}@container style(color: blue){div.svelte-xyz{color:tan}}@container test-container (min-width: calc(400px + 1px)){div.svelte-xyz{color:green}}@container test-container (width < clamp(200px, 40%, 400px)){div.svelte-xyz{color:blue}}@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)){div.svelte-xyz{color:purple}}@container style(--var: calc(400px + 1px)){div.svelte-xyz{color:sandybrown}}

@ -0,0 +1,87 @@
<div>container query</div>
<style>
div {
container: test-container / inline-size;
}
/* Most common container query statements. */
@container (min-width: 400px) {
div {
color: red;
}
}
@container test-container (min-width: 410px) {
div {
color: green;
}
}
@container test-container (width < 400px) {
div {
color: blue;
}
}
@container test-container (0 <= width < 300px) {
div {
color: purple;
}
}
@container not (width < 400px) {
div {
color: pink;
}
}
@container (width > 400px) and (height > 400px) {
div {
color: lightgreen;
}
}
@container (width > 400px) or (height > 400px) {
div {
color: lightblue;
}
}
@container (width > 400px) and (width > 800px) or (orientation: portrait) {
div {
color: salmon;
}
}
@container style(color: blue) {
div {
color: tan;
}
}
@container test-container (min-width: calc(400px + 1px)) {
div {
color: green;
}
}
@container test-container (width < clamp(200px, 40%, 400px)) {
div {
color: blue;
}
}
@container test-container (calc(400px + 1px) <= width < calc(500px + 1px)) {
div {
color: purple;
}
}
@container style(--var: calc(400px + 1px)) {
div {
color: sandybrown;
}
}
</style>

@ -0,0 +1 @@
<div><!-- test1 --><!-- test2 --></div>

@ -0,0 +1,20 @@
export default {
compileOptions: {
preserveComments:true
},
snapshot(target) {
const div = target.querySelector('div');
return {
div,
comment: div.childNodes[0]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.childNodes[0], snapshot.comment);
assert.equal(div.childNodes[1].nodeType, 8);
}
};

@ -0,0 +1 @@
<div><!-- test1 --><!-- test2 --></div>

@ -0,0 +1,5 @@
export default {
options: {
preserveComments: true
}
};

@ -0,0 +1,58 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
comment,
detach,
element,
init,
insert,
noop,
safe_not_equal,
space
} from "svelte/internal";
function create_fragment(ctx) {
let div0;
let t1;
let c;
let t2;
let div1;
return {
c() {
div0 = element("div");
div0.textContent = "content";
t1 = space();
c = comment(" comment ");
t2 = space();
div1 = element("div");
div1.textContent = "more content";
},
m(target, anchor) {
insert(target, div0, anchor);
insert(target, t1, anchor);
insert(target, c, anchor);
insert(target, t2, anchor);
insert(target, div1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div0);
if (detaching) detach(t1);
if (detaching) detach(c);
if (detaching) detach(t2);
if (detaching) detach(div1);
}
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,3 @@
<div>content</div>
<!-- comment -->
<div>more content</div>

@ -0,0 +1,13 @@
// A puppeteer test because JSDOM doesn't support contenteditable
export default {
html: '<div contenteditable="false"></div>',
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');
}
};

@ -0,0 +1,6 @@
<script>
export let text = '';
const updater = (event) => {text = event.target.textContent}
</script>
<div contenteditable="false" on:input={updater}>{text}</div>

@ -0,0 +1,24 @@
// A puppeteer test because JSDOM doesn't support contenteditable
export default {
html: '<div contenteditable="true"></div>',
ssrHtml: '<div contenteditable=""></div>',
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');
}
};

@ -0,0 +1,11 @@
<script>
let text = "";
const updater = (event) => {
text = event.target.textContent;
};
$: spread = {
contenteditable: text !== "a",
};
</script>
<div {...spread} on:input={updater}>{text}</div>

@ -0,0 +1,33 @@
// A puppeteer test because JSDOM doesn't support contenteditable
export default {
html: '<div contenteditable=""></div>',
// 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 <div> 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');
}
};

@ -0,0 +1,10 @@
export default {
props: {
hidden: true
},
html: '<div hidden />',
test({ assert, component, target }) {
component.hidden = false;
assert.htmlEqual(target.innerHTML, '<div />');
}
};

@ -0,0 +1,5 @@
<script>
export let hidden = false;
</script>
<div {hidden} />

@ -1,11 +0,0 @@
export default {
props: {
itemscope: true
},
test({ assert, target, component }) {
const div = target.querySelector('div');
assert.ok(div.itemscope);
component.itemscope = false;
assert.ok(!div.itemscope);
}
};

@ -1,5 +0,0 @@
<script>
export let itemscope;
</script>
<div {itemscope} />

@ -0,0 +1,25 @@
// There is no relationship between the attribute and the dom node with regards to microdata attributes https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata
export default {
html: `<div itemscope itemtype="https://schema.org/SoftwareApplication">
<span itemprop="name">Game</span> - REQUIRES
<span itemprop="operatingSystem">OS</span><br/>
<link itemprop="applicationCategory" href="https://schema.org/GameApplication"/>
<div itemprop="aggregateRating" itemscope="" itemtype="https://schema.org/AggregateRating">RATING:
<span itemprop="ratingValue">4.6</span> (
<span itemprop="ratingCount">8864</span> ratings )</div>
<div itemref="offers"></div>
</div>
<div
itemprop="offers"
itemid="offers"
id="offers"
itemscope
itemtype="https://schema.org/Offer"
>
Price: $<span itemprop="price">1.00</span>
<meta itemprop="priceCurrency" content="USD"/>
</div>
`
};

@ -0,0 +1,31 @@
<!-- Example from https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata -->
<div itemscope itemtype="https://schema.org/SoftwareApplication">
<span itemprop="name">Game</span> - REQUIRES
<span itemprop="operatingSystem">OS</span><br />
<link
itemprop="applicationCategory"
href="https://schema.org/GameApplication"
/>
<div
itemprop="aggregateRating"
itemscope
itemtype="https://schema.org/AggregateRating"
>
RATING:
<span itemprop="ratingValue">4.6</span> (
<span itemprop="ratingCount">8864</span> ratings )
</div>
<div itemref="offers" />
</div>
<div
itemprop="offers"
itemid="offers"
id="offers"
itemscope
itemtype="https://schema.org/Offer"
>
Price: $<span itemprop="price">1.00</span>
<meta itemprop="priceCurrency" content="USD" />
</div>

@ -1,15 +0,0 @@
export default {
html: `
<div contenteditable=""></div>
`,
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');
}
};

@ -0,0 +1,20 @@
export default {
html: `
<p>4, 12, 60</p>
`,
async test({ component, target, assert }) {
component.permutation = [2, 3, 1];
await (component.promise1 = Promise.resolve({length: 1, width: 2, height: 3}));
try {
await (component.promise2 = Promise.reject({length: 97, width: 98, height: 99}));
} catch (e) {
// nothing
}
assert.htmlEqual(target.innerHTML, `
<p>2, 11, 2</p>
<p>9506, 28811, 98</p>
`);
}
};

@ -0,0 +1,27 @@
<script>
export let promise1 = {length: 5, width: 3, height: 4};
export let promise2 = {length: 12, width: 5, height: 13};
export let permutation = [1, 2, 3];
function calculate(length, width, height) {
return {
'1-Dimensions': [length, width, height],
'2-Dimensions': [length * width, width * height, length * height],
'3-Dimensions': [length * width * height, length + width + height, length * width + width * height + length * height]
};
}
const th = 'th';
</script>
{#await promise1 then { length, width, height }}
{@const { [0]: a, [1]: b, [2]: c } = permutation}
{@const { [`${a}-Dimensions`]: { [c - 1]: first }, [`${b}-Dimensions`]: { [b - 1]: second }, [`${c}-Dimensions`]: { [a - 1]: third } } = calculate(length, width, height) }
<p>{first}, {second}, {third}</p>
{/await}
{#await promise2 catch { [`leng${th}`]: l, [`wid${th}`]: w, height: h }}
{@const [a, b, c] = permutation}
{@const { [`${a}-Dimensions`]: { [c - 1]: first }, [`${b}-Dimensions`]: { [b - 1]: second }, [`${c}-Dimensions`]: { [a - 1]: third } } = calculate(l, w, h) }
<p>{first}, {second}, {third}</p>
{/await}

@ -0,0 +1,15 @@
export default {
html: `
<button>6, 12, 8, 24</button>
<button>45, 35, 63, 315</button>
<button>60, 48, 80, 480</button>
`,
async test({ component, target, assert }) {
component.boxes = [{ length: 10, width: 20, height: 30 }];
assert.htmlEqual(target.innerHTML,
'<button>200, 600, 300, 6000</button>'
);
}
};

@ -0,0 +1,44 @@
<script>
export let boxes = [
{length: 2, width: 3, height: 4},
{length: 9, width: 5, height: 7},
{length: 10, width: 6, height: 8}
];
function calculate(length, width, height) {
return {
twoDimensions: {
bottomArea: length * width,
sideArea1: width * height,
sideArea2: length * height
},
threeDimensions: {
volume: length * width * height
}
};
}
export let dimension = 'Dimensions';
function changeDimension() {
dimension = 'DIMENSIONS';
}
let area = 'Area';
let th = 'th';
</script>
{#each boxes as { [`leng${th}`]: length, [`wid${th}`]: width, height }}
{@const {
[`two${dimension}`]: areas,
[`three${dimension}`]: {
volume
}
} = calculate(length, width, height)}
{@const {
i = 1,
[`bottom${area}`]: bottom,
[`side${area}${i++}`]: sideone,
[`side${area}${i++}`]: sidetwo
} = areas}
<button on:click={changeDimension}>{bottom}, {sideone}, {sidetwo}, {volume}</button>
{/each}

@ -0,0 +1,9 @@
export default {
html:'<div>same text</div>',
async test({ assert, target }) {
await new Promise(f => setTimeout(f, 10));
assert.htmlEqual(target.innerHTML, `
<div>same text text</div>
`);
}
};

@ -0,0 +1,8 @@
<script>
let text = 'same';
setTimeout(() => {
text = 'same text';
}, 5);
</script>
<div>{text} text</div>

@ -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
}
}
]

@ -622,197 +622,5 @@
"column": 0,
"line": 53
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 18,
"line": 57
},
"message": "A11y: Non-interactive element <ul> cannot have interactive role 'menu'",
"start": {
"column": 0,
"line": 57
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 21,
"line": 58
},
"message": "A11y: Non-interactive element <ul> cannot have interactive role 'menubar'",
"start": {
"column": 0,
"line": 58
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 24,
"line": 59
},
"message": "A11y: Non-interactive element <ul> cannot have interactive role 'radiogroup'",
"start": {
"column": 0,
"line": 59
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 21,
"line": 60
},
"message": "A11y: Non-interactive element <ul> cannot have interactive role 'tablist'",
"start": {
"column": 0,
"line": 60
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 18,
"line": 61
},
"message": "A11y: Non-interactive element <ul> cannot have interactive role 'tree'",
"start": {
"column": 0,
"line": 61
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 22,
"line": 62
},
"message": "A11y: Non-interactive element <ul> cannot have interactive role 'treegrid'",
"start": {
"column": 0,
"line": 62
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 18,
"line": 64
},
"message": "A11y: Non-interactive element <ol> cannot have interactive role 'menu'",
"start": {
"column": 0,
"line": 64
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 21,
"line": 65
},
"message": "A11y: Non-interactive element <ol> cannot have interactive role 'menubar'",
"start": {
"column": 0,
"line": 65
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 24,
"line": 66
},
"message": "A11y: Non-interactive element <ol> cannot have interactive role 'radiogroup'",
"start": {
"column": 0,
"line": 66
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 21,
"line": 67
},
"message": "A11y: Non-interactive element <ol> cannot have interactive role 'tablist'",
"start": {
"column": 0,
"line": 67
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 18,
"line": 68
},
"message": "A11y: Non-interactive element <ol> cannot have interactive role 'tree'",
"start": {
"column": 0,
"line": 68
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 22,
"line": 69
},
"message": "A11y: Non-interactive element <ol> cannot have interactive role 'treegrid'",
"start": {
"column": 0,
"line": 69
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 17,
"line": 71
},
"message": "A11y: Non-interactive element <li> cannot have interactive role 'tab'",
"start": {
"column": 0,
"line": 71
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 22,
"line": 72
},
"message": "A11y: Non-interactive element <li> cannot have interactive role 'menuitem'",
"start": {
"column": 0,
"line": 72
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 17,
"line": 73
},
"message": "A11y: Non-interactive element <li> cannot have interactive role 'row'",
"start": {
"column": 0,
"line": 73
}
},
{
"code": "a11y-no-noninteractive-element-to-interactive-role",
"end": {
"column": 44,
"line": 74
},
"message": "A11y: Non-interactive element <li> cannot have interactive role 'treeitem'",
"start": {
"column": 0,
"line": 74
}
}
]

Loading…
Cancel
Save