diff --git a/CHANGELOG.md b/CHANGELOG.md index fba777ad9c..c4ea2fbe7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,22 @@ ## 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 ` ``` +##### Image element bindings + +--- + +Image elements (``) 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 + +``` + ##### 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. -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. @@ -1307,14 +1324,12 @@ A custom animation function can also return a `tick` function, which is called * const d = Math.sqrt(dx * dx + dy * dy); return { - delay: 0, - duration: Math.sqrt(d) * 120, - easing: cubicOut, - tick: (t, u) => - Object.assign(node.style, { - color: t > 0.5 ? 'Pink' : 'Blue' - }); - }; + delay: 0, + duration: Math.sqrt(d) * 120, + easing: cubicOut, + tick: (t, u) => + Object.assign(node.style, { color: t > 0.5 ? 'Pink' : 'Blue' }) + }; } @@ -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 /* global.css */ @@ -1575,7 +1590,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 `` is equivalent to ``. @@ -1666,11 +1681,11 @@ If `this` is falsy, no component is rendered. The `` 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` is the name of a void tag (e.g., `br`) and `` 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 `` has child elements, a runtime error will be thrown in development mode. ```sv - -{#if showModal} - -

- modal - adjective mod·al \ˈmō-dəl\ -

+ +

+ modal + adjective mod·al \ˈmō-dəl\ +

-
    -
  1. of or relating to modality in logic
  2. -
  3. containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy
  4. -
  5. of or relating to a musical mode
  6. -
  7. of or relating to structure as opposed to substance
  8. -
  9. of, relating to, or constituting a grammatical form or category characteristically indicating predication
  10. -
  11. of or relating to a statistical mode
  12. -
+
    +
  1. of or relating to modality in logic
  2. +
  3. + containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy +
  4. +
  5. of or relating to a musical mode
  6. +
  7. of or relating to structure as opposed to substance
  8. +
  9. of, relating to, or constituting a grammatical form or category characteristically indicating predication
  10. +
  11. of or relating to a statistical mode
  12. +
- merriam-webster.com -
-{/if} + merriam-webster.com +
diff --git a/site/content/examples/15-composition/05-modal/Modal.svelte b/site/content/examples/15-composition/05-modal/Modal.svelte index baa57824bc..acdbd50ab1 100644 --- a/site/content/examples/15-composition/05-modal/Modal.svelte +++ b/site/content/examples/15-composition/05-modal/Modal.svelte @@ -1,81 +1,63 @@ - - - - - + + (showModal = false)} + on:click|self={() => dialog.close()} +> +
+ +
+ +
+ + +
+
\ No newline at end of file + diff --git a/site/content/faq/800-how-do-i-test-svelte-apps.md b/site/content/faq/800-how-do-i-test-svelte-apps.md index 0e9bc9e146..5ebabdb214 100644 --- a/site/content/faq/800-how-do-i-test-svelte-apps.md +++ b/site/content/faq/800-how-do-i-test-svelte-apps.md @@ -2,12 +2,20 @@ 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 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 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) diff --git a/site/content/tutorial/01-introduction/07-making-an-app/app-a/App.svelte b/site/content/tutorial/01-introduction/06-making-an-app/app-a/App.svelte similarity index 100% rename from site/content/tutorial/01-introduction/07-making-an-app/app-a/App.svelte rename to site/content/tutorial/01-introduction/06-making-an-app/app-a/App.svelte diff --git a/site/content/tutorial/01-introduction/07-making-an-app/text.md b/site/content/tutorial/01-introduction/06-making-an-app/text.md similarity index 100% rename from site/content/tutorial/01-introduction/07-making-an-app/text.md rename to site/content/tutorial/01-introduction/06-making-an-app/text.md diff --git a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte index 02e6fa7640..e91026fb98 100644 --- a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte +++ b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte @@ -1,4 +1,6 @@

diff --git a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte index 02e6fa7640..e91026fb98 100644 --- a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte +++ b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte @@ -1,4 +1,6 @@

diff --git a/site/content/tutorial/06-bindings/05-textarea-inputs/text.md b/site/content/tutorial/06-bindings/05-textarea-inputs/text.md index 42763200c7..d9a08eee91 100644 --- a/site/content/tutorial/06-bindings/05-textarea-inputs/text.md +++ b/site/content/tutorial/06-bindings/05-textarea-inputs/text.md @@ -1,8 +1,7 @@ --- title: Textarea inputs --- - -The ` @@ -14,4 +13,4 @@ In cases like these, where the names match, we can also use a shorthand form: ``` -This applies to all bindings, not just textareas. \ No newline at end of file +This applies to all bindings, not just textareas. diff --git a/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte b/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte index 57d4dcc79a..cc3856316c 100644 --- a/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte +++ b/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte @@ -4,22 +4,24 @@ export let todo; - let div; + let button; afterUpdate(() => { - flash(div); + flash(button); }); -

+
+ \ No newline at end of file + diff --git a/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte b/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte index fa81d35989..848f167d02 100644 --- a/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte +++ b/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte @@ -6,22 +6,24 @@ export let todo; - let div; + let button; afterUpdate(() => { - flash(div); + flash(button); }); -
+
+ \ No newline at end of file + diff --git a/site/content/tutorial/18-debugging/meta.json b/site/content/tutorial/18-debugging/meta.json deleted file mode 100644 index 5eeed281d3..0000000000 --- a/site/content/tutorial/18-debugging/meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Debugging" -} \ No newline at end of file diff --git a/site/content/tutorial/18-debugging/01-debug/app-a/App.svelte b/site/content/tutorial/18-special-tags/01-debug/app-a/App.svelte similarity index 100% rename from site/content/tutorial/18-debugging/01-debug/app-a/App.svelte rename to site/content/tutorial/18-special-tags/01-debug/app-a/App.svelte diff --git a/site/content/tutorial/18-debugging/01-debug/app-b/App.svelte b/site/content/tutorial/18-special-tags/01-debug/app-b/App.svelte similarity index 100% rename from site/content/tutorial/18-debugging/01-debug/app-b/App.svelte rename to site/content/tutorial/18-special-tags/01-debug/app-b/App.svelte diff --git a/site/content/tutorial/18-debugging/01-debug/text.md b/site/content/tutorial/18-special-tags/01-debug/text.md similarity index 100% rename from site/content/tutorial/18-debugging/01-debug/text.md rename to site/content/tutorial/18-special-tags/01-debug/text.md diff --git a/site/content/tutorial/01-introduction/06-html-tags/app-a/App.svelte b/site/content/tutorial/18-special-tags/02-html-tags/app-a/App.svelte similarity index 100% rename from site/content/tutorial/01-introduction/06-html-tags/app-a/App.svelte rename to site/content/tutorial/18-special-tags/02-html-tags/app-a/App.svelte diff --git a/site/content/tutorial/01-introduction/06-html-tags/app-b/App.svelte b/site/content/tutorial/18-special-tags/02-html-tags/app-b/App.svelte similarity index 100% rename from site/content/tutorial/01-introduction/06-html-tags/app-b/App.svelte rename to site/content/tutorial/18-special-tags/02-html-tags/app-b/App.svelte diff --git a/site/content/tutorial/01-introduction/06-html-tags/text.md b/site/content/tutorial/18-special-tags/02-html-tags/text.md similarity index 57% rename from site/content/tutorial/01-introduction/06-html-tags/text.md rename to site/content/tutorial/18-special-tags/02-html-tags/text.md index ff547247e2..0da4708020 100644 --- a/site/content/tutorial/01-introduction/06-html-tags/text.md +++ b/site/content/tutorial/18-special-tags/02-html-tags/text.md @@ -12,4 +12,4 @@ In Svelte, you do this with the special `{@html ...}` tag:

{@html string}

``` -> 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. diff --git a/site/content/tutorial/18-special-tags/meta.json b/site/content/tutorial/18-special-tags/meta.json new file mode 100644 index 0000000000..06e1732030 --- /dev/null +++ b/site/content/tutorial/18-special-tags/meta.json @@ -0,0 +1,3 @@ +{ + "title": "Special tags" +} \ No newline at end of file diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index c0d703892b..ffac6e1f07 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings'; import compiler_errors from './compiler_errors'; 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 is_dynamic from './render_dom/wrappers/shared/is_dynamic'; interface ComponentOptions { namespace?: string; @@ -1380,12 +1381,11 @@ export default class Component { module_dependencies.add(name); } } - const is_writable_or_mutated = - variable && (variable.writable || variable.mutated); + if ( should_add_as_dependency && (!owner || owner === component.instance_scope) && - (name[0] === '$' || is_writable_or_mutated) + (name[0] === '$' || variable) ) { dependencies.add(name); } @@ -1409,6 +1409,19 @@ export default class Component { const { expression } = node.body as ExpressionStatement; 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({ assignees, dependencies, diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index e4d4ffddad..b0c836681c 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -44,7 +44,7 @@ export default { code: 'invalid-binding', message: 'Cannot bind to a variable declared with {@const ...}' }, - invalid_binding_writibale: { + invalid_binding_writable: { code: 'invalid-binding', message: 'Cannot bind to a variable which is not writable' }, diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 3f552eb8b8..a10fe6155c 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -187,6 +187,10 @@ export default { code: 'a11y-no-noninteractive-tabindex', 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: { code: 'redundant-event-modifier', message: 'Touch event handlers that don\'t use the \'event\' object are passive by default' diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 07e2dc439e..5feb59ec0c 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -350,7 +350,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string, const attr = node.attributes.find((attr: CssNode) => attr.name === name); if (!attr) return false; if (attr.is_true) return operator === null; - if (!expected_value) return true; + if (expected_value == null) return true; if (attr.chunks.length === 1) { const value = attr.chunks[0]; diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index f826df4828..9a62b68850 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -22,7 +22,9 @@ const read_only_media_attributes = new Set([ 'seeking', 'ended', 'videoHeight', - 'videoWidth' + 'videoWidth', + 'naturalWidth', + 'naturalHeight' ]); 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; 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; } } diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 0d3e8a01bd..1ac75faffd 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -225,6 +225,7 @@ export default class Element extends Node { namespace: string; needs_manual_style_scoping: boolean; tag_expr: Expression; + contains_a11y_label: boolean; get is_dynamic_element() { return this.name === 'svelte:element'; @@ -484,6 +485,11 @@ export default class Element extends Node { 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 @@ -620,6 +626,7 @@ export default class Element extends Node { const id_attribute = attribute_map.get('id'); const name_attribute = attribute_map.get('name'); const target_attribute = attribute_map.get('target'); + const aria_label_attribute = attribute_map.get('aria-label'); // 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 @@ -642,6 +649,13 @@ export default class Element extends Node { } } + if (aria_label_attribute) { + const aria_value = aria_label_attribute.get_static_value(); + if (aria_value != '') { + this.contains_a11y_label = true; + } + } + if (href_attribute) { const href_value = href_attribute.get_static_value(); @@ -718,7 +732,10 @@ export default class Element extends Node { } 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; } @@ -901,6 +918,13 @@ export default class Element extends Node { } 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`)); } + } else if ( + name === 'naturalWidth' || + name === 'naturalHeight' + ) { + if (this.name !== 'img') { + return component.error(binding, compiler_errors.invalid_binding_element_with('', name)); + } } else if ( name === 'textContent' || name === 'innerHTML' @@ -922,6 +946,7 @@ export default class Element extends Node { validate_content() { if (!a11y_required_content.has(this.name)) return; + if (this.contains_a11y_label) return; if ( this.bindings .some((binding) => ['textContent', 'innerHTML'].includes(binding.name)) @@ -1034,14 +1059,14 @@ export default class Element extends Node { } } -const regex_starts_with_vovel = /^[aeiou]/; +const regex_starts_with_vowel = /^[aeiou]/; function should_have_attribute( node, attributes: string[], 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 ? attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : attributes[0]; diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 58b7a8317b..bc56f2eb27 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -390,13 +390,13 @@ export default function dom( const resubscribable_reactive_store_unsubscribers = reactive_stores .filter(store => { 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)}`}());`); if (has_definition) { - const reactive_declarations: (Node | Node[]) = []; - const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever + const reactive_declarations: Node[] = []; + const fixed_reactive_declarations: Array = []; // not really 'reactive' but whatever component.reactive_declarations.forEach(d => { const dependencies = Array.from(d.dependencies); @@ -417,6 +417,15 @@ export default function dom( reactive_declarations.push(statement); } else { 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 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 subscribe = `$$subscribe_${name}`; const i = renderer.context_lookup.get($name).index; diff --git a/src/compiler/compile/render_dom/invalidate.ts b/src/compiler/compile/render_dom/invalidate.ts index ffc5b6b42e..d9efdb5142 100644 --- a/src/compiler/compile/render_dom/invalidate.ts +++ b/src/compiler/compile/render_dom/invalidate.ts @@ -19,6 +19,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: !variable.hoistable && !variable.global && !variable.module && + !variable.is_reactive_static && ( variable.referenced || variable.subscribable || diff --git a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts index 9e2ec85f0f..ab863e9dff 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts @@ -120,7 +120,9 @@ export default class BindingWrapper { type === '' || type === 'text' || type === 'email' || - type === 'password' + type === 'password' || + type === 'search' || + type === 'url' ) { update_conditions.push( x`${parent.var}.${this.node.name} !== ${this.snippet}` diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 8a8f59810b..39ca782af0 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -137,6 +137,10 @@ const events = [ event_names: ['toggle'], filter: (node: Element, _name: string) => node.name === 'details' + }, + { + event_names: ['load'], + filter: (_: Element, name: string) => name === 'naturalHeight' || name === 'naturalWidth' } ]; diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index eb2cc336ed..402f9ff5e1 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -223,6 +223,7 @@ export interface Var { subscribable?: boolean; is_reactive_dependency?: boolean; imported?: boolean; + is_reactive_static?: boolean; } export interface CssResult { diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts index 8de6563c8b..86c27b33cf 100644 --- a/src/compiler/parse/index.ts +++ b/src/compiler/parse/index.ts @@ -132,6 +132,10 @@ export class Parser { 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) { const match = pattern.exec(this.template.slice(this.index)); 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) { const result = this.match_regex(pattern); if (result) this.index += result.length; diff --git a/src/compiler/parse/read/script.ts b/src/compiler/parse/read/script.ts index 02506ab3d5..43974c26ab 100644 --- a/src/compiler/parse/read/script.ts +++ b/src/compiler/parse/read/script.ts @@ -6,6 +6,7 @@ import parser_errors from '../errors'; import { regex_not_newline_characters } from '../../utils/patterns'; 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 { 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; - parser.read(regex_closing_script_tag); + parser.read(regex_starts_with_closing_script_tag); let ast: Program; diff --git a/src/compiler/parse/read/style.ts b/src/compiler/parse/read/style.ts index 71455403a3..eba86d22de 100644 --- a/src/compiler/parse/read/style.ts +++ b/src/compiler/parse/read/style.ts @@ -8,6 +8,7 @@ import { Style } from '../../interfaces'; import parser_errors from '../errors'; 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 { const content_start = parser.index; @@ -22,7 +23,7 @@ export default function read_style(parser: Parser, start: number, attributes: No // discard styles when css is disabled if (parser.css_mode === 'none') { - parser.read(regex_closing_style_tag); + parser.read(regex_starts_with_closing_style_tag); return null; } @@ -77,7 +78,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; diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index c3b58afe2a..328a043677 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -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) { const start = parser.index; diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 9d1bf5e5c2..efcff645a7 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -12,6 +12,9 @@ import { closing_tag_omitted, decode_character_references } from '../utils/html' // eslint-disable-next-line no-useless-escape 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([ ['svelte:head', 'Head'], ['svelte:options', 'Options'], @@ -293,7 +296,7 @@ function read_tag_name(parser: Parser) { // eslint-disable-next-line no-useless-escape const regex_token_ending_character = /[\s=\/>"']/; -const regex_quote_characters = /["']/; +const regex_starts_with_quote_characters = /^["']/; function read_attribute(parser: Parser, unique_names: Set) { const start = parser.index; @@ -368,7 +371,7 @@ function read_attribute(parser: Parser, unique_names: Set) { parser.allow_whitespace(); value = read_attribute_value(parser); 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); } @@ -475,15 +478,13 @@ function read_attribute_value(parser: Parser) { }]; } - const regex = ( - quote_mark === "'" ? /'/ : - quote_mark === '"' ? /"/ : - /(\/>|[\s"'=<>`])/ - ); - let value; 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) { if (error.code === 'parse-error') { // if the attribute value didn't close + self-closing tag diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 5aec24c651..a8a500b25b 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -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 { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; 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) { const $$ = component.$$; if ($$.fragment !== null) { + flush_render_callbacks($$.after_update); + run_all($$.on_destroy); $$.fragment && $$.fragment.d(detaching); diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index ca76b11ab3..52f1198833 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -624,6 +624,10 @@ export function add_resize_listener(node: HTMLElement, fn: () => void) { iframe.src = 'about:blank'; iframe.onload = () => { 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(); }; } diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index f95ba446f5..5327e7b5b5 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -5,7 +5,7 @@ export const dirty_components = []; export const intros = { enabled: false }; export const binding_callbacks = []; -const render_callbacks = []; +let render_callbacks = []; const flush_callbacks = []; const resolved_promise = Promise.resolve(); @@ -122,3 +122,14 @@ function update($$) { $$.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; +} diff --git a/src/runtime/store/index.ts b/src/runtime/store/index.ts index e947fa0740..ee277c6216 100644 --- a/src/runtime/store/index.ts +++ b/src/runtime/store/index.ts @@ -98,7 +98,7 @@ export function writable(value?: T, start: StartStopNotifier = noop): Writ return () => { subscribers.delete(subscriber); - if (subscribers.size === 0) { + if (subscribers.size === 0 && stop) { stop(); stop = null; } @@ -207,6 +207,17 @@ export function derived(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(store: Readable): Readable { + return { + subscribe: store.subscribe.bind(store) + }; +} + /** * Get the current value from a store by subscribing and immediately unsubscribing. * @param store readable diff --git a/src/runtime/transition/index.ts b/src/runtime/transition/index.ts index c6d9dbc97a..cc8ace194f 100644 --- a/src/runtime/transition/index.ts +++ b/src/runtime/transition/index.ts @@ -211,7 +211,7 @@ export interface CrossfadeParams { easing?: EasingFunction; } -type ClientRectMap = Map; +type ClientRectMap = Map; export function crossfade({ fallback, ...defaults }: CrossfadeParams & { 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_send: ClientRectMap = new Map(); - function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig { + function crossfade(from_node: Element, node: Element, params: CrossfadeParams): TransitionConfig { const { delay = 0, duration = d => Math.sqrt(d) * 30, easing = cubicOut } = assign(assign({}, defaults), params); + const from = from_node.getBoundingClientRect(); const to = node.getBoundingClientRect(); const dx = from.left - to.left; const dy = from.top - to.top; @@ -264,16 +265,14 @@ export function crossfade({ fallback, ...defaults }: CrossfadeParams & { function transition(items: ClientRectMap, counterparts: ClientRectMap, intro: boolean) { return (node: Element, params: CrossfadeParams & { key: any }) => { - items.set(params.key, { - rect: node.getBoundingClientRect() - }); + items.set(params.key, node); return () => { if (counterparts.has(params.key)) { - const { rect } = counterparts.get(params.key); + const other_node = counterparts.get(params.key); counterparts.delete(params.key); - return crossfade(rect, node, params); + return crossfade(other_node, node, params); } // if the node is disappearing altogether diff --git a/test/css/samples/unused-selector-empty-attribute/_config.js b/test/css/samples/unused-selector-empty-attribute/_config.js new file mode 100644 index 0000000000..6f77c6a3e1 --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/_config.js @@ -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: }` + }] +}; diff --git a/test/css/samples/unused-selector-empty-attribute/expected.css b/test/css/samples/unused-selector-empty-attribute/expected.css new file mode 100644 index 0000000000..987fa142db --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/expected.css @@ -0,0 +1 @@ +img[alt].svelte-xyz{border:1px solid green} \ No newline at end of file diff --git a/test/css/samples/unused-selector-empty-attribute/expected.html b/test/css/samples/unused-selector-empty-attribute/expected.html new file mode 100644 index 0000000000..889908c217 --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/expected.html @@ -0,0 +1 @@ +a foo \ No newline at end of file diff --git a/test/css/samples/unused-selector-empty-attribute/input.svelte b/test/css/samples/unused-selector-empty-attribute/input.svelte new file mode 100644 index 0000000000..0200e537be --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/input.svelte @@ -0,0 +1,11 @@ +a foo + + diff --git a/test/js/samples/capture-inject-state/expected.js b/test/js/samples/capture-inject-state/expected.js index 882b3e9cf6..41f9b966b0 100644 --- a/test/js/samples/capture-inject-state/expected.js +++ b/test/js/samples/capture-inject-state/expected.js @@ -48,7 +48,7 @@ function create_fragment(ctx) { t8 = text(/*$prop*/ ctx[2]); t9 = space(); t10 = text(/*shadowedByModule*/ ctx[4]); - add_location(p, file, 22, 0, 430); + add_location(p, file, 22, 0, 431); }, l: function claim(nodes) { 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; -const moduleContantProps = 4; +const moduleConstantProps = 4; let moduleLet; const moduleConst = 2; let shadowedByModule; @@ -137,7 +137,7 @@ function instance($$self, $$props, $$invalidate) { $$self.$capture_state = () => ({ moduleLiveBinding, - moduleContantProps, + moduleConstantProps, moduleLet, moduleConst, shadowedByModule, @@ -197,4 +197,4 @@ class Component extends SvelteComponentDev { } export default Component; -export { moduleLiveBinding, moduleContantProps }; \ No newline at end of file +export { moduleLiveBinding, moduleConstantProps }; diff --git a/test/js/samples/capture-inject-state/input.svelte b/test/js/samples/capture-inject-state/input.svelte index a1051bc147..a80fecf00f 100644 --- a/test/js/samples/capture-inject-state/input.svelte +++ b/test/js/samples/capture-inject-state/input.svelte @@ -1,6 +1,6 @@ + + + +{naturalWidth} x {naturalHeight} diff --git a/test/js/samples/reactive-class-optimized/expected.js b/test/js/samples/reactive-class-optimized/expected.js index 1d0606ad60..f75a4015b0 100644 --- a/test/js/samples/reactive-class-optimized/expected.js +++ b/test/js/samples/reactive-class-optimized/expected.js @@ -9,7 +9,6 @@ import { noop, safe_not_equal, space, - subscribe, toggle_class } from "svelte/internal"; @@ -133,13 +132,8 @@ let reactiveModuleVar = Math.random(); function instance($$self, $$props, $$invalidate) { let reactiveDeclaration; let $reactiveStoreVal; - - let $reactiveDeclaration, - $$unsubscribe_reactiveDeclaration = noop, - $$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration); - + let $reactiveDeclaration; component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value)); - $$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration()); nonReactiveGlobal = Math.random(); const reactiveConst = { x: Math.random() }; reactiveModuleVar += 1; @@ -148,7 +142,8 @@ function instance($$self, $$props, $$invalidate) { 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]; } diff --git a/test/js/samples/reactive-values/expected.js b/test/js/samples/reactive-values/expected.js new file mode 100644 index 0000000000..7ed435d6ad --- /dev/null +++ b/test/js/samples/reactive-values/expected.js @@ -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; \ No newline at end of file diff --git a/test/js/samples/reactive-values/input.svelte b/test/js/samples/reactive-values/input.svelte new file mode 100644 index 0000000000..d713217771 --- /dev/null +++ b/test/js/samples/reactive-values/input.svelte @@ -0,0 +1,7 @@ + + +

Hello {name}!

+{foo} \ No newline at end of file diff --git a/test/runtime-puppeteer/samples/binding-width-height-initialize/_config.js b/test/runtime-puppeteer/samples/binding-width-height-initialize/_config.js new file mode 100644 index 0000000000..d67299ad56 --- /dev/null +++ b/test/runtime-puppeteer/samples/binding-width-height-initialize/_config.js @@ -0,0 +1,6 @@ +export default { + async test({ assert, component }) { + assert.equal(component.toggle, true); + assert.equal(component.offsetHeight, 800); + } +}; diff --git a/test/runtime-puppeteer/samples/binding-width-height-initialize/main.svelte b/test/runtime-puppeteer/samples/binding-width-height-initialize/main.svelte new file mode 100644 index 0000000000..6136365a1d --- /dev/null +++ b/test/runtime-puppeteer/samples/binding-width-height-initialize/main.svelte @@ -0,0 +1,18 @@ + + +
+
{offsetHeight}
+
+ + diff --git a/test/runtime/samples/action-update-before-destroy/Component.svelte b/test/runtime/samples/action-update-before-destroy/Component.svelte new file mode 100644 index 0000000000..e38a0fff64 --- /dev/null +++ b/test/runtime/samples/action-update-before-destroy/Component.svelte @@ -0,0 +1,27 @@ + + + +{#if selected} +
{item.id}
+{/if} diff --git a/test/runtime/samples/action-update-before-destroy/_config.js b/test/runtime/samples/action-update-before-destroy/_config.js new file mode 100644 index 0000000000..a822c4d8a5 --- /dev/null +++ b/test/runtime/samples/action-update-before-destroy/_config.js @@ -0,0 +1,16 @@ +export default { + html: ` + +
1
+ `, + 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']); + } +}; diff --git a/test/runtime/samples/action-update-before-destroy/main.svelte b/test/runtime/samples/action-update-before-destroy/main.svelte new file mode 100644 index 0000000000..7f99ce0f38 --- /dev/null +++ b/test/runtime/samples/action-update-before-destroy/main.svelte @@ -0,0 +1,10 @@ + + +{#each Object.values($items) as item (item.id)} + +{/each} diff --git a/test/store/index.ts b/test/store/index.ts index b6fc5940e1..7e8bdb2f64 100644 --- a/test/store/index.ts +++ b/test/store/index.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { readable, writable, derived, get } from '../../store'; +import { readable, writable, derived, get, readonly } from '../../store'; describe('store', () => { describe('writable', () => { @@ -88,6 +88,14 @@ describe('store', () => { unsubscribe(); }); + + it('no error even if unsubscribe calls twice', () => { + let num = 0; + const store = writable(num, set => set(num += 1)); + const unsubscribe = store.subscribe(() => { }); + unsubscribe(); + assert.doesNotThrow(() => unsubscribe()); + }); }); describe('readable', () => { @@ -411,4 +419,21 @@ describe('store', () => { assert.equal(get(fake_observable), 42); }); }); + + describe('readonly', () => { + it('makes a store readonly', () => { + const writableStore = writable(1); + const readableStore = readonly(writableStore); + + assert.equal(get(readableStore), get(writableStore)); + + writableStore.set(2); + + assert.equal(get(readableStore), 2); + assert.equal(get(readableStore), get(writableStore)); + + assert.throws(() => readableStore.set(3)); + }); + + }); }); diff --git a/test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte b/test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte new file mode 100644 index 0000000000..c13df04b22 --- /dev/null +++ b/test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte @@ -0,0 +1 @@ + diff --git a/test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json b/test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json @@ -0,0 +1 @@ +[] diff --git a/test/validator/samples/a11y-aria-activedescendant/input.svelte b/test/validator/samples/a11y-aria-activedescendant/input.svelte new file mode 100644 index 0000000000..22570e8c50 --- /dev/null +++ b/test/validator/samples/a11y-aria-activedescendant/input.svelte @@ -0,0 +1,17 @@ + + + + + + + + + + +
+
+
+ + +
+ diff --git a/test/validator/samples/a11y-aria-activedescendant/warnings.json b/test/validator/samples/a11y-aria-activedescendant/warnings.json new file mode 100644 index 0000000000..8749c1c125 --- /dev/null +++ b/test/validator/samples/a11y-aria-activedescendant/warnings.json @@ -0,0 +1,17 @@ +[ + { + "code": "a11y-aria-activedescendant-has-tabindex", + "end": { + "character": 568, + "column": 36, + "line": 16 + }, + "message": "A11y: Elements with attribute aria-activedescendant should have tabindex value", + "pos": 537, + "start": { + "character": 537, + "column": 5, + "line": 16 + } + } +] diff --git a/test/validator/samples/a11y-media-has-caption/input.svelte b/test/validator/samples/a11y-media-has-caption/input.svelte index 6d98fd3734..30f9a05f91 100644 --- a/test/validator/samples/a11y-media-has-caption/input.svelte +++ b/test/validator/samples/a11y-media-has-caption/input.svelte @@ -2,3 +2,6 @@ + + + diff --git a/test/validator/samples/a11y-media-has-caption/warnings.json b/test/validator/samples/a11y-media-has-caption/warnings.json index 72c2caae53..6db618f03c 100644 --- a/test/validator/samples/a11y-media-has-caption/warnings.json +++ b/test/validator/samples/a11y-media-has-caption/warnings.json @@ -28,5 +28,35 @@ "column": 0, "line": 3 } + }, + { + "code": "a11y-media-has-caption", + "end": { + "character": 124, + "column": 27, + "line": 5 + }, + "message": "A11y: