[feat] Support style props for SVG components (#7859)

pull/7819/head
Tan Li Hau 2 years ago committed by GitHub
parent ea2f83adeb
commit 5c5bc27d97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1374,6 +1374,22 @@ Desugars to this:
--- ---
For SVG namespace, the example above desugars into using `<g>` instead:
```sv
<g style="--rail-color: black; --track-color: rgb(0, 0, 255)">
<Slider
bind:value
min={0}
max={100}
/>
</g>
```
**Note**: Since this is an extra `<g>`, beware that your CSS structure might accidentally target this. Be mindful of this added wrapper element when using this feature.
---
Svelte's CSS Variables support allows for easily themable components: Svelte's CSS Variables support allows for easily themable components:
```sv ```sv

@ -22,6 +22,7 @@ export default class InlineComponent extends Node {
css_custom_properties: Attribute[] = []; css_custom_properties: Attribute[] = [];
children: INode[]; children: INode[];
scope: TemplateScope; scope: TemplateScope;
namespace: string;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
@ -33,6 +34,7 @@ export default class InlineComponent extends Node {
} }
this.name = info.name; this.name = info.name;
this.namespace = get_namespace(parent, component.namespace);
this.expression = this.name === 'svelte:component' this.expression = this.name === 'svelte:component'
? new Expression(component, this, scope, info.expression) ? new Expression(component, this, scope, info.expression)
@ -165,3 +167,13 @@ export default class InlineComponent extends Node {
function not_whitespace_text(node) { function not_whitespace_text(node) {
return !(node.type === 'Text' && /^\s+$/.test(node.data)); return !(node.type === 'Text' && /^\s+$/.test(node.data));
} }
function get_namespace(parent: Node, explicit_namespace: string) {
const parent_element = parent.find_nearest(/^Element/);
if (!parent_element) {
return explicit_namespace;
}
return parent_element.namespace;
}

@ -20,6 +20,7 @@ import { string_to_member_expression } from '../../../utils/string_to_member_exp
import SlotTemplate from '../../../nodes/SlotTemplate'; import SlotTemplate from '../../../nodes/SlotTemplate';
import { is_head } from '../shared/is_head'; import { is_head } from '../shared/is_head';
import compiler_warnings from '../../../compiler_warnings'; import compiler_warnings from '../../../compiler_warnings';
import { namespaces } from '../../../../utils/namespaces';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
@ -150,7 +151,9 @@ export default class InlineComponentWrapper extends Wrapper {
} }
const has_css_custom_properties = this.node.css_custom_properties.length > 0; const has_css_custom_properties = this.node.css_custom_properties.length > 0;
const css_custom_properties_wrapper = has_css_custom_properties ? block.get_unique_name('div') : null; const is_svg_namespace = this.node.namespace === namespaces.svg;
const css_custom_properties_wrapper_element = is_svg_namespace ? 'g' : 'div';
const css_custom_properties_wrapper = has_css_custom_properties ? block.get_unique_name(css_custom_properties_wrapper_element) : null;
if (has_css_custom_properties) { if (has_css_custom_properties) {
block.add_variable(css_custom_properties_wrapper); block.add_variable(css_custom_properties_wrapper);
} }
@ -411,7 +414,7 @@ export default class InlineComponentWrapper extends Wrapper {
const snippet = this.node.expression.manipulate(block); const snippet = this.node.expression.manipulate(block);
if (has_css_custom_properties) { if (has_css_custom_properties) {
this.set_css_custom_properties(block, css_custom_properties_wrapper); this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
} }
block.chunks.init.push(b` block.chunks.init.push(b`
@ -440,7 +443,7 @@ export default class InlineComponentWrapper extends Wrapper {
block.chunks.mount.push(b`if (${name}) @mount_component(${name}, ${mount_target}, ${mount_anchor});`); block.chunks.mount.push(b`if (${name}) @mount_component(${name}, ${mount_target}, ${mount_anchor});`);
if (to_claim) { if (to_claim) {
if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper); if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
block.chunks.claim.push(b`if (${name}) @claim_component(${name}.$$.fragment, ${claim_nodes});`); block.chunks.claim.push(b`if (${name}) @claim_component(${name}.$$.fragment, ${claim_nodes});`);
} }
@ -514,7 +517,7 @@ export default class InlineComponentWrapper extends Wrapper {
`); `);
if (has_css_custom_properties) { if (has_css_custom_properties) {
this.set_css_custom_properties(block, css_custom_properties_wrapper); this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
} }
block.chunks.create.push(b`@create_component(${name}.$$.fragment);`); block.chunks.create.push(b`@create_component(${name}.$$.fragment);`);
@ -522,7 +525,7 @@ export default class InlineComponentWrapper extends Wrapper {
block.chunks.mount.push(b`@mount_component(${name}, ${mount_target}, ${mount_anchor});`); block.chunks.mount.push(b`@mount_component(${name}, ${mount_target}, ${mount_anchor});`);
if (to_claim) { if (to_claim) {
if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper); if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
block.chunks.claim.push(b`@claim_component(${name}.$$.fragment, ${claim_nodes});`); block.chunks.claim.push(b`@claim_component(${name}.$$.fragment, ${claim_nodes});`);
} }
@ -568,11 +571,14 @@ export default class InlineComponentWrapper extends Wrapper {
private create_css_custom_properties_wrapper_claim_chunk( private create_css_custom_properties_wrapper_claim_chunk(
block: Block, block: Block,
parent_nodes: Identifier, parent_nodes: Identifier,
css_custom_properties_wrapper: Identifier | null css_custom_properties_wrapper: Identifier | null,
css_custom_properties_wrapper_element: string,
is_svg_namespace: boolean
) { ) {
const nodes = block.get_unique_name(`${css_custom_properties_wrapper.name}_nodes`); const nodes = block.get_unique_name(`${css_custom_properties_wrapper.name}_nodes`);
const claim_element = is_svg_namespace ? x`@claim_svg_element` : x`@claim_element`;
block.chunks.claim.push(b` block.chunks.claim.push(b`
${css_custom_properties_wrapper} = @claim_element(${parent_nodes}, "DIV", { style: true }) ${css_custom_properties_wrapper} = ${claim_element}(${parent_nodes}, "${css_custom_properties_wrapper_element.toUpperCase()}", { style: true })
var ${nodes} = @children(${css_custom_properties_wrapper}); var ${nodes} = @children(${css_custom_properties_wrapper});
`); `);
return nodes; return nodes;
@ -580,10 +586,13 @@ export default class InlineComponentWrapper extends Wrapper {
private set_css_custom_properties( private set_css_custom_properties(
block: Block, block: Block,
css_custom_properties_wrapper: Identifier css_custom_properties_wrapper: Identifier,
css_custom_properties_wrapper_element: string,
is_svg_namespace: boolean
) { ) {
block.chunks.create.push(b`${css_custom_properties_wrapper} = @element("div");`); const element = is_svg_namespace ? x`@svg_element` : x`@element`;
block.chunks.hydrate.push(b`@set_style(${css_custom_properties_wrapper}, "display", "contents");`); block.chunks.create.push(b`${css_custom_properties_wrapper} = ${element}("${css_custom_properties_wrapper_element}");`);
if (!is_svg_namespace) block.chunks.hydrate.push(b`@set_style(${css_custom_properties_wrapper}, "display", "contents");`);
this.node.css_custom_properties.forEach((attr) => { this.node.css_custom_properties.forEach((attr) => {
const dependencies = attr.get_dependencies(); const dependencies = attr.get_dependencies();
const should_cache = attr.should_cache(); const should_cache = attr.should_cache();

@ -3,6 +3,7 @@ import { get_attribute_value } from './shared/get_attribute_value';
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
import InlineComponent from '../../nodes/InlineComponent'; import InlineComponent from '../../nodes/InlineComponent';
import { p, x } from 'code-red'; import { p, x } from 'code-red';
import { namespaces } from '../../../utils/namespaces';
function get_prop_value(attribute) { function get_prop_value(attribute) {
if (attribute.is_true) return x`true`; if (attribute.is_true) return x`true`;
@ -88,11 +89,16 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
}`; }`;
if (node.css_custom_properties.length > 0) { if (node.css_custom_properties.length > 0) {
if (node.namespace === namespaces.svg) {
renderer.add_string('<g style="');
} else {
renderer.add_string('<div style="display: contents; '); renderer.add_string('<div style="display: contents; ');
node.css_custom_properties.forEach(attr => { }
node.css_custom_properties.forEach((attr, index) => {
renderer.add_string(`${attr.name}:`); renderer.add_string(`${attr.name}:`);
renderer.add_expression(get_attribute_value(attr)); renderer.add_expression(get_attribute_value(attr));
renderer.add_string(';'); renderer.add_string(';');
if (index < node.css_custom_properties.length - 1) renderer.add_string(' ');
}); });
renderer.add_string('">'); renderer.add_string('">');
} }
@ -100,6 +106,10 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
renderer.add_expression(x`@validate_component(${expression}, "${node.name}").$$render($$result, ${props}, ${bindings}, ${slots})`); renderer.add_expression(x`@validate_component(${expression}, "${node.name}").$$render($$result, ${props}, ${bindings}, ${slots})`);
if (node.css_custom_properties.length > 0) { if (node.css_custom_properties.length > 0) {
if (node.namespace === namespaces.svg) {
renderer.add_string('</g>');
} else {
renderer.add_string('</div>'); renderer.add_string('</div>');
} }
} }
}

@ -25,4 +25,4 @@ export const valid_namespaces = [
xmlns xmlns
]; ];
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns }; export const namespaces = { foreign, html, mathml, svg, xlink, xml, xmlns } as const;

@ -0,0 +1,17 @@
<script>
export let id;
</script>
<g {id}>
<circle cx="50" cy="50" r="10" />
<rect width="100" height="100" />
</g>
<style>
circle {
fill: var(--circle-color);
}
rect {
fill: var(--rect-color)
}
</style>

@ -0,0 +1,55 @@
export default {
props: {
rectColor1: 'green',
circleColor1: 'red',
rectColor2: 'black',
circleColor2: 'blue'
},
html: `
<svg xmlns="http://www.w3.org/2000/svg">
<g style="--rect-color:green; --circle-color:red;">
<g id="svg-1">
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
</g>
</g>
<g style="--rect-color:black; --circle-color:blue;">
<g id="svg-2">
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
</g>
</g>
</svg>
`,
test({ component, assert, target }) {
component.rectColor1 = 'yellow';
component.circleColor2 = 'cyan';
assert.htmlEqual(target.innerHTML, `
<svg xmlns="http://www.w3.org/2000/svg">
<g style="--rect-color:yellow; --circle-color:red;">
<g id="svg-1">
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
</g>
</g>
<g style="--rect-color:black; --circle-color:cyan;">
<g id="svg-2">
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
</g>
</g>
</svg>
`);
const circleColor1 = target.querySelector('#svg-1 circle');
const rectColor1 = target.querySelector('#svg-1 rect');
const circleColor2 = target.querySelector('#svg-2 circle');
const rectColor2 = target.querySelector('#svg-2 rect');
assert.htmlEqual(window.getComputedStyle(circleColor1).fill, 'rgb(255, 0, 0)');
assert.htmlEqual(window.getComputedStyle(rectColor1).fill, 'rgb(255, 255, 0)');
assert.htmlEqual(window.getComputedStyle(circleColor2).fill, 'rgb(0, 255, 255)');
assert.htmlEqual(window.getComputedStyle(rectColor2).fill, 'rgb(0, 0, 0)');
}
};

@ -0,0 +1,25 @@
<script>
import Svg from './Svg.svelte';
export let rectColor1;
export let rectColor2;
export let circleColor1;
export let circleColor2;
function identity(color) {
return color;
}
</script>
<svg xmlns="http://www.w3.org/2000/svg">
<Svg
id="svg-1"
--rect-color={rectColor1}
--circle-color={circleColor1}
/>
<Svg
id="svg-2"
--rect-color={rectColor2}
--circle-color={identity(circleColor2)}
/>
</svg>
Loading…
Cancel
Save