Skip to content

Commit 27f9dc4

Browse files
authored
[6.x] Support icon sets in names (#14579)
1 parent 6aaa172 commit 27f9dc4

4 files changed

Lines changed: 97 additions & 8 deletions

File tree

resources/js/components/ui/Icon/Icon.vue

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,54 @@ const props = defineProps({
1212
1313
const svgContent = ref('');
1414
const iconComponent = computed(() => ({ template: svgContent.value }));
15+
const delimiter = '::';
16+
17+
const icon = computed(() => {
18+
const delimiterIndex = props.name.indexOf(delimiter);
19+
const hasSetInName = delimiterIndex > 0 && delimiterIndex < props.name.length - delimiter.length;
20+
21+
if (! hasSetInName) {
22+
return { name: props.name, set: props.set, setFromName: null };
23+
}
24+
25+
const set = props.name.substring(0, delimiterIndex);
26+
const name = props.name.substring(delimiterIndex + delimiter.length);
27+
28+
return { name, set, setFromName: set };
29+
});
1530
1631
const loadIcon = async () => {
1732
if (props.name.startsWith('<svg')) {
1833
svgContent.value = DOMPurify.sanitize(props.name);
1934
return;
2035
}
2136
22-
const iconSet = getIconSet(props.set);
37+
const { name, set, setFromName } = icon.value;
38+
39+
if (setFromName && props.set !== 'default' && props.set !== setFromName) {
40+
console.warn(`Icon name [${props.name}] includes set [${setFromName}], ignoring set prop [${props.set}]`);
41+
}
42+
43+
const iconSet = getIconSet(set);
2344
2445
if (!iconSet) {
25-
console.warn(`Icon set [${props.set}] not registered`);
46+
console.warn(`Icon set [${set}] not registered`);
2647
svgContent.value = ''
2748
return
2849
}
2950
3051
let rawSvg = '';
3152
3253
if (iconSet.type === 'strings') {
33-
rawSvg = loadFromStringSet(iconSet.data, props.name);
54+
rawSvg = loadFromStringSet(iconSet.data, name);
3455
} else if (iconSet.type === 'glob') {
35-
rawSvg = await loadFromGlobSet(iconSet.data, props.name);
56+
rawSvg = await loadFromGlobSet(iconSet.data, name);
3657
}
3758
3859
if (!rawSvg) {
39-
console.warn(props.set === 'default'
40-
? `Icon [${props.name}] not found`
41-
: `Icon [${props.name}] not found in set [${props.set}]`);
60+
console.warn(set === 'default'
61+
? `Icon [${name}] not found`
62+
: `Icon [${name}] not found in set [${set}]`);
4263
svgContent.value = ''
4364
return
4465
}

resources/js/stories/Icon.stories.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import type {Meta, StoryObj} from '@storybook/vue3';
2-
import {CardPanel, Icon, Input} from '@ui';
2+
import {CardPanel, Icon, Input, registerIconSetFromStrings} from '@ui';
33
import {computed, ref} from 'vue';
44
import {icons} from "@/stories/icons";
55

6+
registerIconSetFromStrings('storybook', {
7+
spark: `
8+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
9+
<path d="M12 2l1.9 6.1L20 10l-6.1 1.9L12 18l-1.9-6.1L4 10l6.1-1.9L12 2z" />
10+
<path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15z" />
11+
</svg>
12+
`,
13+
});
14+
615
const meta = {
716
title: 'Components/Icon',
817
component: Icon,
@@ -23,6 +32,19 @@ export const Default: Story = {
2332
},
2433
};
2534

35+
export const AlternateSetInName: Story = {
36+
args: {
37+
name: 'storybook::spark',
38+
},
39+
parameters: {
40+
docs: {
41+
source: {
42+
code: '<Icon name="storybook::spark" />',
43+
},
44+
},
45+
},
46+
};
47+
2648
export const _DocsIntro: Story = {
2749
tags: ['!dev'],
2850
args: {

resources/js/stories/docs/Icon.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Icons are used throughout the Control Panel to provide visual cues and improve u
1111
Use the `name` prop to specify which icon to display.
1212
<Canvas of={IconStories.Default} />
1313

14+
## Alternate Sets
15+
Use the `set` prop or prefix the icon name with `set::` to render an icon from another registered set. When both are provided and they disagree, the set in the icon name wins and a warning is logged.
16+
<Canvas of={IconStories.AlternateSetInName} />
17+
1418
## Available Icons
1519
All icons available in the default icon set. Click to copy the icon name.
1620
<Canvas of={IconStories.AllIcons} />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { flushPromises, mount } from '@vue/test-utils';
2+
import { expect, test, vi } from 'vitest';
3+
import Icon from '@/components/ui/Icon/Icon.vue';
4+
import { registerIconSetFromStrings } from '@/components/ui/Icon/registry.js';
5+
6+
registerIconSetFromStrings('icon-test-alternate', {
7+
foo: '<svg><title>Alternate Foo</title></svg>',
8+
});
9+
10+
registerIconSetFromStrings('icon-test-ignored', {
11+
foo: '<svg><title>Ignored Foo</title></svg>',
12+
});
13+
14+
test('it can resolve the icon set from the name', async () => {
15+
const wrapper = mount(Icon, {
16+
props: {
17+
name: 'icon-test-alternate::foo',
18+
},
19+
});
20+
21+
await flushPromises();
22+
23+
expect(wrapper.find('title').text()).toBe('Alternate Foo');
24+
});
25+
26+
test('the icon set from the name takes precedence over the set prop', async () => {
27+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
28+
29+
const wrapper = mount(Icon, {
30+
props: {
31+
name: 'icon-test-alternate::foo',
32+
set: 'icon-test-ignored',
33+
},
34+
});
35+
36+
await flushPromises();
37+
38+
expect(wrapper.find('title').text()).toBe('Alternate Foo');
39+
expect(warn).toHaveBeenCalledWith('Icon name [icon-test-alternate::foo] includes set [icon-test-alternate], ignoring set prop [icon-test-ignored]');
40+
41+
warn.mockRestore();
42+
});

0 commit comments

Comments
 (0)