Merge branch 'master' into version-4

pull/8566/head
Simon Holthausen 1 year ago
commit 418f4c2381

@ -1,3 +1,7 @@
# HEADS UP: BIG RESTRUCTURING UNDERWAY
The Svelte repo is currently in the process of heavy restructuring for Svelte 4. After that, work on Svelte 5 will likely change a lot on the compiler aswell. For that reason, please don't open PRs that are large in scope, touch more than a couple of files etc. In other words, bug fixes are fine, but feature PRs will likely not be merged.
### Before submitting the PR, please make sure you do the following
- [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [ ] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.

@ -23,11 +23,18 @@
* Fix transitions so that they don't require a `style-src 'unsafe-inline'` Content Security Policy (CSP) ([#6662](https://github.com/sveltejs/svelte/issues/6662)).
* Explicitly disallow `var` declarations extending the reactive statement scope ([#6800](https://github.com/sveltejs/svelte/pull/6800))
## Unreleased (3.0)
* Handle `width`/`height` attributes when spreading ([#6752](https://github.com/sveltejs/svelte/issues/6752))
* Add support for resize observer bindings (`<div bind:contentRect|contentBoxSize|borderBoxSize|devicePixelContentBoxSize>`) ([#8022](https://github.com/sveltejs/svelte/pull/8022))
## 3.59.0
* Add `ResizeObserver` bindings `contentRect`/`contentBoxSize`/`borderBoxSize`/`devicePixelContentBoxSize` ([#8022](https://github.com/sveltejs/svelte/pull/8022))
* Add `devicePixelRatio` binding for `<svelte:window>` ([#8285](https://github.com/sveltejs/svelte/issues/8285))
* Add `fullscreenElement` and `visibilityState` bindings for `<svelte:document>` ([#8507](https://github.com/sveltejs/svelte/pull/8507))
* Add `a11y-autocomplete-valid` warning ([#8520](https://github.com/sveltejs/svelte/pull/8520))
* Fix handling of `width`/`height` attributes when spreading ([#6752](https://github.com/sveltejs/svelte/issues/6752))
* Fix updating of interpolated `style:` directive when using spread ([#8438](https://github.com/sveltejs/svelte/issues/8438))
* Remove `style:` directive property when value is `undefined` ([#8462](https://github.com/sveltejs/svelte/issues/8462))
* Fix type of `VERSION` compiler export ([#8498](https://github.com/sveltejs/svelte/issues/8498))
* Relax `a11y-no-redundant-roles` warning ([#8536](https://github.com/sveltejs/svelte/pull/8536))
* Handle nested array rest destructuring ([#8552](https://github.com/sveltejs/svelte/issues/8552), [#8554](https://github.com/sveltejs/svelte/issues/8554))
## 3.58.0
* Add `bind:innerText` for `contenteditable` elements ([#3311](https://github.com/sveltejs/svelte/issues/3311))

@ -62,6 +62,8 @@ When [opening a new issue](https://github.com/sveltejs/svelte/issues/new/choose)
## Pull requests
> HEADS UP: The Svelte repo is currently in the process of heavy restructuring for Svelte 4. After that, work on Svelte 5 will likely change a lot on the compiler aswell. For that reason, please don't open PRs that are large in scope, touch more than a couple of files etc. In other words, bug fixes are fine, but feature PRs will likely not be merged.
### Proposing a change
If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with [feature template](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml).

@ -1081,11 +1081,17 @@ export interface SvelteMediaTimeRange {
end: number;
}
export interface SvelteDocumentAttributes extends HTMLAttributes<Document> {
readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null;
readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null;
}
export interface SvelteWindowAttributes extends HTMLAttributes<Window> {
readonly 'bind:innerWidth'?: Window['innerWidth'] | undefined | null;
readonly 'bind:innerHeight'?: Window['innerHeight'] | undefined | null;
readonly 'bind:outerWidth'?: Window['outerWidth'] | undefined | null;
readonly 'bind:outerHeight'?: Window['outerHeight'] | undefined | null;
readonly 'bind:devicePixelRatio'?: Window['devicePixelRatio'] | undefined | null;
'bind:scrollX'?: Window['scrollX'] | undefined | null;
'bind:scrollY'?: Window['scrollY'] | undefined | null;
readonly 'bind:online'?: Window['navigator']['onLine'] | undefined | null;
@ -1594,7 +1600,7 @@ export interface SvelteHTMLElements {
// Svelte specific
'svelte:window': SvelteWindowAttributes;
'svelte:document': HTMLAttributes<Document>;
'svelte:document': SvelteDocumentAttributes;
'svelte:body': HTMLAttributes<HTMLElement>;
'svelte:fragment': { slot?: string };
'svelte:options': {

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.58.0",
"version": "3.59.0",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"main": "index",

@ -42,7 +42,7 @@ Svelte uses the `export` keyword to mark a variable declaration as a *property*
---
You can specify a default initial value for a prop. It will be used if the component's consumer doesn't specify the prop on the component (or if its initial value is `undefined`) when instantiating the component. Note that whenever a prop is removed by the consumer, its value is set to `undefined` rather than the initial value.
You can specify a default initial value for a prop. It will be used if the component's consumer doesn't specify the prop on the component (or if its initial value is `undefined`) when instantiating the component. Note that if the values of props are subsequently updated, then any prop whose value is not specified will be set to `undefined` (rather than its initial value).
In development mode (see the [compiler options](/docs#compile-time-svelte-compile)), a warning will be printed if no default initial value is provided and the consumer does not specify a value. To squelch this warning, ensure that a default initial value is specified, even if it is `undefined`.

@ -823,6 +823,8 @@ Inputs that work together can use `bind:group`.
<input type="checkbox" bind:group={fillings} value="Guac (extra)">
```
> `bind:group` only works if the inputs are in the same Svelte component.
#### bind:this
```sv
@ -1742,6 +1744,7 @@ You can also bind to the following properties:
* `scrollX`
* `scrollY`
* `online` — an alias for `window.navigator.onLine`
* `devicePixelRatio`
All except `scrollX` and `scrollY` are readonly.
@ -1756,6 +1759,9 @@ All except `scrollX` and `scrollY` are readonly.
```sv
<svelte:document on:event={handler}/>
```
```sv
<svelte:document bind:prop={value}/>
```
---
@ -1770,6 +1776,15 @@ As with `<svelte:window>`, this element may only appear the top level of your co
/>
```
---
You can also bind to the following properties:
* `fullscreenElement`
* `visibilityState`
All are readonly.
### `<svelte:body>`
```sv

@ -26,7 +26,7 @@ import TemplateScope from './nodes/shared/TemplateScope';
import fuzzymatch from '../utils/fuzzymatch';
import get_object from './utils/get_object';
import Slot from './nodes/Slot';
import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration, FunctionDeclaration, FunctionExpression, VariableDeclarator, ObjectExpression } from 'estree';
import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration, FunctionDeclaration, FunctionExpression, VariableDeclarator, ObjectExpression, Pattern, Expression } from 'estree';
import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, b } from 'code-red';
@ -1036,7 +1036,7 @@ export default class Component {
const inserts = [];
const props = [];
function add_new_props(exported, local, default_value) {
function add_new_props(exported: Identifier, local: Pattern, default_value: Expression) {
props.push({
type: 'Property',
method: false,
@ -1066,7 +1066,7 @@ export default class Component {
for (let index = 0; index < node.declarations.length; index++) {
const declarator = node.declarations[index];
if (declarator.id.type !== 'Identifier') {
function get_new_name(local) {
function get_new_name(local: Identifier): Identifier {
const variable = component.var_lookup.get(local.name);
if (variable.subscribable) {
inserts.push(get_insert(variable));
@ -1080,7 +1080,7 @@ export default class Component {
return local;
}
function rename_identifiers(param: Node) {
function rename_identifiers(param: Pattern) {
switch (param.type) {
case 'ObjectPattern': {
const handle_prop = (prop: Property | RestElement) => {
@ -1089,7 +1089,7 @@ export default class Component {
} else if (prop.value.type === 'Identifier') {
prop.value = get_new_name(prop.value);
} else {
rename_identifiers(prop.value);
rename_identifiers(prop.value as Pattern);
}
};
@ -1097,7 +1097,7 @@ export default class Component {
break;
}
case 'ArrayPattern': {
const handle_element = (element: Node, index: number, array: Node[]) => {
const handle_element = (element: Pattern | null, index: number, array: Array<Pattern | null>) => {
if (element) {
if (element.type === 'Identifier') {
array[index] = get_new_name(element);
@ -1112,7 +1112,11 @@ export default class Component {
}
case 'RestElement':
param.argument = get_new_name(param.argument);
if (param.argument.type === 'Identifier') {
param.argument = get_new_name(param.argument);
} else {
rename_identifiers(param.argument);
}
break;
case 'AssignmentPattern':

@ -170,6 +170,10 @@ export default {
code: 'a11y-missing-attribute',
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
}),
a11y_autocomplete_valid: (type: null | true | string, value: null | true | string) => ({
code: 'a11y-autocomplete-valid',
message: `A11y: The value '${value}' is not supported by the attribute 'autocomplete' on element <input type="${type}">`
}),
a11y_img_redundant_alt: {
code: 'a11y-img-redundant-alt',
message: 'A11y: Screenreaders already announce <img> elements as an image.'
@ -228,7 +232,7 @@ export default {
},
invalid_rest_eachblock_binding: (rest_element_name: string) => ({
code: 'invalid-rest-eachblock-binding',
message: `...${rest_element_name} operator will create a new object and binding propagation with original object will not work`
message: `The rest operator (...) will create a new object and binding '${rest_element_name}' with the original object will not work`
}),
avoid_mouse_events_on_document: {
code: 'avoid-mouse-events-on-document',

@ -9,6 +9,7 @@ import { TemplateNode } from '../../interfaces';
import Element from './Element';
import InlineComponent from './InlineComponent';
import Window from './Window';
import Document from './Document';
import { clone } from '../../utils/clone';
import compiler_errors from '../compiler_errors';
import compiler_warnings from '../compiler_warnings';
@ -36,7 +37,7 @@ export default class Binding extends Node {
is_contextual: boolean;
is_readonly: boolean;
constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) {
constructor(component: Component, parent: Element | InlineComponent | Window | Document, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {

@ -1,14 +1,24 @@
import Node from './shared/Node';
import Binding from './Binding';
import EventHandler from './EventHandler';
import fuzzymatch from '../../utils/fuzzymatch';
import Action from './Action';
import Component from '../Component';
import list from '../../utils/list';
import TemplateScope from './shared/TemplateScope';
import { Element } from '../../interfaces';
import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors';
const valid_bindings = [
'fullscreenElement',
'visibilityState'
];
export default class Document extends Node {
type: 'Document';
handlers: EventHandler[] = [];
bindings: Binding[] = [];
actions: Action[] = [];
constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) {
@ -17,6 +27,17 @@ export default class Document extends Node {
info.attributes.forEach((node) => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Binding') {
if (!~valid_bindings.indexOf(node.name)) {
const match = fuzzymatch(node.name, valid_bindings);
if (match) {
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:document>', ` (did you mean '${match}'?)`));
} else {
return component.error(node, compiler_errors.invalid_binding_on(node.name, '<svelte:document>', ` — valid bindings are ${list(valid_bindings)}`));
}
}
this.bindings.push(new Binding(component, this, scope, node));
} else if (node.type === 'Action') {
this.actions.push(new Action(component, this, scope, node));
} else {

@ -26,7 +26,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, is_static_element, has_disabled_attribute } 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, is_valid_autocomplete } 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);
@ -131,6 +131,7 @@ const a11y_implicit_semantics = new Map([
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
['figure', 'figure'],
['form', 'form'],
['h1', 'heading'],
['h2', 'heading'],
@ -142,6 +143,7 @@ const a11y_implicit_semantics = new Map([
['img', 'img'],
['li', 'listitem'],
['link', 'link'],
['main', 'main'],
['menu', 'list'],
['meter', 'progressbar'],
['nav', 'navigation'],
@ -152,6 +154,7 @@ const a11y_implicit_semantics = new Map([
['progress', 'progressbar'],
['section', 'region'],
['summary', 'button'],
['table', 'table'],
['tbody', 'rowgroup'],
['textarea', 'textbox'],
['tfoot', 'rowgroup'],
@ -660,9 +663,7 @@ export default class Element extends Node {
}
// no-redundant-roles
const has_redundant_role = current_role === get_implicit_role(this.name, attribute_map);
if (this.name === current_role || has_redundant_role) {
if (current_role === get_implicit_role(this.name, attribute_map)) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(current_role));
}
@ -918,6 +919,18 @@ export default class Element extends Node {
should_have_attribute(this, required_attributes, 'input type="image"');
}
}
// autocomplete-valid
const autocomplete = attribute_map.get('autocomplete');
if (type && autocomplete) {
const type_value = type.get_static_value();
const autocomplete_value = autocomplete.get_static_value();
if (!is_valid_autocomplete(type_value, autocomplete_value)) {
component.warn(autocomplete, compiler_warnings.a11y_autocomplete_valid(type_value, autocomplete_value));
}
}
}
if (this.name === 'img') {

@ -17,6 +17,7 @@ const valid_bindings = [
'outerHeight',
'scrollX',
'scrollY',
'devicePixelRatio',
'online'
];

@ -1,5 +1,5 @@
import { x } from 'code-red';
import { Node, Identifier, Expression, PrivateIdentifier } from 'estree';
import { Node, Identifier, Expression, PrivateIdentifier, Pattern } from 'estree';
import { walk } from 'estree-walker';
import is_reference, { NodeWithPropertyDefinition } from 'is-reference';
import { clone } from '../../../utils/clone';
@ -30,15 +30,17 @@ export function unpack_destructuring({
default_modifier = (node) => node,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element = false
}: {
contexts: Context[];
node: Node;
node: Pattern;
modifier?: DestructuredVariable['modifier'];
default_modifier?: DestructuredVariable['default_modifier'];
scope: TemplateScope;
component: Component;
context_rest_properties: Map<string, Node>;
in_rest_element?: boolean;
}) {
if (!node) return;
@ -49,28 +51,26 @@ export function unpack_destructuring({
modifier,
default_modifier
});
} else if (node.type === 'RestElement') {
contexts.push({
type: 'DestructuredVariable',
key: node.argument as Identifier,
modifier,
default_modifier
});
context_rest_properties.set((node.argument as Identifier).name, node);
if (in_rest_element) {
context_rest_properties.set(node.name, node);
}
} else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => {
if (element && element.type === 'RestElement') {
node.elements.forEach((element: Pattern | null, i: number) => {
if (!element) {
return;
} else if (element.type === 'RestElement') {
unpack_destructuring({
contexts,
node: element,
node: element.argument,
modifier: (node) => x`${modifier(node)}.slice(${i})` as Node,
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element: true
});
context_rest_properties.set((element.argument as Identifier).name, element);
} else if (element && element.type === 'AssignmentPattern') {
} else if (element.type === 'AssignmentPattern') {
const n = contexts.length;
mark_referenced(element.right, scope, component);
@ -87,7 +87,8 @@ export function unpack_destructuring({
)}` as Node,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element
});
} else {
unpack_destructuring({
@ -97,7 +98,8 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element
});
}
});
@ -116,9 +118,9 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element: true
});
context_rest_properties.set((property.argument as Identifier).name, property);
} else if (property.type === 'Property') {
const key = property.key;
const value = property.value;
@ -168,7 +170,8 @@ export function unpack_destructuring({
)}` as Node,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element
});
} else {
// e.g. { property } or { property: newName }
@ -179,7 +182,8 @@ export function unpack_destructuring({
default_modifier,
scope,
component,
context_rest_properties
context_rest_properties,
in_rest_element
});
}
}

@ -1,6 +1,6 @@
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import { x } from 'code-red';
import { b, x } from 'code-red';
import Document from '../../nodes/Document';
import { Identifier } from 'estree';
import EventHandler from './Element/EventHandler';
@ -9,6 +9,16 @@ import { TemplateNode } from '../../../interfaces';
import Renderer from '../Renderer';
import add_actions from './shared/add_actions';
const associated_events = {
fullscreenElement: ['fullscreenchange'],
visibilityState: ['visibilitychange']
};
const readonly = new Set([
'fullscreenElement',
'visibilityState'
]);
export default class DocumentWrapper extends Wrapper {
node: Document;
handlers: EventHandler[];
@ -19,7 +29,66 @@ export default class DocumentWrapper extends Wrapper {
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
const { renderer } = this;
const { component } = renderer;
const events: Record<string, Array<{ name: string; value: string }>> = {};
const bindings: Record<string, string> = {};
add_event_handlers(block, x`@_document`, this.handlers);
add_actions(block, x`@_document`, this.node.actions);
this.node.bindings.forEach(binding => {
// TODO: what if it's a MemberExpression?
const binding_name = (binding.expression.node as Identifier).name;
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
renderer.readonly.add(binding_name);
}
bindings[binding.name] = binding_name;
const binding_events = associated_events[binding.name];
const property = binding.name;
binding_events.forEach(associated_event => {
if (!events[associated_event]) events[associated_event] = [];
events[associated_event].push({
name: binding_name,
value: property
});
});
});
Object.keys(events).forEach(event => {
const id = block.get_unique_name(`ondocument${event}`);
const props = events[event];
renderer.add_to_context(id.name);
const fn = renderer.reference(id.name);
props.forEach(prop => {
renderer.meta_bindings.push(
b`this._state.${prop.name} = @_document.${prop.value};`
);
});
block.event_listeners.push(x`
@listen(@_document, "${event}", ${fn})
`);
component.partly_hoisted.push(b`
function ${id}() {
${props.map(prop => renderer.invalidate(prop.name, x`${prop.name} = @_document.${prop.value}`))}
}
`);
block.chunks.init.push(b`
@add_render_callback(${fn});
`);
component.has_reactive_assignments = true;
});
}
}

@ -1238,19 +1238,27 @@ export default class ElementWrapper extends Wrapper {
block.chunks.hydrate.push(updater);
const self_deps = expression.dynamic_dependencies();
const all_deps = new Set([
...self_deps,
...this.dynamic_style_dependencies
]);
let condition = block.renderer.dirty([...all_deps]);
// Assume that style has changed through the spread attribute
if (has_spread) {
if (should_cache && all_deps.size) {
// Update the cached value
block.chunks.update.push(b`
if (${condition}) {
${cached_snippet} = ${snippet};
}`
);
}
block.chunks.update.push(updater);
} else {
const self_deps = expression.dynamic_dependencies();
const all_deps = new Set([
...self_deps,
...this.dynamic_style_dependencies
]);
if (all_deps.size === 0) return;
let condition = block.renderer.dirty([...all_deps]);
if (all_deps.size === 0) return;
if (should_cache) {
condition = x`${condition} && ${cached_snippet} !== (${cached_snippet} = ${snippet})`;

@ -14,6 +14,7 @@ const associated_events = {
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
devicePixelRatio: 'resize',
scrollX: 'scroll',
scrollY: 'scroll'
@ -29,6 +30,7 @@ const readonly = new Set([
'innerHeight',
'outerWidth',
'outerHeight',
'devicePixelRatio',
'online'
]);

@ -6,6 +6,7 @@ import {
} from 'aria-query';
import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
import Attribute from '../nodes/Attribute';
import { regex_whitespaces } from '../../utils/patterns';
const aria_roles = roles_map.keys();
const abstract_roles = new Set(aria_roles.filter(role => roles_map.get(role).abstract));
@ -228,3 +229,104 @@ export function is_semantic_role_element(role: ARIARoleDefinitionKey, tag_name:
}
return false;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
const address_type_tokens = new Set(['shipping', 'billing']);
const autofill_field_name_tokens = new Set([
'',
'on',
'off',
'name',
'honorific-prefix',
'given-name',
'additional-name',
'family-name',
'honorific-suffix',
'nickname',
'username',
'new-password',
'current-password',
'one-time-code',
'organization-title',
'organization',
'street-address',
'address-line1',
'address-line2',
'address-line3',
'address-level4',
'address-level3',
'address-level2',
'address-level1',
'country',
'country-name',
'postal-code',
'cc-name',
'cc-given-name',
'cc-additional-name',
'cc-family-name',
'cc-number',
'cc-exp',
'cc-exp-month',
'cc-exp-year',
'cc-csc',
'cc-type',
'transaction-currency',
'transaction-amount',
'language',
'bday',
'bday-day',
'bday-month',
'bday-year',
'sex',
'url',
'photo'
]);
const contact_type_tokens = new Set(['home', 'work', 'mobile', 'fax', 'pager']);
const autofill_contact_field_name_tokens = new Set([
'tel',
'tel-country-code',
'tel-national',
'tel-area-code',
'tel-local',
'tel-local-prefix',
'tel-local-suffix',
'tel-extension',
'email',
'impp'
]);
export function is_valid_autocomplete(type: null | true | string, autocomplete: null | true | string) {
if (typeof autocomplete !== 'string' || typeof type !== 'string') {
return false;
}
const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces);
if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) {
tokens.shift();
}
if (address_type_tokens.has(tokens[0])) {
tokens.shift();
}
if (autofill_field_name_tokens.has(tokens[0])) {
tokens.shift();
} else {
if (contact_type_tokens.has(tokens[0])) {
tokens.shift();
}
if (autofill_contact_field_name_tokens.has(tokens[0])) {
tokens.shift();
} else {
return false;
}
}
if (tokens[0] === 'webauthn') {
tokens.shift();
}
return tokens.length === 0;
}

@ -313,7 +313,7 @@ export function attr(node: Element, attribute: string, value?: string) {
else if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value);
}
/**
/**
* List of attributes that should always be set through the attr method,
* because updating them through the property setter doesn't work reliably.
* In the example of `width`/`height`, the problem is that the setter only
@ -651,7 +651,7 @@ export function set_input_type(input, type) {
}
export function set_style(node, key, value, important) {
if (value === null) {
if (value == null) {
node.style.removeProperty(key);
} else {
node.style.setProperty(key, value, important ? 'important' : '');

@ -0,0 +1,18 @@
export default {
html: `
<div style="background-color: rgb(255, 0, 0);"></div>
`,
test({ assert, target, window, component }) {
const div = target.querySelector('div');
const styles = window.getComputedStyle(div);
assert.equal(styles.backgroundColor, 'rgb(255, 0, 0)');
{
component.backgroundColor = 128;
const div = target.querySelector('div');
const styles = window.getComputedStyle(div);
assert.equal(styles.backgroundColor, 'rgb(128, 0, 0)');
}
}
};

@ -0,0 +1,5 @@
<script lang="ts">
export let backgroundColor = 255;
</script>
<div style:background-color="rgb({backgroundColor}, 0, 0)" {...$$restProps} />

@ -0,0 +1,12 @@
export default {
html: `
<h1>1</h1>
<h1>2</h1>
<h1>3</h1>
<h1>5</h1>
<h1>10</h1>
<h1>20</h1>
<h1>30</h1>
<h1>6</h1>
`
};

@ -0,0 +1,15 @@
<script>
let [first, second, ...[third, ...[, fifth]]] = [1, 2, 3, 4, 5];
let [one, two, ...[three, ...{ length }]] = [10, 20, 30, 40, 50, 60, 70, 80, 90];
</script>
<h1>{first}</h1>
<h1>{second}</h1>
<h1>{third}</h1>
<h1>{fifth}</h1>
<h1>{one}</h1>
<h1>{two}</h1>
<h1>{three}</h1>
<h1>{length}</h1>

@ -0,0 +1,69 @@
export default {
props: {
thePromise: new Promise(_ => {})
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8]));
assert.htmlEqual(
target.innerHTML,
`
<p>a: 1</p>
<p>b: 2</p>
<p>c: 5</p>
<p>remaining length: 3</p>
`
);
await (component.thePromise = Promise.resolve([9, 10, 11, 12, 13, 14, 15]));
assert.htmlEqual(
target.innerHTML,
`
<p>a: 9</p>
<p>b: 10</p>
<p>c: 13</p>
<p>remaining length: 2</p>
`
);
try {
await (component.thePromise = Promise.reject([16, 17, 18, 19, 20, 21, 22]));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>c: 16</p>
<p>d: 17</p>
<p>e: 18</p>
<p>f: 19</p>
<p>g: 22</p>
`
);
try {
await (component.thePromise = Promise.reject([23, 24, 25, 26, 27, 28, 29, 30, 31]));
} catch (e) {
// do nothing
}
assert.htmlEqual(
target.innerHTML,
`
<p>c: 23</p>
<p>d: 24</p>
<p>e: 25</p>
<p>f: 26</p>
<p>g: 29</p>
`
);
}
};

@ -0,0 +1,18 @@
<script>
export let thePromise;
</script>
{#await thePromise}
loading...
{:then [ a, b, ...[,, c, ...{ length } ]]}
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
<p>remaining length: {length}</p>
{:catch [c, ...[d, e, f, ...[,,g]]]}
<p>c: {c}</p>
<p>d: {d}</p>
<p>e: {e}</p>
<p>f: {f}</p>
<p>g: {g}</p>
{/await}

@ -0,0 +1,19 @@
export default {
html: '<div>12 120 70, 30+4=34</div>',
async test({ component, target, assert }) {
component.promise1 = Promise.resolve({width: 5, height: 6});
component.promise2 = Promise.reject({width: 6, height: 7});
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `
<div>30 300 110, 50+6=56</div>
<div>42 420 130, 60+7=67</div>
`);
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,23 @@
<script>
export let promise1 = {width: 3, height: 4};
export let promise2 = {width: 5, height: 7};
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#await promise1 then { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, ...[_height, ...[sum]]] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/await}
{#await promise2 catch { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, ...[_height, ...[sum]]] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/await}

@ -0,0 +1,30 @@
export default {
html: `
<div>12 120 70, 30+4=34</div>
<div>35 350 120, 50+7=57</div>
<div>48 480 140, 60+8=68</div>
`,
async test({ component, target, assert }) {
component.constant = 20;
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>35 700 240, 100+7=107</div>
<div>48 960 280, 120+8=128</div>
`);
component.boxes = [
{width: 3, height: 4},
{width: 4, height: 5},
{width: 5, height: 6},
{width: 6, height: 7}
];
assert.htmlEqual(target.innerHTML, `
<div>12 240 140, 60+4=64</div>
<div>20 400 180, 80+5=85</div>
<div>30 600 220, 100+6=106</div>
<div>42 840 260, 120+7=127</div>
`);
}
};

@ -0,0 +1,19 @@
<script>
export let boxes = [
{width: 3, height: 4},
{width: 5, height: 7},
{width: 6, height: 8},
];
export let constant = 10;
function calculate(width, height, constant) {
return { area: width * height, volume: width * height * constant };
}
</script>
{#each boxes as { width, height }}
{@const {area, volume} = calculate(width, height, constant)}
{@const perimeter = (width + height) * constant}
{@const [_width, ...[_height, ...[sum]]] = [width * constant, height, width * constant + height]}
<div>{area} {volume} {perimeter}, {_width}+{_height}={sum}</div>
{/each}

@ -0,0 +1,25 @@
<script>
import { writable } from 'svelte/store';
const THING = { a: 1, b: { c: 2, d: [3, 4, writable(5), 6, 7] }, e: [6], h: 8 };
const default_g = 9;
export let { a, b: { c, d: [d_one, ...[, ...[d_three, ...{ length }]]], f }, e: [e_one], g = default_g } = THING;
export const { a: A, b: { c: C } } = THING;
</script>
<div>
a: {a},
b: {typeof b},
c: {c},
d_one: {d_one},
d_three: {$d_three},
length: {length},
f: {f},
g: {g},
e: {typeof e},
e_one: {e_one},
A: {A},
C: {C}
</div>
<div>{JSON.stringify(THING)}</div>

@ -0,0 +1,9 @@
export default {
html: `
<div>a: 1, b: undefined, c: 2, d_one: 3, d_three: 5, length: 2, f: undefined, g: 9, e: undefined, e_one: 6, A: 1, C: 2</div>
<div>{"a":1,"b":{"c":2,"d":[3,4,{},6,7]},"e":[6],"h":8}</div>
<br>
<div>a: a, b: undefined, c: 2, d_one: d_one, d_three: 5, length: 7, f: f, g: g, e: undefined, e_one: 6, A: 1, C: 2</div>
<div>{"a":1,"b":{"c":2,"d":[3,4,{},6,7]},"e":[6],"h":8}</div>
`
};

@ -0,0 +1,7 @@
<script>
import A from './A.svelte';
</script>
<A />
<br />
<A a="a" d_one="d_one" list_one="list_one" f="f" list_two_b="list_two_b" g="g" A="A" C="C" length={7} />

@ -0,0 +1,23 @@
<script>
import { writable } from "svelte/store";
let default_b = 5;
const LIST = [1, 2, 3, { a: 4 }, [5, writable(6), writable(7), 8]];
export const [
x,
,
...[, { a: list_two_a, b: list_two_b = default_b }, [, ...{ length: y }]]
] = LIST;
export let [
l,
m,
,
...[{ a: n, b: o = default_b }, [p, q, ...[r, ...{ length: s }]]]
] = LIST;
</script>
<div>
x: {x}, list_two_a: {list_two_a}, list_two_b: {list_two_b}, y: {y}, l: {l}, m: {m},
n: {n}, o: {o}, p: {p}, q: {$q}, r: {$r}, s: {s}
</div>
<div>{JSON.stringify(LIST)}</div>

@ -0,0 +1,19 @@
export default {
html: `
<div>x: 1, list_two_a: 4, list_two_b: 5, y: 3, l: 1, m: 2, n: 4, o: 5, p: 5, q: 6, r: 7, s: 1</div>
<div>[1,2,3,{"a":4},[5,{},{},8]]</div>
<br><div>x: 1, list_two_a: 4, list_two_b: 5, y: 3, l: l, m: m, n: n, o: o, p: p, q: q, r: r, s: s</div>
<div>[1,2,3,{"a":4},[5,{},{},8]]</div>
`,
async test({ component, assert, target }) {
await component.update();
assert.htmlEqual(target.innerHTML, `
<div>x: 1, list_two_a: 4, list_two_b: 5, y: 3, l: 1, m: 2, n: 4, o: 5, p: 5, q: 6, r: 7, s: 1</div>
<div>[1,2,3,{"a":4},[5,{},{},8]]</div>
<br><div>x: 1, list_two_a: 4, list_two_b: 5, y: 3, l: LL, m: MM, n: NN, o: OO, p: PP, q: QQ, r: RR, s: SS</div>
<div>[1,2,3,{"a":4},[5,{},{},8]]</div>
`);
}
};

@ -0,0 +1,36 @@
<script>
import A from "./A.svelte";
import { writable } from "svelte/store";
let x = "x",
list_two_a = "list_two_a",
list_two_b = "list_two_b",
y = writable("y"),
l = "l",
m = "m",
n = "n",
o = "o",
p = "p",
q = writable("q"),
r = writable("r"),
s = "s";
export function update() {
x = "XX";
list_two_a = "LIST_TWO_A";
list_two_b = "LIST_TWO_B";
y = writable("YY");
l = "LL";
m = "MM";
n = "NN";
o = "OO";
p = "PP";
q = writable("QQ");
r = writable("RR");
s = "SS";
}
</script>
<A />
<br />
<A {x} {list_two_a} {list_two_b} {y} {l} {m} {n} {o} {p} {q} {r} {s} />

@ -0,0 +1,31 @@
export default {
before_test() {
Object.defineProperties(window.document, {
fullscreenElement: {
value: null,
configurable: true
}
});
},
// copied from window-binding
// there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason
skip_if_ssr: true,
async test({ assert, target, window, component }) {
const event = new window.Event('fullscreenchange');
const div = target.querySelector('div');
Object.defineProperties(window.document, {
fullscreenElement: {
value: div,
configurable: true
}
});
window.document.dispatchEvent(event);
assert.equal(component.fullscreen, div);
}
};

@ -0,0 +1,7 @@
<script>
export let fullscreen;
</script>
<svelte:document bind:fullscreenElement={fullscreen}/>
<div />

@ -0,0 +1,24 @@
export default {
props: {
array: [
[1, 2, 3, 4, 5],
[6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
]
},
html: `
<p>First: 1, Second: 2, Third: 3, Elements remaining: 2</p>
<p>First: 6, Second: 7, Third: 8, Elements remaining: 0</p>
<p>First: 9, Second: 10, Third: 11, Elements remaining: 1</p>
<p>First: 13, Second: 14, Third: 15, Elements remaining: 7</p>
`,
test({ assert, component, target }) {
component.array = [[23, 24, 25, 26, 27, 28, 29]];
assert.htmlEqual( target.innerHTML, `
<p>First: 23, Second: 24, Third: 25, Elements remaining: 4</p>
`);
}
};

@ -0,0 +1,9 @@
<script>
export let array;
</script>
{#each array as [first, second, ...[third, ...{ length }]]}
<p>
First: {first}, Second: {second}, Third: {third}, Elements remaining: {length}
</p>
{/each}

@ -0,0 +1,11 @@
export default {
async test({ assert, target, window }) {
const div = target.querySelector('div');
const click = new window.MouseEvent('click');
assert.htmlEqual(target.innerHTML, '<div style="background: red;"></div>');
await div.dispatchEvent(click);
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<div style=""></div>');
}
};

@ -0,0 +1,9 @@
<script>
let bg = "red";
const handle = () => {
bg = undefined;
};
</script>
<div style:background={bg} on:click={handle}></div>

@ -1,5 +1,5 @@
export default {
html: '<div>1024x768</div>',
html: '<div>1024x768</div><div>1</div>',
before_test() {
Object.defineProperties(window, {
@ -10,6 +10,10 @@ export default {
innerHeight: {
value: 768,
configurable: true
},
devicePixelRatio: {
value: 1,
configurable: true
}
});
},
@ -27,13 +31,17 @@ export default {
innerHeight: {
value: 456,
configurable: true
},
devicePixelRatio: {
value: 2,
configurable: true
}
});
await window.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<div>567x456</div>
<div>567x456</div><div>2</div>
`);
}
};

@ -1,8 +1,10 @@
<script>
export let width;
export let height;
export let devicePixelRatio;
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height}/>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} bind:devicePixelRatio={devicePixelRatio}/>
<div>{width}x{height}</div>
<div>{width}x{height}</div>
<div>{devicePixelRatio}</div>

@ -0,0 +1,21 @@
<!-- VALID -->
<input type="text" />
<input type="text" autocomplete="name" />
<input type="text" autocomplete="off" />
<input type="text" autocomplete="on" />
<input type="text" autocomplete="billing family-name" />
<input type="hidden" autocomplete="section-blue shipping street-address" />
<input type="text" autocomplete="section-somewhere shipping work email" />
<input type="text" autocomplete="section-somewhere shipping work email webauthn" />
<input type="text" autocomplete="SECTION-SOMEWHERE SHIPPING WORK EMAIL WEBAUTHN" />
<input type="TEXT" autocomplete="ON" />
<input type="email" autocomplete="url" />
<input type="text" autocomplete="section-blue shipping street-address" />
<input type="hidden" autocomplete="off" />
<input type="hidden" autocomplete="on" />
<input type="text" autocomplete="" />
<!-- INVALID -->
<input type="text" autocomplete />
<input type="text" autocomplete="incorrect" />
<input type="text" autocomplete="webauthn" />

@ -0,0 +1,38 @@
[
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 31,
"line": 19
},
"message": "A11y: The value 'true' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 19
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 43,
"line": 20
},
"message": "A11y: The value 'incorrect' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 20
}
},
{
"code": "a11y-autocomplete-valid",
"end": {
"column": 42,
"line": 21
},
"message": "A11y: The value 'webauthn' is not supported by the attribute 'autocomplete' on element <input type=\"text\">",
"start": {
"column": 19,
"line": 21
}
}
]

@ -41,4 +41,8 @@
<!-- Tested header/footer not nested in section/article -->
<header role="banner"></header>
<footer role="contentinfo"></footer>
<footer role="contentinfo"></footer>
<!-- Allowed -->
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
<menu role="menu" />

@ -1,8 +1,8 @@
[
{
"code": "invalid-rest-eachblock-binding",
"message": "...rest operator will create a new object and binding propagation with original object will not work",
"start": { "line": 8, "column": 24 },
"message": "The rest operator (...) will create a new object and binding 'rest' with the original object will not work",
"start": { "line": 8, "column": 27 },
"end": { "line": 8, "column": 31 }
}
]

@ -1,8 +1,8 @@
[
{
"code": "invalid-rest-eachblock-binding",
"message": "...rest operator will create a new object and binding propagation with original object will not work",
"start": { "line": 5, "column": 32 },
"end": { "line": 5, "column": 39 }
"message": "The rest operator (...) will create a new object and binding 'rest' with the original object will not work",
"start": { "line": 5, "column": 35 },
"end": { "line": 5, "column": 39 }
}
]

@ -0,0 +1,9 @@
<script>
const a = [[1, 2, 3, 4, 5]];
</script>
{#each a as [first, second, ...[third, ...{ length }]]}
<p>{first}, {second}, {length}</p>
<input bind:value={third} />
<input bind:value={length} />
{/each}

@ -0,0 +1,26 @@
[
{
"code": "invalid-rest-eachblock-binding",
"end": {
"column": 37,
"line": 5
},
"message": "The rest operator (...) will create a new object and binding 'third' with the original object will not work",
"start": {
"column": 32,
"line": 5
}
},
{
"code": "invalid-rest-eachblock-binding",
"end": {
"column": 50,
"line": 5
},
"message": "The rest operator (...) will create a new object and binding 'length' with the original object will not work",
"start": {
"column": 44,
"line": 5
}
}
]

@ -1,8 +1,8 @@
[
{
"code": "invalid-rest-eachblock-binding",
"message": "...rest operator will create a new object and binding propagation with original object will not work",
"start": { "line": 5, "column": 25 },
"message": "The rest operator (...) will create a new object and binding 'rest' with the original object will not work",
"start": { "line": 5, "column": 28 },
"end": { "line": 5, "column": 32 }
}
]

@ -1,6 +1,6 @@
[{
"code": "invalid-binding",
"message": "'potato' is not a valid binding on <svelte:window> — valid bindings are innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY or online",
"message": "'potato' is not a valid binding on <svelte:window> — valid bindings are innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY, devicePixelRatio or online",
"start": {
"line": 1,
"column": 15

Loading…
Cancel
Save