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,
attributes_id,
(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
);

@ -4,6 +4,7 @@
import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { is_custom_element_node } from '../../../../nodes.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) {
if (node.type !== 'RegularElement') 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) {
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).
return { value, has_state };
} else {
let expression = value;
// only add nullish coallescence if it hasn't been added already
if (value.type === 'LogicalExpression' && value.operator === '??') {
const { right } = value;
// `undefined` isn't a Literal (due to pre-ES5 shenanigans), so the only nullish literal is `null`
// 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') {
expression = b.logical('??', value, b.literal(''));
} else if (right.value === null) {
// if they do something weird like `stuff ?? null`, replace `null` with empty string
value.right = b.literal('');
// add `?? ''` where necessary (TODO optimise more cases)
if (
value.type === 'LogicalExpression' &&
value.right.type === 'Literal' &&
(value.operator === '??' || value.operator === '||')
) {
// `foo ?? null` -=> `foo ?? ''`
// otherwise leave the expression untouched
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
} 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);

@ -23,10 +23,14 @@ export function is_element_node(node) {
/**
* @param {AST.RegularElement | AST.SvelteElement} node
* @returns {node is AST.RegularElement}
* @returns {boolean}
*/
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
value ?? undefined) ||
// @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'))
) {
return;
}
// @ts-expect-error
element.value = value;
element.value = value ?? '';
}
/**

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