Merge branch 'main' into aa

aaa
Rich Harris 9 months ago
commit e8e0d7338d

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: consistently set value to blank string when value attribute is undefined

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: optimise || expressions in template

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: expand boolean attribute support

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add check for `is` attribute to correctly detect custom elements

@ -1,26 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Docs preview create request
on:
pull_request_target:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: docs-preview-create
client-payload: |-
{
"package": "svelte",
"repo": "${{ github.repository }}",
"owner": "${{ github.event.pull_request.head.repo.owner.login }}",
"branch": "${{ github.event.pull_request.head.ref }}",
"pr": ${{ github.event.pull_request.number }}
}

@ -1,27 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Docs preview delete request
on:
pull_request_target:
branches:
- main
types: [closed]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: docs-preview-delete
client-payload: |-
{
"package": "svelte",
"repo": "${{ github.repository }}",
"owner": "${{ github.event.pull_request.head.repo.owner.login }}",
"branch": "${{ github.event.pull_request.head.ref }}",
"pr": ${{ github.event.pull_request.number }}
}

@ -1,22 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Sync request
on:
push:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: sync-request
client-payload: |-
{
"package": "svelte"
}

@ -227,7 +227,7 @@ export function RegularElement(node, context) {
node_id, node_id,
attributes_id, attributes_id,
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true, (node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
node.name.includes('-') && b.true, is_custom_element_node(node) && b.true,
context.state context.state
); );

@ -4,6 +4,7 @@
import { cannot_be_set_statically } from '../../../../../../utils.js'; import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
import { is_custom_element_node } from '../../../../nodes.js';
import { build_template_chunk } from './utils.js'; import { build_template_chunk } from './utils.js';
/** /**
@ -128,7 +129,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function is_static_element(node, state) { function is_static_element(node, state) {
if (node.type !== 'RegularElement') return false; if (node.type !== 'RegularElement') return false;
if (node.fragment.metadata.dynamic) return false; if (node.fragment.metadata.dynamic) return false;
if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties if (is_custom_element_node(node)) return false; // we're setting all attributes on custom elements through properties
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') { if (attribute.type !== 'Attribute') {

@ -126,22 +126,22 @@ export function build_template_chunk(
// extra work in the template_effect (instead we do the work in set_text). // extra work in the template_effect (instead we do the work in set_text).
return { value, has_state }; return { value, has_state };
} else { } else {
let expression = value; // add `?? ''` where necessary (TODO optimise more cases)
// only add nullish coallescence if it hasn't been added already if (
if (value.type === 'LogicalExpression' && value.operator === '??') { value.type === 'LogicalExpression' &&
const { right } = value; value.right.type === 'Literal' &&
// `undefined` isn't a Literal (due to pre-ES5 shenanigans), so the only nullish literal is `null` (value.operator === '??' || value.operator === '||')
// however, you _can_ make a variable called `undefined` in a Svelte component, so we can't just treat it the same way ) {
if (right.type !== 'Literal') { // `foo ?? null` -=> `foo ?? ''`
expression = b.logical('??', value, b.literal('')); // otherwise leave the expression untouched
} else if (right.value === null) { if (value.right.value === null) {
// if they do something weird like `stuff ?? null`, replace `null` with empty string value = { ...value, right: b.literal('') };
value.right = b.literal('');
} }
} else { } else {
expression = b.logical('??', value, b.literal('')); value = b.logical('??', value, b.literal(''));
} }
expressions.push(expression);
expressions.push(value);
} }
quasi = b.quasi('', i + 1 === values.length); quasi = b.quasi('', i + 1 === values.length);

@ -23,10 +23,14 @@ export function is_element_node(node) {
/** /**
* @param {AST.RegularElement | AST.SvelteElement} node * @param {AST.RegularElement | AST.SvelteElement} node
* @returns {node is AST.RegularElement} * @returns {boolean}
*/ */
export function is_custom_element_node(node) { export function is_custom_element_node(node) {
return node.type === 'RegularElement' && node.name.includes('-'); return (
node.type === 'RegularElement' &&
(node.name.includes('-') ||
node.attributes.some((attr) => attr.type === 'Attribute' && attr.name === 'is'))
);
} }
/** /**

@ -68,14 +68,14 @@ export function set_value(element, value) {
// treat null and undefined the same for the initial value // treat null and undefined the same for the initial value
value ?? undefined) || value ?? undefined) ||
// @ts-expect-error // @ts-expect-error
// `progress` elements always need their value set when its `0` // `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS')) (element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
) { ) {
return; return;
} }
// @ts-expect-error // @ts-expect-error
element.value = value; element.value = value ?? '';
} }
/** /**

@ -170,7 +170,10 @@ const DOM_BOOLEAN_ATTRIBUTES = [
'reversed', 'reversed',
'seamless', 'seamless',
'selected', 'selected',
'webkitdirectory' 'webkitdirectory',
'defer',
'disablepictureinpicture',
'disableremoteplayback'
]; ];
/** /**
@ -197,7 +200,10 @@ const ATTRIBUTE_ALIASES = {
defaultvalue: 'defaultValue', defaultvalue: 'defaultValue',
defaultchecked: 'defaultChecked', defaultchecked: 'defaultChecked',
srcobject: 'srcObject', srcobject: 'srcObject',
novalidate: 'noValidate' novalidate: 'noValidate',
allowfullscreen: 'allowFullscreen',
disablepictureinpicture: 'disablePictureInPicture',
disableremoteplayback: 'disableRemotePlayback'
}; };
/** /**
@ -219,7 +225,11 @@ const DOM_PROPERTIES = [
'volume', 'volume',
'defaultValue', 'defaultValue',
'defaultChecked', 'defaultChecked',
'srcObject' 'srcObject',
'noValidate',
'allowFullscreen',
'disablePictureInPicture',
'disableRemotePlayback'
]; ];
/** /**

@ -0,0 +1,60 @@
import { test } from '../../test';
export default test({
// JSDOM lacks support for some of these attributes, so we'll skip it for now.
//
// See:
// - `async`: https://github.com/jsdom/jsdom/issues/1564
// - `nomodule`: https://github.com/jsdom/jsdom/issues/2475
// - `autofocus`: https://github.com/jsdom/jsdom/issues/3041
// - `inert`: https://github.com/jsdom/jsdom/issues/3605
// - etc...: https://github.com/jestjs/jest/issues/139#issuecomment-592673550
skip_mode: ['client'],
html: `
<script nomodule async defer></script>
<form novalidate></form>
<input readonly required checked webkitdirectory>
<select multiple disabled></select>
<button formnovalidate></button>
<img ismap>
<video autoplay controls loop muted playsinline disablepictureinpicture disableremoteplayback></video>
<audio disableremoteplayback></audio>
<track default>
<iframe allowfullscreen></iframe>
<details open></details>
<ol reversed></ol>
<div autofocus></div>
<span inert></span>
<script nomodule async defer></script>
<form novalidate></form>
<input readonly required checked webkitdirectory>
<select multiple disabled></select>
<button formnovalidate></button>
<img ismap>
<video autoplay controls loop muted playsinline disablepictureinpicture disableremoteplayback></video>
<audio disableremoteplayback></audio>
<track default>
<iframe allowfullscreen></iframe>
<details open></details>
<ol reversed></ol>
<div autofocus></div>
<span inert></span>
<script></script>
<form></form>
<input>
<select></select>
<button></button>
<img>
<video></video>
<audio></audio>
<track>
<iframe></iframe>
<details></details>
<ol></ol>
<div></div>
<span></span>
`
});

@ -0,0 +1,22 @@
<script>
let runesMode = $state('using a rune so that we trigger runes mode');
const attributeValues = [true, 'test', false];
</script>
{#each attributeValues as val}
<script NOMODULE={val} ASYNC={val} DEFER={val}></script>
<form NOVALIDATE={val}></form>
<input READONLY={val} REQUIRED={val} CHECKED={val} WEBKITDIRECTORY={val} />
<select MULTIPLE={val} DISABLED={val}></select>
<button FORMNOVALIDATE={val}></button>
<img ISMAP={val} />
<video AUTOPLAY={val} CONTROLS={val} LOOP={val} MUTED={val} PLAYSINLINE={val} DISABLEPICTUREINPICTURE={val} DISABLEREMOTEPLAYBACK={val}></video>
<audio DISABLEREMOTEPLAYBACK={val}></audio>
<track DEFAULT={val} />
<iframe ALLOWFULLSCREEN={val}></iframe>
<details OPEN={val}></details>
<ol REVERSED={val}></ol>
<div AUTOFOCUS={val}></div>
<span INERT={val}></span>
{/each}

@ -0,0 +1,19 @@
import { test } from '../../test';
export default test({
mode: ['client', 'server'],
async test({ assert, target }) {
const my_element = /** @type HTMLElement & { object: { test: true }; } */ (
target.querySelector('my-element')
);
const my_link = /** @type HTMLAnchorElement & { object: { test: true }; } */ (
target.querySelector('a')
);
assert.equal(my_element.getAttribute('string'), 'test');
assert.equal(my_element.hasAttribute('object'), false);
assert.deepEqual(my_element.object, { test: true });
assert.equal(my_link.getAttribute('string'), 'test');
assert.equal(my_link.hasAttribute('object'), false);
assert.deepEqual(my_link.object, { test: true });
}
});

@ -0,0 +1,2 @@
<my-element string="test" object={{ test: true }}></my-element>
<a is="my-link" string="test" object={{ test: true }}></a>

@ -0,0 +1,49 @@
import { test, ok } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client'],
async test({ assert, target }) {
/**
* @type {HTMLInputElement | null}
*/
const input = target.querySelector('input[type=text]');
/**
* @type {HTMLButtonElement | null}
*/
const setString = target.querySelector('#setString');
/**
* @type {HTMLButtonElement | null}
*/
const setNull = target.querySelector('#setNull');
/**
* @type {HTMLButtonElement | null}
*/
const setUndefined = target.querySelector('#setUndefined');
ok(input);
ok(setString);
ok(setNull);
ok(setUndefined);
// value should always be blank string when value attribute is set to null or undefined
assert.equal(input.value, '');
setString.click();
flushSync();
assert.equal(input.value, 'foo');
setNull.click();
flushSync();
assert.equal(input.value, '');
setString.click();
flushSync();
assert.equal(input.value, 'foo');
setUndefined.click();
flushSync();
assert.equal(input.value, '');
}
});

@ -0,0 +1,9 @@
<script>
let value = $state();
</script>
<input type="text" {value} />
<button id="setString" onclick={() => {value = "foo";}}></button>
<button id="setNull" onclick={() => {value = null;}}></button>
<button id="setUndefined" onclick={() => {value = undefined;}}></button>
Loading…
Cancel
Save