Merge branch 'master' into a11y-no-noninteractive-element-to-interactive-role

pull/8167/head
Nguyen Tran 4 years ago
commit 2c50f95d2d

@ -1,6 +1,6 @@
### Before submitting the PR, please make sure you do the following ### 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 - [ ] 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]`. - [ ] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [ ] This message body should clearly illustrate what problems it solves. - [ ] This message body should clearly illustrate what problems it solves.
- [ ] Ideally, include a test that fails without this PR but passes with it. - [ ] Ideally, include a test that fails without this PR but passes with it.

@ -46,7 +46,7 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
strategy: strategy:
matrix: matrix:
node-version: [8, 10, 12, 14, 16] node-version: [8, 10, 12, 14, 16, 18]
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

@ -2,14 +2,36 @@
## Unreleased ## Unreleased
* Add a11y warnings:
* `aria-activedescendant-has-tabindex`: elements with `aria-activedescendant` need to have a `tabindex` ([#8172](https://github.com/sveltejs/svelte/pull/8172))
*
* Omit a11y warning on `<video>` tags with `aria-hidden="true"` ([#7874](https://github.com/sveltejs/svelte/issues/7874))
* Omit a11y "no child content" warning on elements with `aria-label` ([#8299](https://github.com/sveltejs/svelte/pull/8299))
* Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289))
* `trusted-types` CSP compatibility for Web Components ([#8134](https://github.com/sveltejs/svelte/issues/8134))
* Add `data-sveltekit-replacestate` and `data-sveltekit-keepfocus` attribute typings ([#8281](https://github.com/sveltejs/svelte/issues/8281))
* Don't throw when calling `unsubscribe` twice ([#8186](https://github.com/sveltejs/svelte/pull/8186))
* Detect unused empty attribute CSS selectors ([#8042](https://github.com/sveltejs/svelte/issues/8042))
* Simpler output for reactive statements if dependencies are all static ([#7942](https://github.com/sveltejs/svelte/pull/7942))
* Flush remaining `afterUpdate` calls before `onDestroy` ([#7476](https://github.com/sveltejs/svelte/issues/7476))
* Check value equality for `input` types `url` and `search` ([#7027](https://github.com/sveltejs/svelte/issues/7027))
* Compute node dimensions directly before crossfading ([#4111](https://github.com/sveltejs/svelte/issues/4111))
* Add `readonly` method to convert `writable` store to readonly ([#6518](https://github.com/sveltejs/svelte/pull/6518))
* Ensure `bind:offsetHeight` updates initially ([#4233](https://github.com/sveltejs/svelte/issues/4233))
* Better handling of `inert` attribute ([#7500](https://github.com/sveltejs/svelte/issues/7500))
## 3.55.1
* Fix `draw` transition with delay showing a dot at the beginning of the path ([#6816](https://github.com/sveltejs/svelte/issues/6816))
* Fix infinity runtime call stack when propagating bindings ([#7032](https://github.com/sveltejs/svelte/issues/7032))
* Fix static `<svelte:element>` optimization in production mode ([#7937](https://github.com/sveltejs/svelte/issues/7937))
* Fix `svelte-ignore` comment breaking named slot ([#8075](https://github.com/sveltejs/svelte/issues/8075))
* Revert change to prevent running init binding unnecessarily ([#8103](https://github.com/sveltejs/svelte/issues/8103))
* Fix adding duplicate event listeners with `<svelte:element on:event>` ([#8129](https://github.com/sveltejs/svelte/issues/8129))
* Improve detection of promises that are also functions ([#8162](https://github.com/sveltejs/svelte/pull/8162))
* Avoid mutating spread component props during SSR ([#8171](https://github.com/sveltejs/svelte/issues/8171))
* Add missing typing for global `part` attribute ([#8181](https://github.com/sveltejs/svelte/issues/8181))
* Add missing `submitter` property to `on:submit` event type * Add missing `submitter` property to `on:submit` event type
* Fix static `<svelte:element>` optimization in production mode ([#7938](https://github.com/sveltejs/svelte/pull/7938))
* Fix `draw` transition with delay showing a dot at the beginning of transition ([#6816](https://github.com/sveltejs/svelte/issues/6816))
* Fix `svelte-ignore` comment breaks named slot ([#8075](https://github.com/sveltejs/svelte/issues/8075))
* Fix duplicate event listeners for `<svelte:element on:event>` ([#8129](https://github.com/sveltejs/svelte/issues/8129))
* Fix max call stack exceeded ([#7032](https://github.com/sveltejs/svelte/issues/7032))
* Revert to prevent running init binding unnecessarily ([#8103](https://github.com/sveltejs/svelte/issues/8103))
* Improve `is_promise` logic ([#8162](https://github.com/sveltejs/svelte/pull/8162))
## 3.55.0 ## 3.55.0
@ -1036,7 +1058,7 @@ Also:
## 3.5.1 ## 3.5.1
* Accommodate webpack idiosyncracies * Accommodate webpack idiosyncrasies
## 3.5.0 ## 3.5.0

@ -1,4 +1,4 @@
Copyright (c) 2016-22 [these people](https://github.com/sveltejs/svelte/graphs/contributors) Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

@ -478,6 +478,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
hidden?: boolean | undefined | null; hidden?: boolean | undefined | null;
id?: string | undefined | null; id?: string | undefined | null;
lang?: string | undefined | null; lang?: string | undefined | null;
part?: string | undefined | null;
placeholder?: string | undefined | null; placeholder?: string | undefined | null;
slot?: string | undefined | null; slot?: string | undefined | null;
spellcheck?: Booleanish | undefined | null; spellcheck?: Booleanish | undefined | null;
@ -539,10 +540,12 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
'bind:textContent'?: string | undefined | null; 'bind:textContent'?: string | undefined | null;
// SvelteKit // SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null; 'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;
'data-sveltekit-preload-code'?: true | '' | 'eager' | 'viewport' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-preload-code'?: true | '' | 'eager' | 'viewport' | 'hover' | 'tap' | 'off' | undefined | null;
'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
'data-sveltekit-reload'?: true | '' | 'off' | undefined | null; 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
} }
export type HTMLAttributeAnchorTarget = export type HTMLAttributeAnchorTarget =

42
package-lock.json generated

@ -1,15 +1,16 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.55.0", "version": "3.55.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "svelte", "name": "svelte",
"version": "3.55.0", "version": "3.55.1",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^0.3.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
"@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1", "@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^11.2.1",
@ -44,7 +45,6 @@
"rollup": "^1.27.14", "rollup": "^1.27.14",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^3.7.5", "typescript": "^3.7.5",
@ -162,6 +162,12 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -3410,9 +3416,9 @@
"dev": true "dev": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@ -4138,9 +4144,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.5.2", "version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -5373,6 +5379,12 @@
"integrity": "sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA==", "integrity": "sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA==",
"dev": true "dev": true
}, },
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -7854,9 +7866,9 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@ -8432,9 +8444,9 @@
} }
}, },
"qs": { "qs": {
"version": "6.5.2", "version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"dev": true "dev": true
}, },
"queue-microtask": { "queue-microtask": {

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.55.0", "version": "3.55.1",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",
@ -120,6 +120,7 @@
"homepage": "https://svelte.dev", "homepage": "https://svelte.dev",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^0.3.0",
"@jridgewell/sourcemap-codec": "^1.4.14",
"@rollup/plugin-commonjs": "^11.0.0", "@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1", "@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-node-resolve": "^11.2.1",
@ -154,7 +155,6 @@
"rollup": "^1.27.14", "rollup": "^1.27.14",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^3.7.5", "typescript": "^3.7.5",

@ -147,7 +147,7 @@ Any top-level statement (i.e. not inside a block or a function) can be made reac
```sv ```sv
<script> <script>
export let title; export let title;
export let person export let person;
// this will update `document.title` whenever // this will update `document.title` whenever
// the `title` prop changes // the `title` prop changes

@ -206,6 +206,7 @@ Additional conditions can be added with `{:else if expression}`, optionally endi
{/if} {/if}
``` ```
(Blocks don't have to wrap elements, they can also wrap text within elements!)
### {#each ...} ### {#each ...}
@ -663,7 +664,7 @@ A `<select>` value binding corresponds to the `value` property on the selected `
--- ---
A `<select multiple>` element behaves similarly to a checkbox group. A `<select multiple>` element behaves similarly to a checkbox group. The bound variable is an array with an entry corresponding to the `value` property of each selected `<option>`.
```sv ```sv
<select multiple bind:value={fillings}> <select multiple bind:value={fillings}>
@ -750,6 +751,22 @@ Videos additionally have readonly `videoWidth` and `videoHeight` bindings.
></video> ></video>
``` ```
##### Image element bindings
---
Image elements (`<img>`) have two readonly bindings:
* `naturalWidth` (readonly) — the original width of the image, available after the image has loaded
* `naturalHeight` (readonly) — the original height of the image, available after the image has loaded
```sv
<img
bind:naturalWidth
bind:naturalHeight
></img>
```
##### Block-level element bindings ##### Block-level element bindings
--- ---
@ -1024,7 +1041,7 @@ Like actions, transitions can have parameters.
Transitions can use custom functions. If the returned object has a `css` function, Svelte will create a CSS animation that plays on the element. Transitions can use custom functions. If the returned object has a `css` function, Svelte will create a CSS animation that plays on the element.
The `t` argument passed to `css` is a value between `0` and `1` after the `easing` function has been applied. *In* transitions run from `0` to `1`, *out* transitions run from `1` to `0` in other words `1` is the element's natural state, as though no transition had been applied. The `u` argument is equal to `1 - t`. The `t` argument passed to `css` is a value between `0` and `1` after the `easing` function has been applied. *In* transitions run from `0` to `1`, *out* transitions run from `1` to `0` in other words, `1` is the element's natural state, as though no transition had been applied. The `u` argument is equal to `1 - t`.
The function is called repeatedly *before* the transition begins, with different `t` and `u` arguments. The function is called repeatedly *before* the transition begins, with different `t` and `u` arguments.
@ -1311,9 +1328,7 @@ A custom animation function can also return a `tick` function, which is called *
duration: Math.sqrt(d) * 120, duration: Math.sqrt(d) * 120,
easing: cubicOut, easing: cubicOut,
tick: (t, u) => tick: (t, u) =>
Object.assign(node.style, { Object.assign(node.style, { color: t > 0.5 ? 'Pink' : 'Blue' })
color: t > 0.5 ? 'Pink' : 'Blue'
});
}; };
} }
</script> </script>
@ -1415,7 +1430,7 @@ Svelte's CSS Variables support allows for easily themeable components:
--- ---
So you can set a high level theme color: So you can set a high-level theme color:
```css ```css
/* global.css */ /* global.css */
@ -1502,6 +1517,8 @@ The content is exposed in the child component using the `<slot>` element, which
</Widget> </Widget>
``` ```
Note: If you want to render regular `<slot>` element, You can use `<svelte:element this="slot" />`.
#### `<slot name="`*name*`">` #### `<slot name="`*name*`">`
--- ---
@ -1575,7 +1592,7 @@ Note that explicitly passing in an empty named slot will add that slot's name to
--- ---
Slots can be rendered zero or more times, and can pass values *back* to the parent using props. The parent exposes the values to the slot template using the `let:` directive. Slots can be rendered zero or more times and can pass values *back* to the parent using props. The parent exposes the values to the slot template using the `let:` directive.
The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`. The usual shorthand rules apply — `let:item` is equivalent to `let:item={item}`, and `<slot {item}>` is equivalent to `<slot item={item}>`.
@ -1666,11 +1683,11 @@ If `this` is falsy, no component is rendered.
The `<svelte:element>` element lets you render an element of a dynamically specified type. This is useful for example when displaying rich text content from a CMS. Any properties and event listeners present will be applied to the element. The `<svelte:element>` element lets you render an element of a dynamically specified type. This is useful for example when displaying rich text content from a CMS. Any properties and event listeners present will be applied to the element.
The only supported binding is `bind:this`, since the element type specific bindings that Svelte does at build time (e.g. `bind:value` for input elements) do not work with a dynamic tag type. The only supported binding is `bind:this`, since the element type-specific bindings that Svelte does at build time (e.g. `bind:value` for input elements) do not work with a dynamic tag type.
If `this` has a nullish value, the element and its children will not be rendered. If `this` has a nullish value, the element and its children will not be rendered.
If `this` is the name of a void tag (e.g., `br`) and `<svelte:element>` has child elements, a runtime error will be thrown in development mode. If `this` is the name of a [void element](https://developer.mozilla.org/en-US/docs/Glossary/Void_element) (e.g., `br`) and `<svelte:element>` has child elements, a runtime error will be thrown in development mode.
```sv ```sv
<script> <script>
@ -1716,7 +1733,7 @@ You can also bind to the following properties:
* `outerHeight` * `outerHeight`
* `scrollX` * `scrollX`
* `scrollY` * `scrollY`
* `online` — an alias for window.navigator.onLine * `online` — an alias for `window.navigator.onLine`
All except `scrollX` and `scrollY` are readonly. All except `scrollX` and `scrollY` are readonly.

@ -452,6 +452,29 @@ import { get } from 'svelte/store';
const value = get(store); const value = get(store);
``` ```
#### `readonly`
```js
readableStore = readonly(writableStore);
```
---
This simple helper function makes a store readonly. You can still subscribe to the changes from the original one using this new readable store.
```js
import { readonly } from 'svelte/store';
const writableStore = writable(1);
const readableStore = readonly(writableStore);
readableStore.subscribe(console.log);
writableStore.set(2); // console: 2
readableStore.set(2); // ERROR
```
### `svelte/motion` ### `svelte/motion`
@ -698,7 +721,7 @@ out:fly={params}
--- ---
Animates the x and y positions and the opacity of an element. `in` transitions animate from an element's current (default) values to the provided values, passed as parameters. `out` transitions animate from the provided values to an element's default values. Animates the x and y positions and the opacity of an element. `in` transitions animate from the provided values, passed as parameters to the element's default values. `out` transitions animate from the element's default values to the provided values.
`fly` accepts the following parameters: `fly` accepts the following parameters:

@ -19,6 +19,18 @@ Enforce no `accesskey` on element. Access keys are HTML attributes that allow we
--- ---
### `a11y-aria-activedescendant-has-tabindex`
An element with `aria-activedescendant` must be tabbable, so it must either have an inherent `tabindex` or declare `tabindex` as an attribute.
```sv
<!-- A11y: Elements with attribute aria-activedescendant should have tabindex value -->
<div aria-activedescendant="some-id" />
```
---
### `a11y-aria-attributes` ### `a11y-aria-attributes`
Certain reserved DOM elements do not support ARIA roles, states and properties. This is often because they are not visible, for example `meta`, `html`, `script`, `style`. This rule enforces that these DOM elements do not contain the `aria-*` props. Certain reserved DOM elements do not support ARIA roles, states and properties. This is often because they are not visible, for example `meta`, `html`, `script`, `style`. This rule enforces that these DOM elements do not contain the `aria-*` props.
@ -307,6 +319,20 @@ Elements with ARIA roles must have all required attributes for that role.
--- ---
### `a11y-role-supports-aria-props`
Elements with explicit or implicit roles defined contain only `aria-*` properties supported by that role.
```sv
<!-- A11y: The attribute 'aria-multiline' is not supported by the role 'link'. -->
<div role="link" aria-multiline />
<!-- A11y: The attribute 'aria-required' is not supported by the role 'listitem'. This role is implicit on the element <li>. -->
<li aria-required />
```
---
### `a11y-structure` ### `a11y-structure`
Enforce that certain DOM elements have the correct structure. Enforce that certain DOM elements have the correct structure.

@ -4,12 +4,11 @@
let showModal = false; let showModal = false;
</script> </script>
<button on:click="{() => showModal = true}"> <button on:click={() => (showModal = true)}>
show modal show modal
</button> </button>
{#if showModal} <Modal bind:showModal>
<Modal on:close="{() => showModal = false}">
<h2 slot="header"> <h2 slot="header">
modal modal
<small><em>adjective</em> mod·al \ˈmō-dəl\</small> <small><em>adjective</em> mod·al \ˈmō-dəl\</small>
@ -17,7 +16,9 @@
<ol class="definition-list"> <ol class="definition-list">
<li>of or relating to modality in logic</li> <li>of or relating to modality in logic</li>
<li>containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy</li> <li>
containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy
</li>
<li>of or relating to a musical mode</li> <li>of or relating to a musical mode</li>
<li>of or relating to structure as opposed to substance</li> <li>of or relating to structure as opposed to substance</li>
<li>of, relating to, or constituting a grammatical form or category characteristically indicating predication</li> <li>of, relating to, or constituting a grammatical form or category characteristically indicating predication</li>
@ -25,5 +26,4 @@
</ol> </ol>
<a href="https://www.merriam-webster.com/dictionary/modal">merriam-webster.com</a> <a href="https://www.merriam-webster.com/dictionary/modal">merriam-webster.com</a>
</Modal> </Modal>
{/if}

@ -1,80 +1,62 @@
<script> <script>
import { createEventDispatcher, onDestroy } from 'svelte'; export let showModal; // boolean
const dispatch = createEventDispatcher(); let dialog; // HTMLDialogElement
const close = () => dispatch('close');
let modal; $: if (dialog && showModal) dialog.showModal();
const handle_keydown = e => {
if (e.key === 'Escape') {
close();
return;
}
if (e.key === 'Tab') {
// trap focus
const nodes = modal.querySelectorAll('*');
const tabbable = Array.from(nodes).filter(n => n.tabIndex >= 0);
let index = tabbable.indexOf(document.activeElement);
if (index === -1 && e.shiftKey) index = 0;
index += tabbable.length + (e.shiftKey ? -1 : 1);
index %= tabbable.length;
tabbable[index].focus();
e.preventDefault();
}
};
const previously_focused = typeof document !== 'undefined' && document.activeElement;
if (previously_focused) {
onDestroy(() => {
previously_focused.focus();
});
}
</script> </script>
<svelte:window on:keydown={handle_keydown}/> <!-- svelte-ignore a11y-click-events-have-key-events -->
<dialog
<div class="modal-background" on:click={close}></div> bind:this={dialog}
on:close={() => (showModal = false)}
<div class="modal" role="dialog" aria-modal="true" bind:this={modal}> on:click|self={() => dialog.close()}
<slot name="header"></slot> >
<hr> <div on:click|stopPropagation>
<slot></slot> <slot name="header" />
<hr> <hr />
<slot />
<hr />
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
<button autofocus on:click={close}>close modal</button> <button autofocus on:click={() => dialog.close()}>close modal</button>
</div> </div>
</dialog>
<style> <style>
.modal-background { dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3);
}
.modal {
position: absolute;
left: 50%;
top: 50%;
width: calc(100vw - 4em);
max-width: 32em; max-width: 32em;
max-height: calc(100vh - 4em);
overflow: auto;
transform: translate(-50%,-50%);
padding: 1em;
border-radius: 0.2em; border-radius: 0.2em;
background: white; border: none;
padding: 0;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.3);
}
dialog > div {
padding: 1em;
}
dialog[open] {
animation: zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes zoom {
from {
transform: scale(0.95);
}
to {
transform: scale(1);
}
}
dialog[open]::backdrop {
animation: fade 0.2s ease-out;
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }
button { button {
display: block; display: block;
} }

@ -2,12 +2,20 @@
question: How do I test Svelte apps? question: How do I test Svelte apps?
--- ---
We recommend trying to separate your view logic from your business logic. Data transformation or cross component state management is best kept outside of Svelte components. You can test those parts like you would test any JavaScript functionality that way. When it comes to testing the components, it is best to test the logic of the component and remember that the Svelte library has its own tests and you do not need to test implementation details provided by Svelte. How your application is structured and where logic is defined will determine the best way to ensure it is properly tested. It is important to note that not all logic belongs within a component - this includes concerns such as data transformation, cross-component state management, and logging, among others. Remember that the Svelte library has its own test suite, so you do not need to write tests to validate implementation details provided by Svelte.
There are a few approaches that people take when testing, but it generally involves compiling the component and mounting it to something and then performing the tests. You essentially need to create a bundle for each component you're testing (since svelte is a compiler and not a normal library) and then mount them. You can mount to a JSDOM instance. Or you can use a real browser powered by a library like Playwright, Puppeteer, WebdriverIO or Cypress. A Svelte application will typically have three different types of tests: Unit, Component, and End-to-End (E2E).
Some resources for getting started with unit testing: *Unit Tests*: Focus on testing business logic in isolation. Often this is validating individual functions and edge cases. By minimizing the surface area of these tests they can be kept lean and fast, and by extracting as much logic as possible from your Svelte components more of your application can be covered using them. When creating a new SvelteKit project, you will be asked whether you would like to setup [Vitest](https://vitest.dev/) for unit testing. There are a number of other test runners that could be used as well.
*Component Tests*: Validating that a Svelte component mounts and interacts as expected throughout its lifecycle requires a tool that provides a Document Object Model (DOM). Components can be compiled (since Svelte is a compiler and not a normal library) and mounted to allow asserting against element structure, listeners, state, and all the other capabilities provided by a Svelte component. Tools for component testing range from an in-memory implementation like jsdom paired with a test runner like [Vitest](https://vitest.dev/) to solutions that leverage an actual browser to provide a visual testing capability such as [Playwright](https://playwright.dev/docs/test-components) or [Cypress](https://www.cypress.io/).
*End-to-End Tests*: To ensure your users are able to interact with your application it is necessary to test it as a whole in a manner as close to production as possible. This is done by writing end-to-end (E2E) tests which load and interact with a deployed version of your application in order to simulate how the user will interact with your application. When creating a new SvelteKit project, you will be asked whether you would like to setup [Playwright](https://playwright.dev/) for end-to-end testing. There are many other E2E test libraries available for use as well.
Some resources for getting started with testing:
- [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/) - [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/)
- [Svelte Component Testing in Cypress](https://docs.cypress.io/guides/component-testing/svelte/overview)
- [Example using vitest](https://github.com/vitest-dev/vitest/tree/main/examples/svelte) - [Example using vitest](https://github.com/vitest-dev/vitest/tree/main/examples/svelte)
- [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte) - [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte)
- [Component testing in real browser](https://webdriver.io/docs/component-testing/svelte) - [Test Svelte components using Vitest & Playwright](https://davipon.hashnode.dev/test-svelte-component-using-vitest-playwright)
- [Component testing with WebdriverIO](https://webdriver.io/docs/component-testing/svelte)

@ -23,7 +23,7 @@ function addNumber() {
} }
``` ```
The same rule applies to array methods such as `pop`, `shift`, and `splice` and to objects methods such as `Map.set`, `Set.add`, etc. The same rule applies to array methods such as `pop`, `shift`, and `splice` and to object methods such as `Map.set`, `Set.add`, etc.
Assignments to *properties* of arrays and objects — e.g. `obj.foo += 1` or `array[i] = x` — work the same way as assignments to the values themselves. Assignments to *properties* of arrays and objects — e.g. `obj.foo += 1` or `array[i] = x` — work the same way as assignments to the values themselves.

@ -1,4 +1,6 @@
<script> <script>
import { onDestroy } from 'svelte';
const emojis = { const emojis = {
apple: "🍎", apple: "🍎",
banana: "🍌", banana: "🍌",
@ -12,6 +14,11 @@
// ...but the "emoji" variable is fixed upon initialisation of the component // ...but the "emoji" variable is fixed upon initialisation of the component
const emoji = emojis[name]; const emoji = emojis[name];
// observe in the console which entry is removed
onDestroy(() => {
console.log('thing destroyed: ' + name)
});
</script> </script>
<p> <p>

@ -1,4 +1,6 @@
<script> <script>
import { onDestroy } from 'svelte';
const emojis = { const emojis = {
apple: "🍎", apple: "🍎",
banana: "🍌", banana: "🍌",
@ -12,6 +14,11 @@
// ...but the "emoji" variable is fixed upon initialisation of the component // ...but the "emoji" variable is fixed upon initialisation of the component
const emoji = emojis[name]; const emoji = emojis[name];
// observe in the console which entry is removed
onDestroy(() => {
console.log('thing destroyed: ' + name)
});
</script> </script>
<p> <p>

@ -1,8 +1,7 @@
--- ---
title: Textarea inputs title: Textarea inputs
--- ---
The `<textarea>` element behaves similarly to a text input in Svelte — use `bind:value` to create a two-way binding between the `<textarea>` content and the `value` variable:
The `<textarea>` element behaves similarly to a text input in Svelte — use `bind:value`:
```html ```html
<textarea bind:value={value}></textarea> <textarea bind:value={value}></textarea>

@ -4,21 +4,23 @@
export let todo; export let todo;
let div; let button;
afterUpdate(() => { afterUpdate(() => {
flash(div); flash(button);
}); });
</script> </script>
<!-- the text will flash red whenever <!-- the text will flash red whenever
the `todo` object changes --> the `todo` object changes -->
<div bind:this={div} on:click> <button bind:this={button} type="button" on:click>
{todo.done ? '👍': ''} {todo.text} {todo.done ? '👍': ''} {todo.text}
</div> </button>
<style> <style>
div { button {
all: unset;
display: block;
cursor: pointer; cursor: pointer;
line-height: 1.5; line-height: 1.5;
} }

@ -6,21 +6,23 @@
export let todo; export let todo;
let div; let button;
afterUpdate(() => { afterUpdate(() => {
flash(div); flash(button);
}); });
</script> </script>
<!-- the text will flash red whenever <!-- the text will flash red whenever
the `todo` object changes --> the `todo` object changes -->
<div bind:this={div} on:click> <button bind:this={button} type="button" on:click>
{todo.done ? '👍': ''} {todo.text} {todo.done ? '👍': ''} {todo.text}
</div> </button>
<style> <style>
div { button {
all: unset;
display: block;
cursor: pointer; cursor: pointer;
line-height: 1.5; line-height: 1.5;
} }

@ -1,3 +0,0 @@
{
"title": "Debugging"
}

@ -12,4 +12,4 @@ In Svelte, you do this with the special `{@html ...}` tag:
<p>{@html string}</p> <p>{@html string}</p>
``` ```
> Svelte doesn't perform any sanitization of the expression inside `{@html ...}` before it gets inserted into the DOM. In other words, if you use this feature it's critical that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks. > **Warning!** Svelte doesn't perform any sanitization of the expression inside `{@html ...}` before it gets inserted into the DOM. In other words, if you use this feature it's **critical** that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks.

@ -0,0 +1,3 @@
{
"title": "Special tags"
}

@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors'; import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap'; import check_enable_sourcemap from './utils/check_enable_sourcemap';
import is_dynamic from './render_dom/wrappers/shared/is_dynamic';
interface ComponentOptions { interface ComponentOptions {
namespace?: string; namespace?: string;
@ -793,20 +794,31 @@ export default class Component {
} }
let deep = false; let deep = false;
let names: string[] | undefined; let names: string[] = [];
if (node.type === 'AssignmentExpression') { if (node.type === 'AssignmentExpression') {
if (node.left.type === 'ArrayPattern') {
walk(node.left, {
enter(node: Node, parent: Node) {
if (node.type === 'Identifier' &&
parent.type !== 'MemberExpression' &&
(parent.type !== 'AssignmentPattern' || parent.right !== node)) {
names.push(node.name);
}
}
});
} else {
deep = node.left.type === 'MemberExpression'; deep = node.left.type === 'MemberExpression';
names = deep names = deep
? [get_object(node.left).name] ? [get_object(node.left).name]
: extract_names(node.left); : extract_names(node.left);
}
} else if (node.type === 'UpdateExpression') { } else if (node.type === 'UpdateExpression') {
deep = node.argument.type === 'MemberExpression'; deep = node.argument.type === 'MemberExpression';
const { name } = get_object(node.argument); const { name } = get_object(node.argument);
names = [name]; names.push(name);
} }
if (names.length > 0) {
if (names) {
names.forEach(name => { names.forEach(name => {
let current_scope = scope; let current_scope = scope;
let declaration; let declaration;
@ -1380,12 +1392,11 @@ export default class Component {
module_dependencies.add(name); module_dependencies.add(name);
} }
} }
const is_writable_or_mutated =
variable && (variable.writable || variable.mutated);
if ( if (
should_add_as_dependency && should_add_as_dependency &&
(!owner || owner === component.instance_scope) && (!owner || owner === component.instance_scope) &&
(name[0] === '$' || is_writable_or_mutated) (name[0] === '$' || variable)
) { ) {
dependencies.add(name); dependencies.add(name);
} }
@ -1409,6 +1420,19 @@ export default class Component {
const { expression } = node.body as ExpressionStatement; const { expression } = node.body as ExpressionStatement;
const declaration = expression && (expression as AssignmentExpression).left; const declaration = expression && (expression as AssignmentExpression).left;
const is_dependency_static = Array.from(dependencies).every(
dependency => dependency !== '$$props' && dependency !== '$$restProps' && !is_dynamic(this.var_lookup.get(dependency))
);
if (is_dependency_static) {
assignees.forEach(assignee => {
const variable = component.var_lookup.get(assignee);
if (variable) {
variable.is_reactive_static = true;
}
});
}
unsorted_reactive_declarations.push({ unsorted_reactive_declarations.push({
assignees, assignees,
dependencies, dependencies,

@ -44,7 +44,7 @@ export default {
code: 'invalid-binding', code: 'invalid-binding',
message: 'Cannot bind to a variable declared with {@const ...}' message: 'Cannot bind to a variable declared with {@const ...}'
}, },
invalid_binding_writibale: { invalid_binding_writable: {
code: 'invalid-binding', code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable' message: 'Cannot bind to a variable which is not writable'
}, },

@ -127,6 +127,17 @@ export default {
code: 'a11y-role-has-required-aria-props', code: 'a11y-role-has-required-aria-props',
message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}` message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}`
}), }),
a11y_role_supports_aria_props: (attribute: string, role: string, is_implicit: boolean, name: string) => {
let message = `The attribute '${attribute}' is not supported by the role '${role}'.`;
if (is_implicit) {
message += ` This role is implicit on the element <${name}>.`;
}
return {
code: 'a11y-role-supports-aria-props',
message: `A11y: ${message}`
};
},
a11y_accesskey: { a11y_accesskey: {
code: 'a11y-accesskey', code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey' message: 'A11y: Avoid using accesskey'
@ -191,6 +202,10 @@ export default {
code: 'a11y-no-noninteractive-tabindex', code: 'a11y-no-noninteractive-tabindex',
message: 'A11y: noninteractive element cannot have nonnegative tabIndex value' message: 'A11y: noninteractive element cannot have nonnegative tabIndex value'
}, },
a11y_aria_activedescendant_has_tabindex: {
code: 'a11y-aria-activedescendant-has-tabindex',
message: 'A11y: Elements with attribute aria-activedescendant should have tabindex value'
},
redundant_event_modifier_for_touch: { redundant_event_modifier_for_touch: {
code: 'redundant-event-modifier', code: 'redundant-event-modifier',
message: 'Touch event handlers that don\'t use the \'event\' object are passive by default' message: 'Touch event handlers that don\'t use the \'event\' object are passive by default'

@ -350,7 +350,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string,
const attr = node.attributes.find((attr: CssNode) => attr.name === name); const attr = node.attributes.find((attr: CssNode) => attr.name === name);
if (!attr) return false; if (!attr) return false;
if (attr.is_true) return operator === null; if (attr.is_true) return operator === null;
if (!expected_value) return true; if (expected_value == null) return true;
if (attr.chunks.length === 1) { if (attr.chunks.length === 1) {
const value = attr.chunks[0]; const value = attr.chunks[0];

@ -22,7 +22,9 @@ const read_only_media_attributes = new Set([
'seeking', 'seeking',
'ended', 'ended',
'videoHeight', 'videoHeight',
'videoWidth' 'videoWidth',
'naturalWidth',
'naturalHeight'
]); ]);
export default class Binding extends Node { export default class Binding extends Node {
@ -80,7 +82,7 @@ export default class Binding extends Node {
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true; variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) { if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node as any, compiler_errors.invalid_binding_writibale); component.error(this.expression.node as any, compiler_errors.invalid_binding_writable);
return; return;
} }
} }

@ -81,11 +81,15 @@ const a11y_nested_implicit_semantics = new Map([
const a11y_implicit_semantics = new Map([ const a11y_implicit_semantics = new Map([
['a', 'link'], ['a', 'link'],
['area', 'link'],
['article', 'article'],
['aside', 'complementary'], ['aside', 'complementary'],
['body', 'document'], ['body', 'document'],
['button', 'button'],
['datalist', 'listbox'], ['datalist', 'listbox'],
['dd', 'definition'], ['dd', 'definition'],
['dfn', 'term'], ['dfn', 'term'],
['dialog', 'dialog'],
['details', 'group'], ['details', 'group'],
['dt', 'term'], ['dt', 'term'],
['fieldset', 'group'], ['fieldset', 'group'],
@ -97,10 +101,14 @@ const a11y_implicit_semantics = new Map([
['h5', 'heading'], ['h5', 'heading'],
['h6', 'heading'], ['h6', 'heading'],
['hr', 'separator'], ['hr', 'separator'],
['img', 'img'],
['li', 'listitem'], ['li', 'listitem'],
['link', 'link'],
['menu', 'list'], ['menu', 'list'],
['meter', 'progressbar'],
['nav', 'navigation'], ['nav', 'navigation'],
['ol', 'list'], ['ol', 'list'],
['option', 'option'],
['optgroup', 'group'], ['optgroup', 'group'],
['output', 'status'], ['output', 'status'],
['progress', 'progressbar'], ['progress', 'progressbar'],
@ -114,6 +122,61 @@ const a11y_implicit_semantics = new Map([
['ul', 'list'] ['ul', 'list']
]); ]);
const menuitem_type_to_implicit_role = new Map([
['command', 'menuitem'],
['checkbox', 'menuitemcheckbox'],
['radio', 'menuitemradio']
]);
const input_type_to_implicit_role = new Map([
['button', 'button'],
['image', 'button'],
['reset', 'button'],
['submit', 'button'],
['checkbox', 'checkbox'],
['radio', 'radio'],
['range', 'slider'],
['number', 'spinbutton'],
['email', 'textbox'],
['search', 'searchbox'],
['tel', 'textbox'],
['text', 'textbox'],
['url', 'textbox']
]);
const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']);
function input_implicit_role(attribute_map: Map<string, Attribute>) {
const type_attribute = attribute_map.get('type');
if (!type_attribute || !type_attribute.is_static) return;
const type = type_attribute.get_static_value() as string;
const list_attribute_exists = attribute_map.has('list');
if (list_attribute_exists && combobox_if_list.has(type)) {
return 'combobox';
}
return input_type_to_implicit_role.get(type);
}
function menuitem_implicit_role(attribute_map: Map<string, Attribute>) {
const type_attribute = attribute_map.get('type');
if (!type_attribute || !type_attribute.is_static) return;
const type = type_attribute.get_static_value() as string;
return menuitem_type_to_implicit_role.get(type);
}
function get_implicit_role(name: string, attribute_map: Map<string, Attribute>) : (string | undefined) {
if (name === 'menuitem') {
return menuitem_implicit_role(attribute_map);
} else if (name === 'input') {
return input_implicit_role(attribute_map);
} else {
return a11y_implicit_semantics.get(name);
}
}
const invisible_elements = new Set(['meta', 'html', 'script', 'style']); const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([ const valid_modifiers = new Set([
@ -224,6 +287,7 @@ export default class Element extends Node {
namespace: string; namespace: string;
needs_manual_style_scoping: boolean; needs_manual_style_scoping: boolean;
tag_expr: Expression; tag_expr: Expression;
contains_a11y_label: boolean;
get is_dynamic_element() { get is_dynamic_element() {
return this.name === 'svelte:element'; return this.name === 'svelte:element';
@ -483,6 +547,11 @@ export default class Element extends Node {
component.warn(attribute, compiler_warnings.a11y_incorrect_attribute_type(schema, name)); component.warn(attribute, compiler_warnings.a11y_incorrect_attribute_type(schema, name));
} }
} }
// aria-activedescendant-has-tabindex
if (name === 'aria-activedescendant' && !is_interactive_element(this.name, attribute_map) && !attribute_map.has('tabindex')) {
component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex);
}
} }
// aria-role // aria-role
@ -504,7 +573,7 @@ export default class Element extends Node {
} }
// no-redundant-roles // no-redundant-roles
const has_redundant_role = current_role === a11y_implicit_semantics.get(this.name); const has_redundant_role = current_role === get_implicit_role(this.name, attribute_map);
if (this.name === current_role || has_redundant_role) { if (this.name === current_role || has_redundant_role) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(current_role)); component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(current_role));
@ -607,6 +676,23 @@ export default class Element extends Node {
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex); component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);
} }
} }
// role-supports-aria-props
const role = attribute_map.get('role');
const role_value = (role ? role.get_static_value() : get_implicit_role(this.name, attribute_map)) as ARIARoleDefintionKey;
if (typeof role_value === 'string' && roles.has(role_value)) {
const { props } = roles.get(role_value);
const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props)));
const is_implicit = role_value && role === undefined;
attributes
.filter(prop => prop.type !== 'Spread')
.forEach(prop => {
if (invalid_aria_props.has(prop.name as ARIAProperty)) {
component.warn(prop, compiler_warnings.a11y_role_supports_aria_props(prop.name, role_value, is_implicit, this.name));
}
});
}
} }
validate_special_cases() { validate_special_cases() {
@ -628,27 +714,36 @@ export default class Element extends Node {
const id_attribute = attribute_map.get('id'); const id_attribute = attribute_map.get('id');
const name_attribute = attribute_map.get('name'); const name_attribute = attribute_map.get('name');
const target_attribute = attribute_map.get('target'); const target_attribute = attribute_map.get('target');
const aria_label_attribute = attribute_map.get('aria-label');
if (target_attribute && target_attribute.get_static_value() === '_blank' && href_attribute) { // links with target="_blank" should have noopener or noreferrer: https://developer.chrome.com/docs/lighthouse/best-practices/external-anchors-use-rel-noopener/
// modern browsers add noopener by default, so we only need to check legacy browsers
// legacy browsers don't support noopener so we only check for noreferrer there
if (component.compile_options.legacy && target_attribute && target_attribute.get_static_value() === '_blank' && href_attribute) {
const href_static_value = href_attribute.get_static_value() ? href_attribute.get_static_value().toLowerCase() : null; const href_static_value = href_attribute.get_static_value() ? href_attribute.get_static_value().toLowerCase() : null;
if (href_static_value === null || href_static_value.match(/^(https?:)?\/\//i)) { if (href_static_value === null || href_static_value.match(/^(https?:)?\/\//i)) {
const rel = attribute_map.get('rel'); const rel = attribute_map.get('rel');
if (rel == null || rel.is_static) { if (rel == null || rel.is_static) {
const rel_values = rel ? rel.get_static_value().split(regex_any_repeated_whitespaces) : []; const rel_values = rel ? rel.get_static_value().split(regex_any_repeated_whitespaces) : [];
const expected_values = ['noreferrer']; if (!rel || !rel_values.includes('noreferrer')) {
expected_values.forEach(expected_value => {
if (!rel || rel && rel_values.indexOf(expected_value) < 0) {
component.warn(this, { component.warn(this, {
code: `security-anchor-rel-${expected_value}`, code: 'security-anchor-rel-noreferrer',
message: `Security: Anchor with "target=_blank" should have rel attribute containing the value "${expected_value}"` message:
'Security: Anchor with "target=_blank" should have rel attribute containing the value "noreferrer"'
}); });
} }
});
} }
} }
} }
if (aria_label_attribute) {
const aria_value = aria_label_attribute.get_static_value();
if (aria_value != '') {
this.contains_a11y_label = true;
}
}
if (href_attribute) { if (href_attribute) {
const href_value = href_attribute.get_static_value(); const href_value = href_attribute.get_static_value();
@ -725,7 +820,10 @@ export default class Element extends Node {
} }
if (this.name === 'video') { if (this.name === 'video') {
if (attribute_map.has('muted')) { const aria_hidden_attribute = attribute_map.get('aria-hidden');
const aria_hidden_exist = aria_hidden_attribute && aria_hidden_attribute.get_static_value();
if (attribute_map.has('muted') || aria_hidden_exist === 'true') {
return; return;
} }
@ -908,6 +1006,13 @@ export default class Element extends Node {
} else if (is_void(this.name)) { } else if (is_void(this.name)) {
return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `void elements like <${this.name}>. Use a wrapper element instead`)); return component.error(binding, compiler_errors.invalid_binding_on(binding.name, `void elements like <${this.name}>. Use a wrapper element instead`));
} }
} else if (
name === 'naturalWidth' ||
name === 'naturalHeight'
) {
if (this.name !== 'img') {
return component.error(binding, compiler_errors.invalid_binding_element_with('<img>', name));
}
} else if ( } else if (
name === 'textContent' || name === 'textContent' ||
name === 'innerHTML' name === 'innerHTML'
@ -929,6 +1034,7 @@ export default class Element extends Node {
validate_content() { validate_content() {
if (!a11y_required_content.has(this.name)) return; if (!a11y_required_content.has(this.name)) return;
if (this.contains_a11y_label) return;
if ( if (
this.bindings this.bindings
.some((binding) => ['textContent', 'innerHTML'].includes(binding.name)) .some((binding) => ['textContent', 'innerHTML'].includes(binding.name))
@ -1041,14 +1147,14 @@ export default class Element extends Node {
} }
} }
const regex_starts_with_vovel = /^[aeiou]/; const regex_starts_with_vowel = /^[aeiou]/;
function should_have_attribute( function should_have_attribute(
node, node,
attributes: string[], attributes: string[],
name = node.name name = node.name
) { ) {
const article = regex_starts_with_vovel.test(attributes[0]) ? 'an' : 'a'; const article = regex_starts_with_vowel.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ? const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0]; attributes[0];

@ -390,13 +390,13 @@ export default function dom(
const resubscribable_reactive_store_unsubscribers = reactive_stores const resubscribable_reactive_store_unsubscribers = reactive_stores
.filter(store => { .filter(store => {
const variable = component.var_lookup.get(store.name.slice(1)); const variable = component.var_lookup.get(store.name.slice(1));
return variable && (variable.reassigned || variable.export_name); return variable && (variable.reassigned || variable.export_name) && !variable.is_reactive_static;
}) })
.map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`); .map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`);
if (has_definition) { if (has_definition) {
const reactive_declarations: (Node | Node[]) = []; const reactive_declarations: Node[] = [];
const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever const fixed_reactive_declarations: Array<Node | Node[]> = []; // not really 'reactive' but whatever
component.reactive_declarations.forEach(d => { component.reactive_declarations.forEach(d => {
const dependencies = Array.from(d.dependencies); const dependencies = Array.from(d.dependencies);
@ -417,6 +417,15 @@ export default function dom(
reactive_declarations.push(statement); reactive_declarations.push(statement);
} else { } else {
fixed_reactive_declarations.push(statement); fixed_reactive_declarations.push(statement);
for (const assignee of d.assignees) {
const variable = component.var_lookup.get(assignee);
if (variable && variable.subscribable) {
fixed_reactive_declarations.push(b`
${component.compile_options.dev && b`@validate_store(${assignee}, '${assignee}');`}
@component_subscribe($$self, ${assignee}, $$value => $$invalidate(${renderer.context_lookup.get('$' + assignee).index}, ${'$' + assignee} = $$value));
`);
}
}
} }
}); });
@ -430,7 +439,7 @@ export default function dom(
const name = $name.slice(1); const name = $name.slice(1);
const store = component.var_lookup.get(name); const store = component.var_lookup.get(name);
if (store && (store.reassigned || store.export_name)) { if (store && (store.reassigned || store.export_name) && !store.is_reactive_static) {
const unsubscribe = `$$unsubscribe_${name}`; const unsubscribe = `$$unsubscribe_${name}`;
const subscribe = `$$subscribe_${name}`; const subscribe = `$$subscribe_${name}`;
const i = renderer.context_lookup.get($name).index; const i = renderer.context_lookup.get($name).index;
@ -531,7 +540,10 @@ export default function dom(
constructor(options) { constructor(options) {
super(); super();
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(regex_backslashes, '\\\\')}${css_sourcemap_enabled && options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`} ${css.code && b`
const style = document.createElement('style');
style.textContent = \`${css.code.replace(regex_backslashes, '\\\\')}${css_sourcemap_enabled && options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}\`
this.shadowRoot.appendChild(style)`}
@init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty}); @init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty});

@ -19,6 +19,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names:
!variable.hoistable && !variable.hoistable &&
!variable.global && !variable.global &&
!variable.module && !variable.module &&
!variable.is_reactive_static &&
( (
variable.referenced || variable.referenced ||
variable.subscribable || variable.subscribable ||

@ -448,8 +448,10 @@ export default class EachBlockWrapper extends Wrapper {
block.chunks.mount.push(b` block.chunks.mount.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) { for (let #i = 0; #i < ${view_length}; #i += 1) {
if (${iterations}[#i]) {
${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node}); ${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node});
} }
}
`); `);
const dynamic = this.block.has_update_method; const dynamic = this.block.has_update_method;
@ -542,8 +544,10 @@ export default class EachBlockWrapper extends Wrapper {
block.chunks.mount.push(b` block.chunks.mount.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) { for (let #i = 0; #i < ${view_length}; #i += 1) {
if (${iterations}[#i]) {
${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node}); ${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node});
} }
}
`); `);
if (this.dependencies.size) { if (this.dependencies.size) {

@ -9,7 +9,7 @@ import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding'; import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree'; import { Identifier, Node } from 'estree';
import { namespaces } from '../../../../utils/namespaces'; import { namespaces } from '../../../../utils/namespaces';
import { boolean_attributes } from '../../../../../shared/boolean_attributes'; import { BooleanAttributes, boolean_attributes } from '../../../../../shared/boolean_attributes';
import { regex_double_quotes } from '../../../../utils/patterns'; import { regex_double_quotes } from '../../../../utils/patterns';
const non_textlike_input_types = new Set([ const non_textlike_input_types = new Set([
@ -324,7 +324,8 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
} }
// source: https://html.spec.whatwg.org/multipage/indices.html // source: https://html.spec.whatwg.org/multipage/indices.html
const attribute_lookup = { type AttributeMetadata = { property_name?: string, applies_to?: string[] };
const attribute_lookup: { [key in BooleanAttributes]: AttributeMetadata } & { [key in string]: AttributeMetadata } = {
allowfullscreen: { property_name: 'allowFullscreen', applies_to: ['iframe'] }, allowfullscreen: { property_name: 'allowFullscreen', applies_to: ['iframe'] },
allowpaymentrequest: { property_name: 'allowPaymentRequest', applies_to: ['iframe'] }, allowpaymentrequest: { property_name: 'allowPaymentRequest', applies_to: ['iframe'] },
async: { applies_to: ['script'] }, async: { applies_to: ['script'] },
@ -349,7 +350,9 @@ const attribute_lookup = {
formnovalidate: { property_name: 'formNoValidate', applies_to: ['button', 'input'] }, formnovalidate: { property_name: 'formNoValidate', applies_to: ['button', 'input'] },
hidden: {}, hidden: {},
indeterminate: { applies_to: ['input'] }, indeterminate: { applies_to: ['input'] },
inert: {},
ismap: { property_name: 'isMap', applies_to: ['img'] }, ismap: { property_name: 'isMap', applies_to: ['img'] },
itemscope: {},
loop: { applies_to: ['audio', 'bgsound', 'video'] }, loop: { applies_to: ['audio', 'bgsound', 'video'] },
multiple: { applies_to: ['input', 'select'] }, multiple: { applies_to: ['input', 'select'] },
muted: { applies_to: ['audio', 'video'] }, muted: { applies_to: ['audio', 'video'] },

@ -120,7 +120,9 @@ export default class BindingWrapper {
type === '' || type === '' ||
type === 'text' || type === 'text' ||
type === 'email' || type === 'email' ||
type === 'password' type === 'password' ||
type === 'search' ||
type === 'url'
) { ) {
update_conditions.push( update_conditions.push(
x`${parent.var}.${this.node.name} !== ${this.snippet}` x`${parent.var}.${this.node.name} !== ${this.snippet}`

@ -137,6 +137,10 @@ const events = [
event_names: ['toggle'], event_names: ['toggle'],
filter: (node: Element, _name: string) => filter: (node: Element, _name: string) =>
node.name === 'details' node.name === 'details'
},
{
event_names: ['load'],
filter: (_: Element, name: string) => name === 'naturalHeight' || name === 'naturalWidth'
} }
]; ];
@ -315,10 +319,16 @@ export default class ElementWrapper extends Wrapper {
const has_transitions = !!(this.node.intro || this.node.outro); const has_transitions = !!(this.node.intro || this.node.outro);
const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
const tag_will_be_removed = block.get_unique_name('tag_will_be_removed');
if (has_transitions) {
block.add_variable(tag_will_be_removed, x`false`);
}
block.chunks.update.push(b` block.chunks.update.push(b`
if (${tag}) { if (${tag}) {
if (!${previous_tag}) { if (!${previous_tag}) {
${this.var} = ${this.child_dynamic_element_block.name}(#ctx); ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);
${previous_tag} = ${tag};
${this.var}.c(); ${this.var}.c();
${has_transitions && b`@transition_in(${this.var})`} ${has_transitions && b`@transition_in(${this.var})`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
@ -327,27 +337,39 @@ export default class ElementWrapper extends Wrapper {
${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`}
${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`}
${this.var} = ${this.child_dynamic_element_block.name}(#ctx); ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);
${previous_tag} = ${tag};
${this.var}.c(); ${this.var}.c();
${has_transitions && b`if (${tag_will_be_removed}) {
${tag_will_be_removed} = false;
@transition_in(${this.var})
}`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
} else { } else {
${has_transitions && b`if (${tag_will_be_removed}) {
${tag_will_be_removed} = false;
@transition_in(${this.var})
}`}
${this.var}.p(#ctx, #dirty); ${this.var}.p(#ctx, #dirty);
} }
} else if (${previous_tag}) { } else if (${previous_tag}) {
${has_transitions ${has_transitions
? b` ? b`
${tag_will_be_removed} = true;
@group_outros(); @group_outros();
@transition_out(${this.var}, 1, 1, () => { @transition_out(${this.var}, 1, 1, () => {
${this.var} = null; ${this.var} = null;
${previous_tag} = ${tag};
${tag_will_be_removed} = false;
}); });
@check_outros(); @check_outros();
` `
: b` : b`
${this.var}.d(1); ${this.var}.d(1);
${this.var} = null; ${this.var} = null;
${previous_tag} = ${tag};
` `
} }
} }
${previous_tag} = ${tag};
`); `);
if (this.child_dynamic_element_block.has_intros) { if (this.child_dynamic_element_block.has_intros) {
@ -854,8 +876,9 @@ export default class ElementWrapper extends Wrapper {
} }
block.chunks.mount.push(b` block.chunks.mount.push(b`
(${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); 'value' in ${data} && (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value);
`); `);
block.chunks.update.push(b` 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 ${data}) (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value);
`); `);
@ -863,7 +886,9 @@ export default class ElementWrapper extends Wrapper {
const type = this.node.get_static_attribute_value('type'); const type = this.node.get_static_attribute_value('type');
if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') { if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') {
block.chunks.mount.push(b` block.chunks.mount.push(b`
if ('value' in ${data}) {
${this.var}.value = ${data}.value; ${this.var}.value = ${data}.value;
}
`); `);
block.chunks.update.push(b` block.chunks.update.push(b`
if ('value' in ${data}) { if ('value' in ${data}) {

@ -1,4 +1,4 @@
import { namespaces } from './../../../utils/namespaces'; import { namespaces } from '../../../utils/namespaces';
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import Renderer from '../Renderer'; import Renderer from '../Renderer';
import Block from '../Block'; import Block from '../Block';

@ -36,7 +36,7 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
let props; let props;
if (uses_spread) { if (uses_spread) {
props = x`@_Object.assign(${ props = x`@_Object.assign({}, ${
node.attributes node.attributes
.map(attribute => { .map(attribute => {
if (attribute.is_spread) { if (attribute.is_spread) {

@ -223,6 +223,7 @@ export interface Var {
subscribable?: boolean; subscribable?: boolean;
is_reactive_dependency?: boolean; is_reactive_dependency?: boolean;
imported?: boolean; imported?: boolean;
is_reactive_static?: boolean;
} }
export interface CssResult { export interface CssResult {

@ -132,6 +132,10 @@ export class Parser {
return this.template.slice(this.index, this.index + str.length) === str; return this.template.slice(this.index, this.index + str.length) === str;
} }
/**
* Match a regex at the current index
* @param pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
*/
match_regex(pattern: RegExp) { match_regex(pattern: RegExp) {
const match = pattern.exec(this.template.slice(this.index)); const match = pattern.exec(this.template.slice(this.index));
if (!match || match.index !== 0) return null; if (!match || match.index !== 0) return null;
@ -148,6 +152,10 @@ export class Parser {
} }
} }
/**
* Search for a regex starting at the current index and return the result if it matches
* @param pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
*/
read(pattern: RegExp) { read(pattern: RegExp) {
const result = this.match_regex(pattern); const result = this.match_regex(pattern);
if (result) this.index += result.length; if (result) this.index += result.length;

@ -6,6 +6,7 @@ import parser_errors from '../errors';
import { regex_not_newline_characters } from '../../utils/patterns'; import { regex_not_newline_characters } from '../../utils/patterns';
const regex_closing_script_tag = /<\/script\s*>/; const regex_closing_script_tag = /<\/script\s*>/;
const regex_starts_with_closing_script_tag = /^<\/script\s*>/;
function get_context(parser: Parser, attributes: any[], start: number): string { function get_context(parser: Parser, attributes: any[], start: number): string {
const context = attributes.find(attribute => attribute.name === 'context'); const context = attributes.find(attribute => attribute.name === 'context');
@ -32,7 +33,7 @@ export default function read_script(parser: Parser, start: number, attributes: N
} }
const source = parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data; const source = parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_closing_script_tag); parser.read(regex_starts_with_closing_script_tag);
let ast: Program; let ast: Program;

@ -7,6 +7,7 @@ import { Style } from '../../interfaces';
import parser_errors from '../errors'; import parser_errors from '../errors';
const regex_closing_style_tag = /<\/style\s*>/; const regex_closing_style_tag = /<\/style\s*>/;
const regex_starts_with_closing_style_tag = /^<\/style\s*>/;
export default function read_style(parser: Parser, start: number, attributes: Node[]): Style { export default function read_style(parser: Parser, start: number, attributes: Node[]): Style {
const content_start = parser.index; const content_start = parser.index;
@ -21,7 +22,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
// discard styles when css is disabled // discard styles when css is disabled
if (parser.css_mode === 'none') { if (parser.css_mode === 'none') {
parser.read(regex_closing_style_tag); parser.read(regex_starts_with_closing_style_tag);
return null; return null;
} }
@ -76,7 +77,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
} }
}); });
parser.read(regex_closing_style_tag); parser.read(regex_starts_with_closing_style_tag);
const end = parser.index; const end = parser.index;

@ -33,7 +33,7 @@ function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after:
} }
} }
const regex_whitespace_with_closing_curly_brace = /\s*}/; const regex_whitespace_with_closing_curly_brace = /^\s*}/;
export default function mustache(parser: Parser) { export default function mustache(parser: Parser) {
const start = parser.index; const start = parser.index;

@ -12,6 +12,9 @@ import { closing_tag_omitted, decode_character_references } from '../utils/html'
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
/** Invalid attribute characters if the attribute is not surrounded by quotes */
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/;
const meta_tags = new Map([ const meta_tags = new Map([
['svelte:head', 'Head'], ['svelte:head', 'Head'],
['svelte:options', 'Options'], ['svelte:options', 'Options'],
@ -293,7 +296,7 @@ function read_tag_name(parser: Parser) {
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const regex_token_ending_character = /[\s=\/>"']/; const regex_token_ending_character = /[\s=\/>"']/;
const regex_quote_characters = /["']/; const regex_starts_with_quote_characters = /^["']/;
function read_attribute(parser: Parser, unique_names: Set<string>) { function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index; const start = parser.index;
@ -368,7 +371,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
parser.allow_whitespace(); parser.allow_whitespace();
value = read_attribute_value(parser); value = read_attribute_value(parser);
end = parser.index; end = parser.index;
} else if (parser.match_regex(regex_quote_characters)) { } else if (parser.match_regex(regex_starts_with_quote_characters)) {
parser.error(parser_errors.unexpected_token('='), parser.index); parser.error(parser_errors.unexpected_token('='), parser.index);
} }
@ -475,15 +478,13 @@ function read_attribute_value(parser: Parser) {
}]; }];
} }
const regex = (
quote_mark === "'" ? /'/ :
quote_mark === '"' ? /"/ :
/(\/>|[\s"'=<>`])/
);
let value; let value;
try { try {
value = read_sequence(parser, () => !!parser.match_regex(regex), 'in attribute value'); value = read_sequence(parser, () => {
// handle common case of quote marks existing outside of regex for performance reasons
if (quote_mark) return parser.match(quote_mark);
return !!parser.match_regex(regex_starts_with_invalid_attr_value);
}, 'in attribute value');
} catch (error) { } catch (error) {
if (error.code === 'parse-error') { if (error.code === 'parse-error') {
// if the attribute value didn't close + self-closing tag // if the attribute value didn't close + self-closing tag

@ -1,4 +1,4 @@
import { decode as decode_mappings } from 'sourcemap-codec'; import { decode as decode_mappings } from '@jridgewell/sourcemap-codec';
import { Processed } from './types'; import { Processed } from './types';
/** /**

@ -1,4 +1,4 @@
import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler';
import { current_component, set_current_component } from './lifecycle'; import { current_component, set_current_component } from './lifecycle';
import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils';
import { children, detach, start_hydrating, end_hydrating } from './dom'; import { children, detach, start_hydrating, end_hydrating } from './dom';
@ -51,6 +51,8 @@ export function mount_component(component, target, anchor, customElement) {
export function destroy_component(component, detaching) { export function destroy_component(component, detaching) {
const $$ = component.$$; const $$ = component.$$;
if ($$.fragment !== null) { if ($$.fragment !== null) {
flush_render_callbacks($$.after_update);
run_all($$.on_destroy); run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching); $$.fragment && $$.fragment.d(detaching);

@ -567,8 +567,16 @@ export function select_options(select, value) {
} }
} }
function first_enabled_option(select) {
for (const option of select.options) {
if (!option.disabled) {
return option;
}
}
}
export function select_value(select) { export function select_value(select) {
const selected_option = select.querySelector(':checked') || select.options[0]; const selected_option = select.querySelector(':checked') || first_enabled_option(select);
return selected_option && selected_option.__value; return selected_option && selected_option.__value;
} }
@ -624,6 +632,10 @@ export function add_resize_listener(node: HTMLElement, fn: () => void) {
iframe.src = 'about:blank'; iframe.src = 'about:blank';
iframe.onload = () => { iframe.onload = () => {
unsubscribe = listen(iframe.contentWindow, 'resize', fn); unsubscribe = listen(iframe.contentWindow, 'resize', fn);
// make sure an initial resize event is fired _after_ the iframe is loaded (which is asynchronous)
// see https://github.com/sveltejs/svelte/issues/4233
fn();
}; };
} }

@ -5,7 +5,7 @@ export const dirty_components = [];
export const intros = { enabled: false }; export const intros = { enabled: false };
export const binding_callbacks = []; export const binding_callbacks = [];
const render_callbacks = []; let render_callbacks = [];
const flush_callbacks = []; const flush_callbacks = [];
const resolved_promise = Promise.resolve(); const resolved_promise = Promise.resolve();
@ -122,3 +122,14 @@ function update($$) {
$$.after_update.forEach(add_render_callback); $$.after_update.forEach(add_render_callback);
} }
} }
/**
* Useful for example to execute remaining `afterUpdate` callbacks before executing `destroy`.
*/
export function flush_render_callbacks(fns: Function[]): void {
const filtered = [];
const targets = [];
render_callbacks.forEach((c) => fns.indexOf(c) === -1 ? filtered.push(c) : targets.push(c));
targets.forEach((c) => c());
render_callbacks = filtered;
}

@ -98,7 +98,7 @@ export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writ
return () => { return () => {
subscribers.delete(subscriber); subscribers.delete(subscriber);
if (subscribers.size === 0) { if (subscribers.size === 0 && stop) {
stop(); stop();
stop = null; stop = null;
} }
@ -207,6 +207,17 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
}); });
} }
/**
* Takes a store and returns a new one derived from the old one that is readable.
*
* @param store - store to make readonly
*/
export function readonly<T>(store: Readable<T>): Readable<T> {
return {
subscribe: store.subscribe.bind(store)
};
}
/** /**
* Get the current value from a store by subscribing and immediately unsubscribing. * Get the current value from a store by subscribing and immediately unsubscribing.
* @param store readable * @param store readable

@ -211,7 +211,7 @@ export interface CrossfadeParams {
easing?: EasingFunction; easing?: EasingFunction;
} }
type ClientRectMap = Map<any, { rect: ClientRect }>; type ClientRectMap = Map<any, Element>;
export function crossfade({ fallback, ...defaults }: CrossfadeParams & { export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
fallback?: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig; fallback?: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig;
@ -232,13 +232,14 @@ export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
const to_receive: ClientRectMap = new Map(); const to_receive: ClientRectMap = new Map();
const to_send: ClientRectMap = new Map(); const to_send: ClientRectMap = new Map();
function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig { function crossfade(from_node: Element, node: Element, params: CrossfadeParams): TransitionConfig {
const { const {
delay = 0, delay = 0,
duration = d => Math.sqrt(d) * 30, duration = d => Math.sqrt(d) * 30,
easing = cubicOut easing = cubicOut
} = assign(assign({}, defaults), params); } = assign(assign({}, defaults), params);
const from = from_node.getBoundingClientRect();
const to = node.getBoundingClientRect(); const to = node.getBoundingClientRect();
const dx = from.left - to.left; const dx = from.left - to.left;
const dy = from.top - to.top; const dy = from.top - to.top;
@ -264,16 +265,14 @@ export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
function transition(items: ClientRectMap, counterparts: ClientRectMap, intro: boolean) { function transition(items: ClientRectMap, counterparts: ClientRectMap, intro: boolean) {
return (node: Element, params: CrossfadeParams & { key: any }) => { return (node: Element, params: CrossfadeParams & { key: any }) => {
items.set(params.key, { items.set(params.key, node);
rect: node.getBoundingClientRect()
});
return () => { return () => {
if (counterparts.has(params.key)) { if (counterparts.has(params.key)) {
const { rect } = counterparts.get(params.key); const other_node = counterparts.get(params.key);
counterparts.delete(params.key); counterparts.delete(params.key);
return crossfade(rect, node, params); return crossfade(other_node, node, params);
} }
// if the node is disappearing altogether // if the node is disappearing altogether

@ -1,5 +1,4 @@
// source: https://html.spec.whatwg.org/multipage/indices.html const _boolean_attributes = [
export const boolean_attributes = new Set([
'allowfullscreen', 'allowfullscreen',
'allowpaymentrequest', 'allowpaymentrequest',
'async', 'async',
@ -26,4 +25,12 @@ export const boolean_attributes = new Set([
'required', 'required',
'reversed', 'reversed',
'selected' 'selected'
]); ] as const;
export type BooleanAttributes = typeof _boolean_attributes[number];
/**
* List of HTML boolean attributes (e.g. `<input disabled>`).
* Source: https://html.spec.whatwg.org/multipage/indices.html
*/
export const boolean_attributes: Set<string> = new Set([..._boolean_attributes]);

@ -0,0 +1,25 @@
export default {
warnings: [{
filename: 'SvelteComponent.svelte',
code: 'css-unused-selector',
message: 'Unused CSS selector "img[alt=""]"',
start: {
character: 87,
column: 1,
line: 8
},
end: {
character: 98,
column: 12,
line: 8
},
pos: 87,
frame: `
6: }
7:
8: img[alt=""] {
^
9: border: 1px solid red;
10: }`
}]
};

@ -0,0 +1 @@
img[alt].svelte-xyz{border:1px solid green}

@ -0,0 +1 @@
<img alt="a foo" class="svelte-xyz" src="foo.jpg">

@ -0,0 +1,11 @@
<img src="foo.jpg" alt="a foo" />
<style>
img[alt] {
border: 1px solid green;
}
img[alt=""] {
border: 1px solid red;
}
</style>

@ -48,7 +48,7 @@ function create_fragment(ctx) {
t8 = text(/*$prop*/ ctx[2]); t8 = text(/*$prop*/ ctx[2]);
t9 = space(); t9 = space();
t10 = text(/*shadowedByModule*/ ctx[4]); t10 = text(/*shadowedByModule*/ ctx[4]);
add_location(p, file, 22, 0, 430); add_location(p, file, 22, 0, 431);
}, },
l: function claim(nodes) { l: function claim(nodes) {
throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option"); throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");
@ -91,7 +91,7 @@ function create_fragment(ctx) {
} }
let moduleLiveBinding; let moduleLiveBinding;
const moduleContantProps = 4; const moduleConstantProps = 4;
let moduleLet; let moduleLet;
const moduleConst = 2; const moduleConst = 2;
let shadowedByModule; let shadowedByModule;
@ -137,7 +137,7 @@ function instance($$self, $$props, $$invalidate) {
$$self.$capture_state = () => ({ $$self.$capture_state = () => ({
moduleLiveBinding, moduleLiveBinding,
moduleContantProps, moduleConstantProps,
moduleLet, moduleLet,
moduleConst, moduleConst,
shadowedByModule, shadowedByModule,
@ -197,4 +197,4 @@ class Component extends SvelteComponentDev {
} }
export default Component; export default Component;
export { moduleLiveBinding, moduleContantProps }; export { moduleLiveBinding, moduleConstantProps };

@ -1,6 +1,6 @@
<script context="module"> <script context="module">
export let moduleLiveBinding; export let moduleLiveBinding;
export const moduleContantProps = 4; export const moduleConstantProps = 4;
let moduleLet; let moduleLet;
const moduleConst = 2; const moduleConst = 2;
let shadowedByModule; let shadowedByModule;

@ -34,7 +34,9 @@ function create_fragment(ctx) {
class Component extends SvelteElement { class Component extends SvelteElement {
constructor(options) { constructor(options) {
super(); super();
this.shadowRoot.innerHTML = `<style>div{animation:foo 1s}@keyframes foo{0%{opacity:0}100%{opacity:1}}</style>`; const style = document.createElement('style');
style.textContent = `div{animation:foo 1s}@keyframes foo{0%{opacity:0}100%{opacity:1}}`;
this.shadowRoot.appendChild(style);
init( init(
this, this,

@ -114,8 +114,10 @@ function create_fragment(ctx) {
}, },
m: function mount(target, anchor) { m: function mount(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert_dev(target, t0, anchor); insert_dev(target, t0, anchor);
insert_dev(target, p, anchor); insert_dev(target, p, anchor);

@ -108,8 +108,10 @@ function create_fragment(ctx) {
}, },
m: function mount(target, anchor) { m: function mount(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert_dev(target, t0, anchor); insert_dev(target, t0, anchor);
insert_dev(target, p, anchor); insert_dev(target, p, anchor);

@ -86,8 +86,10 @@ function create_fragment(ctx) {
}, },
m: function mount(target, anchor) { m: function mount(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert_dev(target, each_1_anchor, anchor); insert_dev(target, each_1_anchor, anchor);
}, },

@ -63,8 +63,10 @@ function create_fragment(ctx) {
}, },
m(target, anchor) { m(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert(target, each_1_anchor, anchor); insert(target, each_1_anchor, anchor);
}, },

@ -63,8 +63,10 @@ function create_fragment(ctx) {
}, },
m(target, anchor) { m(target, anchor) {
for (let i = 0; i < 5; i += 1) { for (let i = 0; i < 5; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert(target, each_1_anchor, anchor); insert(target, each_1_anchor, anchor);
}, },

@ -104,8 +104,10 @@ function create_fragment(ctx) {
}, },
m(target, anchor) { m(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert(target, t0, anchor); insert(target, t0, anchor);
insert(target, p, anchor); insert(target, p, anchor);

@ -87,8 +87,10 @@ function create_fragment(ctx) {
}, },
m(target, anchor) { m(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert(target, each_1_anchor, anchor); insert(target, each_1_anchor, anchor);
}, },

@ -72,8 +72,10 @@ function create_fragment(ctx) {
}, },
m(target, anchor) { m(target, anchor) {
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks.length; i += 1) {
if (each_blocks[i]) {
each_blocks[i].m(target, anchor); each_blocks[i].m(target, anchor);
} }
}
insert(target, each_1_anchor, anchor); insert(target, each_1_anchor, anchor);
}, },

@ -0,0 +1,95 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
add_render_callback,
attr,
detach,
element,
init,
insert,
listen,
noop,
safe_not_equal,
set_data,
space,
src_url_equal,
text
} from "svelte/internal";
function create_fragment(ctx) {
let img;
let img_src_value;
let t0;
let t1;
let t2;
let t3;
let mounted;
let dispose;
return {
c() {
img = element("img");
t0 = space();
t1 = text(/*naturalWidth*/ ctx[0]);
t2 = text(" x ");
t3 = text(/*naturalHeight*/ ctx[1]);
if (!src_url_equal(img.src, img_src_value = "something.jpg")) attr(img, "src", img_src_value);
if (/*naturalWidth*/ ctx[0] === void 0 || /*naturalHeight*/ ctx[1] === void 0) add_render_callback(() => /*img_load_handler*/ ctx[2].call(img));
},
m(target, anchor) {
insert(target, img, anchor);
insert(target, t0, anchor);
insert(target, t1, anchor);
insert(target, t2, anchor);
insert(target, t3, anchor);
if (!mounted) {
dispose = listen(img, "load", /*img_load_handler*/ ctx[2]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*naturalWidth*/ 1) set_data(t1, /*naturalWidth*/ ctx[0]);
if (dirty & /*naturalHeight*/ 2) set_data(t3, /*naturalHeight*/ ctx[1]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(img);
if (detaching) detach(t0);
if (detaching) detach(t1);
if (detaching) detach(t2);
if (detaching) detach(t3);
mounted = false;
dispose();
}
};
}
function instance($$self, $$props, $$invalidate) {
let { naturalWidth = 0 } = $$props;
let { naturalHeight = 0 } = $$props;
function img_load_handler() {
naturalWidth = this.naturalWidth;
naturalHeight = this.naturalHeight;
$$invalidate(0, naturalWidth);
$$invalidate(1, naturalHeight);
}
$$self.$$set = $$props => {
if ('naturalWidth' in $$props) $$invalidate(0, naturalWidth = $$props.naturalWidth);
if ('naturalHeight' in $$props) $$invalidate(1, naturalHeight = $$props.naturalHeight);
};
return [naturalWidth, naturalHeight, img_load_handler];
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, { naturalWidth: 0, naturalHeight: 1 });
}
}
export default Component;

@ -0,0 +1,8 @@
<script>
export let naturalWidth = 0;
export let naturalHeight = 0;
</script>
<img src="something.jpg" bind:naturalWidth bind:naturalHeight>
{naturalWidth} x {naturalHeight}

@ -9,7 +9,6 @@ import {
noop, noop,
safe_not_equal, safe_not_equal,
space, space,
subscribe,
toggle_class toggle_class
} from "svelte/internal"; } from "svelte/internal";
@ -133,13 +132,8 @@ let reactiveModuleVar = Math.random();
function instance($$self, $$props, $$invalidate) { function instance($$self, $$props, $$invalidate) {
let reactiveDeclaration; let reactiveDeclaration;
let $reactiveStoreVal; let $reactiveStoreVal;
let $reactiveDeclaration;
let $reactiveDeclaration,
$$unsubscribe_reactiveDeclaration = noop,
$$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration);
component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value)); component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value));
$$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration());
nonReactiveGlobal = Math.random(); nonReactiveGlobal = Math.random();
const reactiveConst = { x: Math.random() }; const reactiveConst = { x: Math.random() };
reactiveModuleVar += 1; reactiveModuleVar += 1;
@ -148,7 +142,8 @@ function instance($$self, $$props, $$invalidate) {
reactiveConst.x += 1; reactiveConst.x += 1;
} }
$: $$subscribe_reactiveDeclaration($$invalidate(1, reactiveDeclaration = reactiveModuleVar * 2)); $: reactiveDeclaration = reactiveModuleVar * 2;
component_subscribe($$self, reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value));
return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration]; return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration];
} }

@ -0,0 +1,60 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal,
set_data,
space,
text
} from "svelte/internal";
function create_fragment(ctx) {
let h1;
let t3;
let t4;
return {
c() {
h1 = element("h1");
h1.textContent = `Hello ${name}!`;
t3 = space();
t4 = text(/*foo*/ ctx[0]);
},
m(target, anchor) {
insert(target, h1, anchor);
insert(target, t3, anchor);
insert(target, t4, anchor);
},
p(ctx, [dirty]) {
if (dirty & /*foo*/ 1) set_data(t4, /*foo*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(h1);
if (detaching) detach(t3);
if (detaching) detach(t4);
}
};
}
let name = 'world';
function instance($$self) {
let foo;
$: foo = name + name;
return [foo];
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,7 @@
<script>
let name = 'world';
$: foo = name + name;
</script>
<h1>Hello {name}!</h1>
{foo}

@ -112,11 +112,13 @@ function create_fragment(ctx) {
if (a) { if (a) {
if (!previous_tag) { if (!previous_tag) {
svelte_element = create_dynamic_element(ctx); svelte_element = create_dynamic_element(ctx);
previous_tag = a;
svelte_element.c(); svelte_element.c();
svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor); svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor);
} else if (safe_not_equal(previous_tag, a)) { } else if (safe_not_equal(previous_tag, a)) {
svelte_element.d(1); svelte_element.d(1);
svelte_element = create_dynamic_element(ctx); svelte_element = create_dynamic_element(ctx);
previous_tag = a;
svelte_element.c(); svelte_element.c();
svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor); svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor);
} else { } else {
@ -125,9 +127,8 @@ function create_fragment(ctx) {
} else if (previous_tag) { } else if (previous_tag) {
svelte_element.d(1); svelte_element.d(1);
svelte_element = null; svelte_element = null;
}
previous_tag = a; previous_tag = a;
}
}, },
i: noop, i: noop,
o: noop, o: noop,

@ -72,11 +72,13 @@ function create_fragment(ctx) {
if (/*tag*/ ctx[0].svg) { if (/*tag*/ ctx[0].svg) {
if (!previous_tag) { if (!previous_tag) {
svelte_element = create_dynamic_element(ctx); svelte_element = create_dynamic_element(ctx);
previous_tag = /*tag*/ ctx[0].svg;
svelte_element.c(); svelte_element.c();
svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor); svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor);
} else if (safe_not_equal(previous_tag, /*tag*/ ctx[0].svg)) { } else if (safe_not_equal(previous_tag, /*tag*/ ctx[0].svg)) {
svelte_element.d(1); svelte_element.d(1);
svelte_element = create_dynamic_element(ctx); svelte_element = create_dynamic_element(ctx);
previous_tag = /*tag*/ ctx[0].svg;
svelte_element.c(); svelte_element.c();
svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor); svelte_element.m(svelte_element_anchor.parentNode, svelte_element_anchor);
} else { } else {
@ -85,9 +87,8 @@ function create_fragment(ctx) {
} else if (previous_tag) { } else if (previous_tag) {
svelte_element.d(1); svelte_element.d(1);
svelte_element = null; svelte_element = null;
}
previous_tag = /*tag*/ ctx[0].svg; previous_tag = /*tag*/ ctx[0].svg;
}
}, },
i: noop, i: noop,
o: noop, o: noop,

@ -0,0 +1,6 @@
export default {
async test({ assert, component }) {
assert.equal(component.toggle, true);
assert.equal(component.offsetHeight, 800);
}
};

@ -0,0 +1,18 @@
<script>
export let offsetHeight;
export let offsetWidth;
export let toggle = false;
$: if (offsetWidth) {
toggle = true;
}
</script>
<div class:toggle>
<div bind:offsetHeight bind:offsetWidth>{offsetHeight}</div>
</div>
<style>
.toggle > div {
height: 800px;
}
</style>

@ -0,0 +1,27 @@
<script>
import { afterUpdate, onDestroy } from "svelte";
export let id;
export let items;
let item = $items[id];
let selected = true;
function onClick() {
selected = !selected;
items.set({});
}
onDestroy(() => {
console.log("onDestroy");
});
afterUpdate(() => {
console.log("afterUpdate");
});
</script>
<button on:click="{onClick}">Click Me</button>
{#if selected}
<div>{item.id}</div>
{/if}

@ -0,0 +1,16 @@
export default {
html: `
<button>Click Me</button>
<div>1</div>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const event = new window.MouseEvent('click');
const messages = [];
const log = console.log;
console.log = msg => messages.push(msg);
await button.dispatchEvent(event);
console.log = log;
assert.deepEqual(messages, ['afterUpdate', 'onDestroy']);
}
};

@ -0,0 +1,10 @@
<script>
import { writable } from 'svelte/store';
import Component from "./Component.svelte";
let items = writable({ 1: { id: 1 } });
</script>
{#each Object.values($items) as item (item.id)}
<Component id="{item.id}" {items} />
{/each}

@ -0,0 +1,3 @@
export default {
html: '<p>2, 1</p>'
};

@ -0,0 +1,6 @@
<script>
const arr = [1, 2];
[arr[0], arr[1]] = [arr[1], arr[0]];
</script>
<p>{arr[0]}, {arr[1]}</p>

@ -0,0 +1,3 @@
export default {
html: '<p>[{"a":2},100]</p>'
};

@ -0,0 +1,7 @@
<script>
const a = 100;
const arr = [{ a: 1 }, 2];
[arr[0].a, arr[1] = a] = [arr[1]];
</script>
<p>{JSON.stringify(arr)}</p>

@ -9,10 +9,6 @@ export default {
html: "<input type='checkbox'>", html: "<input type='checkbox'>",
// somehow ssr will render indeterminate=""
// the hydrated html will still contain that attribute
ssrHtml: "<input type='checkbox' indeterminate=''>",
test({ assert, component, target }) { test({ assert, component, target }) {
const input = target.querySelector('input'); const input = target.querySelector('input');

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

@ -0,0 +1,5 @@
<script>
export let inert;
</script>
<div {inert}>some div <button>click</button></div>

@ -0,0 +1,11 @@
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);
}
};

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

@ -5,6 +5,7 @@ export default {
<p>selected: a</p> <p>selected: a</p>
<select> <select>
<option disabled='' value='x'>x</option>
<option value='a'>a</option> <option value='a'>a</option>
<option value='b'>b</option> <option value='b'>b</option>
<option value='c'>c</option> <option value='c'>c</option>
@ -18,7 +19,8 @@ export default {
const select = target.querySelector('select'); const select = target.querySelector('select');
const options = [...target.querySelectorAll('option')]; const options = [...target.querySelectorAll('option')];
// first enabled option should be selected
assert.equal(select.value, 'a'); assert.equal(select.value, 'a');
assert.ok(options[0].selected); assert.ok(options[1].selected);
} }
}; };

@ -5,6 +5,7 @@
<p>selected: {selected}</p> <p>selected: {selected}</p>
<select bind:value={selected}> <select bind:value={selected}>
<option disabled>x</option>
<option>a</option> <option>a</option>
<option>b</option> <option>b</option>
<option>c</option> <option>c</option>

@ -56,5 +56,40 @@ export default {
<p>selected: d</p> <p>selected: d</p>
`); `);
component.selected = 'b'; // second option should now be selected
assert.equal(select.value, 'b');
assert.ok(options[1].selected);
assert.htmlEqual(target.innerHTML, `
<p>selected: b</p>
<select>
<option value='a'>a</option>
<option value='b'>b</option>
<option value='c'>c</option>
</select>
<p>selected: b</p>
`);
component.selected = undefined; // also doesn't match an option
// now no option should be selected again
assert.equal(select.value, '');
assert.equal(select.selectedIndex, -1);
assert.ok(!options[0].selected);
assert.htmlEqual(target.innerHTML, `
<p>selected: undefined</p>
<select>
<option value='a'>a</option>
<option value='b'>b</option>
<option value='c'>c</option>
</select>
<p>selected: undefined</p>
`);
} }
}; };

@ -0,0 +1,21 @@
<script>
import InnerChild from './InnerChild.svelte';
export let id = 1;
export let count;
export let increment;
let list;
$: {
list = [];
for (let i = 0; i < count; ++i) {
list.push(i);
}
}
</script>
<div data-id={id}>
{#each list as item (item)}
<InnerChild val={item} {increment} />
{/each}
</div>

@ -0,0 +1,14 @@
<script>
import { afterUpdate } from 'svelte';
export let val = 1;
export let increment;
afterUpdate(() => {
increment();
});
</script>
<inner>
{val}
</inner>

@ -0,0 +1,53 @@
export default {
html: `
<div data-id="1">
<inner>0</inner>
<inner>1</inner>
</div>
<div data-id="2">
<inner>0</inner>
<inner>1</inner>
</div>
<div data-id="3">
<inner>0</inner>
<inner>1</inner>
</div>
`,
ssrHtml: `
<div data-id="1">
<inner>0</inner>
<inner>1</inner>
<inner>2</inner>
</div>
<div data-id="2">
<inner>0</inner>
<inner>1</inner>
<inner>2</inner>
</div>
<div data-id="3">
<inner>0</inner>
<inner>1</inner>
<inner>2</inner>
</div>
`,
async test({ assert, component, target }) {
await component.done;
assert.equal(component.getCounter(), 13);
assert.htmlEqual(target.innerHTML, `
<div data-id="3">
<inner>0</inner>
<inner>1</inner>
</div>
<div data-id="2">
<inner>0</inner>
<inner>1</inner>
</div>
<div data-id="1">
<inner>0</inner>
<inner>1</inner>
</div>
`);
}
};

@ -0,0 +1,35 @@
<script>
import { onMount } from 'svelte';
import Child from './Child.svelte';
let updateCounter = 0;
let promiseResolve;
export const done = new Promise(resolve => {
promiseResolve = resolve;
});
export const getCounter = () => {
return updateCounter;
};
let vals = [1, 2, 3];
const instances = [];
let count = 3;
const increment = () => {
++updateCounter;
};
onMount(() => {
count = 2;
setTimeout(() => {
vals = vals.reverse();
setTimeout(promiseResolve);
});
});
</script>
{#each vals as val, index (val)}
<Child bind:this={instances[index]} id={val} {count} {increment} />
{/each}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save