diff --git a/.changeset/thin-panthers-sing.md b/.changeset/thin-panthers-sing.md new file mode 100644 index 000000000000..223de002c605 --- /dev/null +++ b/.changeset/thin-panthers-sing.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow `class` attribute to be an object or array, using `clsx` diff --git a/documentation/docs/03-template-syntax/16-class.md b/documentation/docs/03-template-syntax/16-class.md deleted file mode 100644 index cecbc7cf2041..000000000000 --- a/documentation/docs/03-template-syntax/16-class.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: class: ---- - -The `class:` directive is a convenient way to conditionally set classes on elements, as an alternative to using conditional expressions inside `class` attributes: - -```svelte - -
...
-
...
-``` - -As with other directives, we can use a shorthand when the name of the class coincides with the value: - -```svelte -
...
-``` - -Multiple `class:` directives can be added to a single element: - -```svelte -
...
-``` diff --git a/documentation/docs/03-template-syntax/18-class.md b/documentation/docs/03-template-syntax/18-class.md new file mode 100644 index 000000000000..880a34e9ec53 --- /dev/null +++ b/documentation/docs/03-template-syntax/18-class.md @@ -0,0 +1,90 @@ +--- +title: class +--- + +There are two ways to set classes on elements: the `class` attribute, and the `class:` directive. + +## Attributes + +Primitive values are treated like any other attribute: + +```svelte +
...
+``` + +> [!NOTE] +> For historical reasons, falsy values (like `false` and `NaN`) are stringified (`class="false"`), though `class={undefined}` (or `null`) cause the attribute to be omitted altogether. In a future version of Svelte, all falsy values will cause `class` to be omitted. + +### Objects and arrays + +Since Svelte 5.16, `class` can be an object or array, and is converted to a string using [clsx](https://github.com/lukeed/clsx). + +If the value is an object, the truthy keys are added: + +```svelte + + + +
...
+``` + +If the value is an array, the truthy values are combined: + +```svelte + +
...
+``` + +Note that whether we're using the array or object form, we can set multiple classes simultaneously with a single condition, which is particularly useful if you're using things like Tailwind. + +Arrays can contain arrays and objects, and clsx will flatten them. This is useful for combining local classes with props, for example: + +```svelte + + + + +``` + +The user of this component has the same flexibility to use a mixture of objects, arrays and strings: + +```svelte + + + + +``` + +## The `class:` directive + +Prior to Svelte 5.16, the `class:` directive was the most convenient way to set classes on elements conditionally. + +```svelte + +
...
+
...
+``` + +As with other directives, we can use a shorthand when the name of the class coincides with the value: + +```svelte +
...
+``` + +> [!NOTE] Unless you're using an older version of Svelte, consider avoiding `class:`, since the attribute is more powerful and composable. diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 8800b65172dc..604403f0a261 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -741,7 +741,7 @@ export interface HTMLAttributes extends AriaAttributes, D accesskey?: string | undefined | null; autocapitalize?: 'characters' | 'off' | 'on' | 'none' | 'sentences' | 'words' | undefined | null; autofocus?: boolean | undefined | null; - class?: string | undefined | null; + class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null; contenteditable?: Booleanish | 'inherit' | 'plaintext-only' | undefined | null; contextmenu?: string | undefined | null; dir?: 'ltr' | 'rtl' | 'auto' | undefined | null; @@ -1522,7 +1522,7 @@ export interface SvelteWindowAttributes extends HTMLAttributes { export interface SVGAttributes extends AriaAttributes, DOMAttributes { // Attributes which also defined in HTMLAttributes className?: string | undefined | null; - class?: string | undefined | null; + class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null; color?: string | undefined | null; height?: number | string | undefined | null; id?: string | undefined | null; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 05169a7bc2ab..b9f4d3de0951 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -153,6 +153,7 @@ "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", + "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.3.2", "is-reference": "^3.0.3", diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 35bc675166ae..ca7476ef7fc1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -731,7 +731,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv /** @type {string[]} */ let prev_values = []; for (const chunk of chunks) { - const current_possible_values = get_possible_values(chunk); + const current_possible_values = get_possible_values(chunk, name === 'class'); // impossible to find out all combinations if (!current_possible_values) return true; @@ -784,7 +784,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv prev_values.push(current_possible_value); } }); - if (prev_values.length < current_possible_values.size) { + if (prev_values.length < current_possible_values.length) { prev_values.push(' '); } if (prev_values.length > 20) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js index d3fd71ec395b..d7544b55c063 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -4,14 +4,74 @@ const UNKNOWN = {}; /** * @param {Node} node + * @param {boolean} is_class * @param {Set} set + * @param {boolean} is_nested */ -function gather_possible_values(node, set) { +function gather_possible_values(node, is_class, set, is_nested = false) { + if (set.has(UNKNOWN)) { + // no point traversing any further + return; + } + if (node.type === 'Literal') { set.add(String(node.value)); } else if (node.type === 'ConditionalExpression') { - gather_possible_values(node.consequent, set); - gather_possible_values(node.alternate, set); + gather_possible_values(node.consequent, is_class, set, is_nested); + gather_possible_values(node.alternate, is_class, set, is_nested); + } else if (node.type === 'LogicalExpression') { + if (node.operator === '&&') { + // && is a special case, because the only way the left + // hand value can be included is if it's falsy. this is + // a bit of extra work but it's worth it because + // `class={[condition && 'blah']}` is common, + // and we don't want to deopt on `condition` + const left = new Set(); + gather_possible_values(node.left, is_class, left, is_nested); + + if (left.has(UNKNOWN)) { + // add all non-nullish falsy values, unless this is a `class` attribute that + // will be processed by cslx, in which case falsy values are removed, unless + // they're not inside an array/object (TODO 6.0 remove that last part) + if (!is_class || !is_nested) { + set.add(''); + set.add(false); + set.add(NaN); + set.add(0); // -0 and 0n are also falsy, but stringify to '0' + } + } else { + for (const value of left) { + if (!value && value != undefined && (!is_class || !is_nested)) { + set.add(value); + } + } + } + + gather_possible_values(node.right, is_class, set, is_nested); + } else { + gather_possible_values(node.left, is_class, set, is_nested); + gather_possible_values(node.right, is_class, set, is_nested); + } + } else if (is_class && node.type === 'ArrayExpression') { + for (const entry of node.elements) { + if (entry) { + gather_possible_values(entry, is_class, set, true); + } + } + } else if (is_class && node.type === 'ObjectExpression') { + for (const property of node.properties) { + if ( + property.type === 'Property' && + !property.computed && + (property.key.type === 'Identifier' || property.key.type === 'Literal') + ) { + set.add( + property.key.type === 'Identifier' ? property.key.name : String(property.key.value) + ); + } else { + set.add(UNKNOWN); + } + } } else { set.add(UNKNOWN); } @@ -19,19 +79,20 @@ function gather_possible_values(node, set) { /** * @param {AST.Text | AST.ExpressionTag} chunk - * @returns {Set | null} + * @param {boolean} is_class + * @returns {string[] | null} */ -export function get_possible_values(chunk) { +export function get_possible_values(chunk, is_class) { const values = new Set(); if (chunk.type === 'Text') { values.add(chunk.data); } else { - gather_possible_values(chunk.expression, values); + gather_possible_values(chunk.expression, is_class, values); } if (values.has(UNKNOWN)) return null; - return values; + return [...values].map((value) => String(value)); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c0e4a655712c..76c1e94277be 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -773,6 +773,8 @@ export function analyze_component(root, source, options) { if (attribute.type !== 'Attribute') continue; if (attribute.name.toLowerCase() !== 'class') continue; + // The dynamic class method appends the hash to the end of the class attribute on its own + if (attribute.metadata.needs_clsx) continue outer; class_attribute = attribute; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 6eb9faca6d4e..9d801e095e8d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -38,6 +38,19 @@ export function Attribute(node, context) { mark_subtree_dynamic(context.path); } + // class={[...]} or class={{...}} or `class={x}` need clsx to resolve the classes + if ( + node.name === 'class' && + !Array.isArray(node.value) && + node.value !== true && + node.value.expression.type !== 'Literal' && + node.value.expression.type !== 'TemplateLiteral' && + node.value.expression.type !== 'BinaryExpression' + ) { + mark_subtree_dynamic(context.path); + node.metadata.needs_clsx = true; + } + if (node.value !== true) { for (const chunk of get_attribute_chunks(node.value)) { if (chunk.type !== 'ExpressionTag') continue; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 2c2c287f1275..59a6fafbc5d7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -553,6 +553,10 @@ function build_element_attribute_update_assignment( let update; if (name === 'class') { + if (attribute.metadata.needs_clsx) { + value = b.call('$.clsx', value); + } + if (attribute.metadata.expression.has_state && has_call) { // ensure we're not creating a separate template effect for this so that // potential class directives are added to the same effect and therefore always apply @@ -561,11 +565,13 @@ function build_element_attribute_update_assignment( value = b.call('$.get', id); has_call = false; } + update = b.stmt( b.call( is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class', node_id, - value + value, + attribute.metadata.needs_clsx ? b.literal(context.state.analysis.css.hash) : undefined ) ); } else if (name === 'value') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 5ebc6475713f..d0d800d3cbc5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -86,10 +86,35 @@ export function build_element_attributes(node, context) { } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { if (attribute.name === 'class') { class_index = attributes.length; - } else if (attribute.name === 'style') { - style_index = attributes.length; + + if (attribute.metadata.needs_clsx) { + const clsx_value = b.call( + '$.clsx', + /** @type {AST.ExpressionTag} */ (attribute.value).expression + ); + attributes.push({ + ...attribute, + value: { + .../** @type {AST.ExpressionTag} */ (attribute.value), + expression: context.state.analysis.css.hash + ? b.binary( + '+', + b.binary('+', clsx_value, b.literal(' ')), + b.literal(context.state.analysis.css.hash) + ) + : clsx_value + } + }); + } else { + attributes.push(attribute); + } + } else { + if (attribute.name === 'style') { + style_index = attributes.length; + } + + attributes.push(attribute); } - attributes.push(attribute); } } else if (attribute.type === 'BindDirective') { if (attribute.name === 'value' && node.name === 'select') continue; diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 7aa9fecd3aba..5066833feb8e 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -45,7 +45,8 @@ export function create_attribute(name, start, end, value) { value, metadata: { expression: create_expression_metadata(), - delegated: null + delegated: null, + needs_clsx: false } }; } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 97a25df4a758..8be9aed17723 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -482,6 +482,8 @@ export namespace AST { expression: ExpressionMetadata; /** May be set if this is an event attribute */ delegated: null | DelegatedEvent; + /** May be `true` if this is a `class` attribute that needs `clsx` */ + needs_clsx: boolean; }; } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 9c62d684c183..6656532986d7 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -13,6 +13,7 @@ import { set_active_effect, set_active_reaction } from '../../runtime.js'; +import { clsx } from '../../../shared/attributes.js'; /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -267,6 +268,10 @@ export function set_attributes( } } + if (next.class) { + next.class = clsx(next.class); + } + if (css_hash !== undefined) { next.class = next.class ? next.class + ' ' + css_hash : css_hash; } diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 22f3da0f44f9..62ffb6d14b5c 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -3,12 +3,13 @@ import { hydrating } from '../hydration.js'; /** * @param {SVGElement} dom * @param {string} value + * @param {string} [hash] * @returns {void} */ -export function set_svg_class(dom, value) { +export function set_svg_class(dom, value, hash) { // @ts-expect-error need to add __className to patched prototype var prev_class_name = dom.__className; - var next_class_name = to_class(value); + var next_class_name = to_class(value, hash); if (hydrating && dom.getAttribute('class') === next_class_name) { // In case of hydration don't reset the class as it's already correct. @@ -32,12 +33,13 @@ export function set_svg_class(dom, value) { /** * @param {MathMLElement} dom * @param {string} value + * @param {string} [hash] * @returns {void} */ -export function set_mathml_class(dom, value) { +export function set_mathml_class(dom, value, hash) { // @ts-expect-error need to add __className to patched prototype var prev_class_name = dom.__className; - var next_class_name = to_class(value); + var next_class_name = to_class(value, hash); if (hydrating && dom.getAttribute('class') === next_class_name) { // In case of hydration don't reset the class as it's already correct. @@ -61,12 +63,13 @@ export function set_mathml_class(dom, value) { /** * @param {HTMLElement} dom * @param {string} value + * @param {string} [hash] * @returns {void} */ -export function set_class(dom, value) { +export function set_class(dom, value, hash) { // @ts-expect-error need to add __className to patched prototype var prev_class_name = dom.__className; - var next_class_name = to_class(value); + var next_class_name = to_class(value, hash); if (hydrating && dom.className === next_class_name) { // In case of hydration don't reset the class as it's already correct. @@ -79,7 +82,7 @@ export function set_class(dom, value) { // Removing the attribute when the value is only an empty string causes // peformance issues vs simply making the className an empty string. So // we should only remove the class if the the value is nullish. - if (value == null) { + if (value == null && !hash) { dom.removeAttribute('class'); } else { dom.className = next_class_name; @@ -93,10 +96,11 @@ export function set_class(dom, value) { /** * @template V * @param {V} value + * @param {string} [hash] * @returns {string | V} */ -function to_class(value) { - return value == null ? '' : value; +function to_class(value, hash) { + return (value == null ? '' : value) + (hash ? ' ' + hash : ''); } /** diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index f22c33babc52..3b85ae18166e 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -161,7 +161,7 @@ export { $window as window, $document as document } from './dom/operations.js'; -export { attr } from '../shared/attributes.js'; +export { attr, clsx } from '../shared/attributes.js'; export { snapshot } from '../shared/clone.js'; export { noop, fallback } from '../shared/utils.js'; export { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b8371b7e008f..89b3c33df887 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -2,7 +2,7 @@ /** @import { Component, Payload, RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; -import { attr } from '../shared/attributes.js'; +import { attr, clsx } from '../shared/attributes.js'; import { is_promise, noop } from '../shared/utils.js'; import { subscribe_to_store } from '../../store/utils.js'; import { @@ -195,6 +195,10 @@ export function spread_attributes(attrs, classes, styles, flags = 0) { : style_object_to_string(styles); } + if (attrs.class) { + attrs.class = clsx(attrs.class); + } + if (classes) { const classlist = attrs.class ? [attrs.class] : []; @@ -522,7 +526,7 @@ export function once(get_value) { }; } -export { attr }; +export { attr, clsx }; export { html } from './blocks/html.js'; diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index 867d6ba5d378..a561501bf4f6 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -1,4 +1,5 @@ import { escape_html } from '../../escaping.js'; +import { clsx as _clsx } from 'clsx'; /** * `
` should be rendered as `
` and _not_ @@ -26,3 +27,16 @@ export function attr(name, value, is_boolean = false) { const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; } + +/** + * Small wrapper around clsx to preserve Svelte's (weird) handling of falsy values. + * TODO Svelte 6 revisit this, and likely turn all falsy values into the empty string (what clsx also does) + * @param {any} value + */ +export function clsx(value) { + if (typeof value === 'object') { + return _clsx(value); + } else { + return value ?? ''; + } +} diff --git a/packages/svelte/tests/css/samples/clsx-can-prune/_config.js b/packages/svelte/tests/css/samples/clsx-can-prune/_config.js new file mode 100644 index 000000000000..43c80a318d52 --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-can-prune/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused"\nhttps://svelte.dev/e/css_unused_selector', + start: { + line: 24, + column: 1, + character: 548 + }, + end: { + line: 24, + column: 8, + character: 555 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/clsx-can-prune/expected.css b/packages/svelte/tests/css/samples/clsx-can-prune/expected.css new file mode 100644 index 000000000000..620db0f022aa --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-can-prune/expected.css @@ -0,0 +1,12 @@ + + .used1.svelte-xyz { color: green; } + .used2.svelte-xyz { color: green; } + .used3.svelte-xyz { color: green; } + .used4.svelte-xyz { color: green; } + .used5.svelte-xyz { color: green; } + .used6.svelte-xyz { color: green; } + .used7.svelte-xyz { color: green; } + .used8.svelte-xyz { color: green; } + .used9.svelte-xyz { color: green; } + + /* (unused) .unused { color: red; }*/ diff --git a/packages/svelte/tests/css/samples/clsx-can-prune/input.svelte b/packages/svelte/tests/css/samples/clsx-can-prune/input.svelte new file mode 100644 index 000000000000..2c75a39f9996 --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-can-prune/input.svelte @@ -0,0 +1,25 @@ + + +

+

+

+

+

+

+

+ + diff --git a/packages/svelte/tests/css/samples/clsx-cannot-prune-1/expected.css b/packages/svelte/tests/css/samples/clsx-cannot-prune-1/expected.css new file mode 100644 index 000000000000..243bdb2b82a6 --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-cannot-prune-1/expected.css @@ -0,0 +1,2 @@ + + .x.svelte-xyz { color: green; } diff --git a/packages/svelte/tests/css/samples/clsx-cannot-prune-1/input.svelte b/packages/svelte/tests/css/samples/clsx-cannot-prune-1/input.svelte new file mode 100644 index 000000000000..d9de0bdfd47b --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-cannot-prune-1/input.svelte @@ -0,0 +1,5 @@ +

hello world

+ + diff --git a/packages/svelte/tests/css/samples/clsx-cannot-prune-2/expected.css b/packages/svelte/tests/css/samples/clsx-cannot-prune-2/expected.css new file mode 100644 index 000000000000..243bdb2b82a6 --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-cannot-prune-2/expected.css @@ -0,0 +1,2 @@ + + .x.svelte-xyz { color: green; } diff --git a/packages/svelte/tests/css/samples/clsx-cannot-prune-2/input.svelte b/packages/svelte/tests/css/samples/clsx-cannot-prune-2/input.svelte new file mode 100644 index 000000000000..212deab4f060 --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-cannot-prune-2/input.svelte @@ -0,0 +1,5 @@ +

hello world

+ + diff --git a/packages/svelte/tests/css/samples/clsx-cannot-prune-3/expected.css b/packages/svelte/tests/css/samples/clsx-cannot-prune-3/expected.css new file mode 100644 index 000000000000..243bdb2b82a6 --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-cannot-prune-3/expected.css @@ -0,0 +1,2 @@ + + .x.svelte-xyz { color: green; } diff --git a/packages/svelte/tests/css/samples/clsx-cannot-prune-3/input.svelte b/packages/svelte/tests/css/samples/clsx-cannot-prune-3/input.svelte new file mode 100644 index 000000000000..2dfa1afd18aa --- /dev/null +++ b/packages/svelte/tests/css/samples/clsx-cannot-prune-3/input.svelte @@ -0,0 +1,5 @@ +

hello world

+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-no-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-no-style/_config.js index a7518f7e6cf2..b2f039618153 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-no-style/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-no-style/_config.js @@ -40,7 +40,7 @@ export default test({ assert.equal(div.className, 'true'); component.testName = {}; - assert.equal(div.className, '[object Object]'); + assert.equal(div.className, ''); component.testName = ''; assert.equal(div.className, ''); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js index dbab9fd42b98..c8710f9038b9 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js @@ -32,7 +32,7 @@ export default test({ assert.equal(div.className, 'true svelte-x1o6ra'); component.testName = {}; - assert.equal(div.className, '[object Object] svelte-x1o6ra'); + assert.equal(div.className, ' svelte-x1o6ra'); component.testName = ''; assert.equal(div.className, ' svelte-x1o6ra'); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-no-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-no-style/_config.js index a7518f7e6cf2..b2f039618153 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-no-style/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-no-style/_config.js @@ -40,7 +40,7 @@ export default test({ assert.equal(div.className, 'true'); component.testName = {}; - assert.equal(div.className, '[object Object]'); + assert.equal(div.className, ''); component.testName = ''; assert.equal(div.className, ''); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js index 20426dcf4b4b..8d0f411b8fd2 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js @@ -40,7 +40,7 @@ export default test({ assert.equal(div.className, 'true svelte-x1o6ra'); component.testName = {}; - assert.equal(div.className, '[object Object] svelte-x1o6ra'); + assert.equal(div.className, ' svelte-x1o6ra'); component.testName = ''; assert.equal(div.className, ' svelte-x1o6ra'); diff --git a/packages/svelte/tests/runtime-runes/samples/clsx/_config.js b/packages/svelte/tests/runtime-runes/samples/clsx/_config.js new file mode 100644 index 000000000000..e0813d0e6c40 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/clsx/_config.js @@ -0,0 +1,43 @@ +import { test } from '../../test'; + +export default test({ + html: ` +
+
+
+
+
+ +
child
+
child
+
child
+
child
+
child
+ + + `, + test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + + assert.htmlEqual( + target.innerHTML, + ` +
+
+
+
+
+ +
child
+
child
+
child
+
child
+
child
+ + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/clsx/child.svelte b/packages/svelte/tests/runtime-runes/samples/clsx/child.svelte new file mode 100644 index 000000000000..1b8be697c052 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/clsx/child.svelte @@ -0,0 +1,5 @@ + + +
child
diff --git a/packages/svelte/tests/runtime-runes/samples/clsx/main.svelte b/packages/svelte/tests/runtime-runes/samples/clsx/main.svelte new file mode 100644 index 000000000000..bf68b42e1108 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/clsx/main.svelte @@ -0,0 +1,33 @@ + + +
+
+
+
+
+ + + + + + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7f38700e4f3..8bf7bb42244b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: axobject-query: specifier: ^4.1.0 version: 4.1.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 esm-env: specifier: ^1.2.1 version: 1.2.1 @@ -890,6 +893,10 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -3055,6 +3062,8 @@ snapshots: ci-info@3.9.0: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4