Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions resources/js/components/ui/Icon/Icon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,54 @@ const props = defineProps({

const svgContent = ref('');
const iconComponent = computed(() => ({ template: svgContent.value }));
const delimiter = '::';

const icon = computed(() => {
const delimiterIndex = props.name.indexOf(delimiter);
const hasSetInName = delimiterIndex > 0 && delimiterIndex < props.name.length - delimiter.length;

if (! hasSetInName) {
return { name: props.name, set: props.set, setFromName: null };
}

const set = props.name.substring(0, delimiterIndex);
const name = props.name.substring(delimiterIndex + delimiter.length);

return { name, set, setFromName: set };
});

const loadIcon = async () => {
if (props.name.startsWith('<svg')) {
svgContent.value = DOMPurify.sanitize(props.name);
return;
}

const iconSet = getIconSet(props.set);
const { name, set, setFromName } = icon.value;

if (setFromName && props.set !== 'default' && props.set !== setFromName) {
console.warn(`Icon name [${props.name}] includes set [${setFromName}], ignoring set prop [${props.set}]`);
}

const iconSet = getIconSet(set);

if (!iconSet) {
console.warn(`Icon set [${props.set}] not registered`);
console.warn(`Icon set [${set}] not registered`);
svgContent.value = ''
return
}

let rawSvg = '';

if (iconSet.type === 'strings') {
rawSvg = loadFromStringSet(iconSet.data, props.name);
rawSvg = loadFromStringSet(iconSet.data, name);
} else if (iconSet.type === 'glob') {
rawSvg = await loadFromGlobSet(iconSet.data, props.name);
rawSvg = await loadFromGlobSet(iconSet.data, name);
}

if (!rawSvg) {
console.warn(props.set === 'default'
? `Icon [${props.name}] not found`
: `Icon [${props.name}] not found in set [${props.set}]`);
console.warn(set === 'default'
? `Icon [${name}] not found`
: `Icon [${name}] not found in set [${set}]`);
svgContent.value = ''
return
}
Expand Down
24 changes: 23 additions & 1 deletion resources/js/stories/Icon.stories.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type {Meta, StoryObj} from '@storybook/vue3';
import {CardPanel, Icon, Input} from '@ui';
import {CardPanel, Icon, Input, registerIconSetFromStrings} from '@ui';
import {computed, ref} from 'vue';
import {icons} from "@/stories/icons";

registerIconSetFromStrings('storybook', {
spark: `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2l1.9 6.1L20 10l-6.1 1.9L12 18l-1.9-6.1L4 10l6.1-1.9L12 2z" />
<path d="M19 15l.8 2.2L22 18l-2.2.8L19 21l-.8-2.2L16 18l2.2-.8L19 15z" />
</svg>
`,
});

const meta = {
title: 'Components/Icon',
component: Icon,
Expand All @@ -23,6 +32,19 @@ export const Default: Story = {
},
};

export const AlternateSetInName: Story = {
args: {
name: 'storybook::spark',
},
parameters: {
docs: {
source: {
code: '<Icon name="storybook::spark" />',
},
},
},
};

export const _DocsIntro: Story = {
tags: ['!dev'],
args: {
Expand Down
4 changes: 4 additions & 0 deletions resources/js/stories/docs/Icon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Icons are used throughout the Control Panel to provide visual cues and improve u
Use the `name` prop to specify which icon to display.
<Canvas of={IconStories.Default} />

## Alternate Sets
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.
<Canvas of={IconStories.AlternateSetInName} />

## Available Icons
All icons available in the default icon set. Click to copy the icon name.
<Canvas of={IconStories.AllIcons} />
Expand Down
42 changes: 42 additions & 0 deletions resources/js/tests/components/Icon.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { flushPromises, mount } from '@vue/test-utils';
import { expect, test, vi } from 'vitest';
import Icon from '@/components/ui/Icon/Icon.vue';
import { registerIconSetFromStrings } from '@/components/ui/Icon/registry.js';

registerIconSetFromStrings('icon-test-alternate', {
foo: '<svg><title>Alternate Foo</title></svg>',
});

registerIconSetFromStrings('icon-test-ignored', {
foo: '<svg><title>Ignored Foo</title></svg>',
});

test('it can resolve the icon set from the name', async () => {
const wrapper = mount(Icon, {
props: {
name: 'icon-test-alternate::foo',
},
});

await flushPromises();

expect(wrapper.find('title').text()).toBe('Alternate Foo');
});

test('the icon set from the name takes precedence over the set prop', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});

const wrapper = mount(Icon, {
props: {
name: 'icon-test-alternate::foo',
set: 'icon-test-ignored',
},
});

await flushPromises();

expect(wrapper.find('title').text()).toBe('Alternate Foo');
expect(warn).toHaveBeenCalledWith('Icon name [icon-test-alternate::foo] includes set [icon-test-alternate], ignoring set prop [icon-test-ignored]');

warn.mockRestore();
});
Loading