chore: merge

pull/18317/head
paoloricciuti 4 months ago
commit d8f94880fb

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: unlink errored and otherwise finished batch

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: walk composedPath() directly in delegated event propagation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transfer effects when merging batches

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow `$derived(await ...)` in disconnected effect roots

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove temporary raw-text hydration markers

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: declare `let:` directives before `{@const}` declarations on slotted elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly coordinate component-level effects inside async blocks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make unnecessary commit work less likely

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: add tag name to `a11y_click_events_have_key_events` warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: catch rejected promises while merging/committing

@ -62,7 +62,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
### a11y_click_events_have_key_events ### a11y_click_events_have_key_events
``` ```
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
``` ```
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler. Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -49,7 +49,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
## a11y_click_events_have_key_events ## a11y_click_events_have_key_events
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate > Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler. Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -302,7 +302,7 @@ export function check_element(node, context) {
const has_key_event = const has_key_event =
handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress'); handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress');
if (!has_key_event) { if (!has_key_event) {
w.a11y_click_events_have_key_events(node); w.a11y_click_events_have_key_events(node, node.name);
} }
} }
} }

@ -203,8 +203,8 @@ export function RegularElement(node, context) {
} }
} }
// Let bindings first, they can be used on attributes // Let bindings first, they can be used on attributes and `{@const}` declarations
context.state.init.push(...lets); context.state.let_directives.push(...lets);
const node_id = context.state.node; const node_id = context.state.node;

@ -166,11 +166,12 @@ export function a11y_autofocus(node) {
} }
/** /**
* Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate * Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
* @param {null | NodeLike} node * @param {null | NodeLike} node
* @param {string} element
*/ */
export function a11y_click_events_have_key_events(node) { export function a11y_click_events_have_key_events(node, element) {
w(node, 'a11y_click_events_have_key_events', `Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`); w(node, 'a11y_click_events_have_key_events', `Visible, non-interactive element \`<${element}>\` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
} }
/** /**

@ -14,7 +14,8 @@ import {
append_child, append_child,
create_comment, create_comment,
insert_before, insert_before,
node_type node_type,
remove_child
} from '../operations.js'; } from '../operations.js';
import { block, teardown } from '../../reactivity/effects.js'; import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js'; import { set_should_intro } from '../../render.js';
@ -96,9 +97,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
assign_nodes(element, element); assign_nodes(element, element);
if (render_fn) { if (render_fn) {
var tmp_comment = null;
if (hydrating && is_raw_text_element(next_tag)) { if (hydrating && is_raw_text_element(next_tag)) {
// prevent hydration glitches // prevent hydration glitches (code just below expects an anchor)
append_child(element, create_comment('')); append_child(element, (tmp_comment = create_comment('')));
} }
// If hydrating, use the existing ssr comment as the anchor so that the // If hydrating, use the existing ssr comment as the anchor so that the
@ -122,7 +125,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// contains children, it's a user error (which is warned on elsewhere) // contains children, it's a user error (which is warned on elsewhere)
// and the DOM will be silently discarded // and the DOM will be silently discarded
render_fn(element, child_anchor); render_fn(element, child_anchor);
if (tmp_comment) {
remove_child(element, tmp_comment);
}
set_animation_effect_override(null); set_animation_effect_override(null);
} }

@ -15,8 +15,7 @@ import {
remove_attribute, remove_attribute,
dispatch_event, dispatch_event,
add_event_listener, add_event_listener,
remove_event_listener, remove_event_listener
get_parent_node
} from '../operations.js'; } from '../operations.js';
import { current_renderer } from '../../custom-renderer/state.js'; import { current_renderer } from '../../custom-renderer/state.js';
@ -281,12 +280,7 @@ export function handle_event_propagation(event) {
var other_errors = []; var other_errors = [];
while (current_target !== null) { while (current_target !== null) {
/** @type {null | Element} */ if (current_target === handler_element) break;
var parent_element =
current_target.assignedSlot ||
get_parent_node(current_target) ||
/** @type {any} */ (current_target).host ||
null;
try { try {
// @ts-expect-error // @ts-expect-error
@ -308,10 +302,10 @@ export function handle_event_propagation(event) {
throw_error = error; throw_error = error;
} }
} }
if (event.cancelBubble || parent_element === handler_element || parent_element === null) { if (event.cancelBubble) break;
break;
} path_idx++;
current_target = parent_element; current_target = path_idx < path.length ? /** @type {Element} */ (path[path_idx]) : null;
} }
if (throw_error) { if (throw_error) {

@ -289,9 +289,10 @@ export class Batch {
} }
} }
// we only reschedule previously-deferred effects if we expect // We always reschedule previously-deferred effects, not just when
// to be able to run them after processing the batch // #is_deferred() is true, because traversing the tree could make
if (!this.#is_deferred()) { // an if block that contains the last blocking pending effect falsy,
// causing the block to no longer be deferred.
for (const e of this.#dirty_effects) { for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e); this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY); set_signal_status(e, DIRTY);
@ -302,7 +303,6 @@ export class Batch {
set_signal_status(e, MAYBE_DIRTY); set_signal_status(e, MAYBE_DIRTY);
this.schedule(e); this.schedule(e);
} }
}
const roots = this.#roots; const roots = this.#roots;
this.#roots = []; this.#roots = [];
@ -326,6 +326,12 @@ export class Batch {
this.#traverse(root, effects, render_effects); this.#traverse(root, effects, render_effects);
} catch (e) { } catch (e) {
reset_all(root); reset_all(root);
// If there's no async work left, this branch is now dead and needs
// to be unlinked to not become a zombie that is never cleaned up.
// See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
// for a (non-minimal) reproduction that demonstrates a case where this is necessary
// to not get follow-up false-positives via "batch has scheduled roots" invariant errors.
if (!this.#is_deferred()) this.#unlink();
throw e; throw e;
} }
} }
@ -362,6 +368,10 @@ export class Batch {
const earlier_batch = this.#find_earlier_batch(); const earlier_batch = this.#find_earlier_batch();
if (earlier_batch) { if (earlier_batch) {
// If this batch collected deferred effects during traversal, they still need
// to run after being merged into the earlier batch.
this.#defer_effects(render_effects);
this.#defer_effects(effects);
earlier_batch.#merge(this); earlier_batch.#merge(this);
return; return;
} }
@ -500,9 +510,12 @@ export class Batch {
for (const [effect, deferred] of batch.async_deriveds) { for (const [effect, deferred] of batch.async_deriveds) {
const d = this.async_deriveds.get(effect); const d = this.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve); if (d) deferred.promise.then(d.resolve).catch(d.reject);
} }
// Mark is not guaranteed not touch these, so we transfer them
this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
/** /**
* mark all effects that depend on `batch.current`, except the * mark all effects that depend on `batch.current`, except the
* async effects that we just resolved (TODO unless they depend * async effects that we just resolved (TODO unless they depend
@ -664,14 +677,16 @@ export class Batch {
// immediately resolving them? Likely not because of how this.apply() works. // immediately resolving them? Likely not because of how this.apply() works.
for (const [effect, deferred] of this.async_deriveds) { for (const [effect, deferred] of this.async_deriveds) {
const d = batch.async_deriveds.get(effect); const d = batch.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve); if (d) deferred.promise.then(d.resolve).catch(d.reject);
} }
} }
if (!batch.#started) continue; if (!batch.#started) continue;
// Re-run async/block effects that depend on distinct values changed in both batches // Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds)
var others = [...batch.current.keys()].filter((s) => !this.current.has(s)); var others = [...batch.current.keys()].filter(
(s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s)
);
if (others.length === 0) { if (others.length === 0) {
if (is_earlier) { if (is_earlier) {
@ -711,11 +726,14 @@ export class Batch {
} }
checked = new Map(); checked = new Map();
var current_unequal = [...batch.current.keys()].filter((c) => var current_unequal = [...batch.current]
this.current.has(c) .filter(([c, v1]) => {
? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c.v const v2 = this.current.get(c);
: true if (!v2) return true;
); // Either their values are different or one is a derived but not the other
return v2[0] !== v1[0] || v2[1] !== v1[1];
})
.map(([c]) => c);
if (current_unequal.length > 0) { if (current_unequal.length > 0) {
for (const effect of this.#new_effects) { for (const effect of this.#new_effects) {

@ -187,7 +187,10 @@ export function async_derived(fn, label, location) {
var decrement_pending = increment_pending(); var decrement_pending = increment_pending();
} }
if (/** @type {Boundary} */ (parent.b).is_rendered()) { if (
// boundary can be null if the async derived is inside an $effect.root not connected to the component render tree
parent.b?.is_rendered()
) {
batch.async_deriveds.get(effect)?.reject(OBSOLETE); batch.async_deriveds.get(effect)?.reject(OBSOLETE);
} else { } else {
// While the boundary is still showing pending, a new run supersedes all older in-flight runs // While the boundary is still showing pending, a new run supersedes all older in-flight runs

@ -20,7 +20,6 @@ import {
EFFECT, EFFECT,
DESTROYED, DESTROYED,
INERT, INERT,
REACTION_RAN,
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
EFFECT_TRANSPARENT, EFFECT_TRANSPARENT,
@ -215,7 +214,11 @@ export function user_effect(fn) {
// Non-nested `$effect(...)` in a component should be deferred // Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted // until the component is mounted
var flags = /** @type {Effect} */ (active_effect).f; var flags = /** @type {Effect} */ (active_effect).f;
var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & REACTION_RAN) === 0; var defer =
!active_reaction &&
(flags & BRANCH_EFFECT) !== 0 &&
component_context !== null &&
!component_context.i;
if (defer) { if (defer) {
// Top-level `$effect(...)` in an unmounted component — defer until mount // Top-level `$effect(...)` in an unmounted component — defer until mount

@ -1 +1 @@
<!--[--><!----><script>{}<!----></script><!----><!--]--> <!--[--><!----><script>{}</script><!----><!--]-->

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
props: {
css: 'body { color: red; }'
}
});

@ -0,0 +1,9 @@
<script>
let { css } = $props();
</script>
<svelte:head>
<svelte:element this="style" type="text/css">{css}</svelte:element>
</svelte:head>
<p>content</p>

@ -0,0 +1,7 @@
<script>
export let things;
</script>
{#each things as thing}
<slot name="foo" {thing} />
{/each}

@ -0,0 +1,15 @@
import { test } from '../../test';
// `let:` directives on a slotted element must be declared before sibling `{@const}`
// declarations that capture them. In dev mode the `{@const}` derived is read eagerly,
// so a wrong declaration order throws "Cannot access '...' before initialization".
export default test({
compileOptions: {
dev: true
},
html: `
<div slot="foo"><span>1</span></div>
<div slot="foo"><span>2</span></div>
`
});

@ -0,0 +1,10 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested things={[1, 2]}>
<div slot="foo" let:thing>
{@const props = { thing }}
<span>{props.thing}</span>
</div>
</Nested>

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
// Test that an async derived inside an $effect.root not connected to the component tree still works
async test({ assert, logs }) {
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(logs, [1]);
}
});

@ -0,0 +1,11 @@
<script>
setTimeout(() => {
$effect.root(() => {
async function fn() {
const value = $derived(await 1);
return { get value() { return value } };
}
fn().then(r => console.log(r.value));
})
})
</script>

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [x, x_y, pop] = target.querySelectorAll('button');
x.click();
await tick();
x_y.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>x</button> <button>x/y</button> <button>pop</button> 2 1 1'
);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state(0);
let y = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<button onclick={() => x++}>x</button>
<button onclick={() => {x++;y++}}>x/y</button>
<button onclick={() => (queued.pop()?.())}>pop</button>
{await push(x)} {await push(y)}
{#if true}
{y}
{/if}

@ -0,0 +1,17 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
increment.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button> done');
}
});

@ -0,0 +1,21 @@
<script>
let count = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<button onclick={() => count++}>increment</button>
{#if count < 3}
{await push(count)}
{:else}
done
{/if}

@ -0,0 +1,6 @@
<script>
let x;
$effect(() => console.log(!!x))
</script>
<div bind:this={x}></div>

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
// Test that $effect/onMount etc at the top level of components are correctly deferred/coordinated if inside an async block
async test({ assert, logs }) {
await tick();
assert.deepEqual(logs, [true]);
}
});

@ -0,0 +1,5 @@
<script>
import Child from './Child.svelte';
</script>
<Child foo={await 1} />

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, pop] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
increment.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> failed');
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> failed');
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil,reject) => {
queued.push(() => v === 3 ? reject('boom') : fulfil(v));
});
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queued.pop()?.()}>pop</button>
<svelte:boundary>
{await push(count)}
{#snippet failed()}failed{/snippet}
</svelte:boundary>

@ -2,7 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
async test({ assert, target, compileOptions }) { async test({ assert, target }) {
const [toggle, increment] = target.querySelectorAll('button'); const [toggle, increment] = target.querySelectorAll('button');
flushSync(() => increment.click()); flushSync(() => increment.click());
@ -25,8 +25,8 @@ export default test({
` `
<button>toggle</button> <button>toggle</button>
<button>count: 2</button> <button>count: 2</button>
<p>show: ${compileOptions.experimental?.async ? 'false' : 'true'}</p> <p>show: true</p>
` ` // show: false would also be fine; this is more about ensuring that things continue to work _somehow_
); );
} }
}); });

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 13, "line": 13,
"column": 0 "column": 0
@ -13,7 +13,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 15, "line": 15,
"column": 0 "column": 0
@ -25,7 +25,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<section>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 18, "line": 18,
"column": 0 "column": 0
@ -37,7 +37,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<main>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 20, "line": 20,
"column": 0 "column": 0
@ -49,7 +49,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<article>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 22, "line": 22,
"column": 0 "column": 0
@ -61,7 +61,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<header>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 24, "line": 24,
"column": 0 "column": 0
@ -73,7 +73,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<footer>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 26, "line": 26,
"column": 0 "column": 0
@ -85,7 +85,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<footer>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 28, "line": 28,
"column": 0 "column": 0

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"column": 1, "column": 1,
"line": 7 "line": 7

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate", "message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"column": 1, "column": 1,
"line": 8 "line": 8

Loading…
Cancel
Save