Skip to content

Commit 914a28c

Browse files
committed
add support for custom gradient fills
1 parent 888fda0 commit 914a28c

3 files changed

Lines changed: 138 additions & 3 deletions

File tree

‎src/components/FontAwesomeIcon.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ export default defineComponent({
153153
widthAuto: {
154154
type: Boolean,
155155
default: false
156+
},
157+
gradientFill: {
158+
type: Object,
159+
default: null
156160
}
157161
},
158162

@@ -188,7 +192,7 @@ export default defineComponent({
188192
{ immediate: true }
189193
)
190194

191-
const vnode = computed(() => (renderedIcon.value ? convert(renderedIcon.value.abstract[0], {}, attrs) : null))
195+
const vnode = computed(() => (renderedIcon.value ? convert(renderedIcon.value.abstract[0], {}, attrs, props.gradientFill) : null))
192196
return () => vnode.value
193197
}
194198
})

‎src/components/__tests__/FontAwesomeIcon.test.js‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,3 +580,87 @@ describe('using a family', () => {
580580
})
581581
}
582582
})
583+
584+
describe('gradientFill prop', () => {
585+
test('applies a linearGradient element and fill reference to svg', () => {
586+
const wrapper = mountFromProps({
587+
icon: faCoffee,
588+
gradientFill: {
589+
id: 'myLinearGradient',
590+
type: 'linear',
591+
x1: '0%',
592+
y1: '0%',
593+
x2: '100%',
594+
y2: '0%',
595+
stops: [
596+
{ offset: '0%', color: '#FF5F6D' },
597+
{ offset: '100%', color: '#FFC371' }
598+
]
599+
}
600+
})
601+
602+
expect(wrapper.element.getAttribute('fill')).toBe('url(#myLinearGradient)')
603+
const gradient = wrapper.element.querySelector('linearGradient')
604+
expect(gradient).toBeTruthy()
605+
expect(gradient.getAttribute('id')).toBe('myLinearGradient')
606+
expect(gradient.getAttribute('x1')).toBe('0%')
607+
expect(gradient.getAttribute('x2')).toBe('100%')
608+
const stops = gradient.querySelectorAll('stop')
609+
expect(stops.length).toBe(2)
610+
expect(stops[0].getAttribute('offset')).toBe('0%')
611+
expect(stops[0].getAttribute('stop-color')).toBe('#FF5F6D')
612+
expect(stops[1].getAttribute('offset')).toBe('100%')
613+
expect(stops[1].getAttribute('stop-color')).toBe('#FFC371')
614+
})
615+
616+
test('applies a radialGradient element and fill reference to svg', () => {
617+
const wrapper = mountFromProps({
618+
icon: faCoffee,
619+
gradientFill: {
620+
id: 'myRadialGradient',
621+
type: 'radial',
622+
r: '150%',
623+
cx: '30%',
624+
cy: '107%',
625+
stops: [
626+
{ offset: '0', color: '#FDF497' },
627+
{ offset: '0.45', color: '#FD5949', opacity: 0.8 },
628+
{ offset: '0.9', color: '#285AEB' }
629+
]
630+
}
631+
})
632+
633+
expect(wrapper.element.getAttribute('fill')).toBe('url(#myRadialGradient)')
634+
const gradient = wrapper.element.querySelector('radialGradient')
635+
expect(gradient).toBeTruthy()
636+
expect(gradient.getAttribute('id')).toBe('myRadialGradient')
637+
const stops = gradient.querySelectorAll('stop')
638+
expect(stops.length).toBe(3)
639+
expect(stops[1].getAttribute('stop-opacity')).toBe('0.8')
640+
})
641+
642+
test('strips fill from child path elements when gradientFill is provided', () => {
643+
const wrapper = mountFromProps({
644+
icon: faCoffee,
645+
gradientFill: {
646+
id: 'testGradient',
647+
type: 'linear',
648+
stops: [{ offset: '0%', color: 'red' }]
649+
}
650+
})
651+
652+
const paths = wrapper.element.querySelectorAll('path')
653+
paths.forEach(path => {
654+
expect(path.getAttribute('fill')).toBeNull()
655+
})
656+
})
657+
658+
test('does not render gradient elements when gradientFill is not provided', () => {
659+
const wrapper = mountFromProps({ icon: faCoffee })
660+
661+
expect(wrapper.element.querySelector('linearGradient')).toBeNull()
662+
expect(wrapper.element.querySelector('radialGradient')).toBeNull()
663+
const fill = wrapper.element.getAttribute('fill')
664+
expect(fill === null || !fill.includes('url(')).toBe(true)
665+
})
666+
})

‎src/converter.js‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,49 @@ function classToObject (classes) {
3939
)
4040
}
4141

42+
/**
43+
* Creates a Vue VNode for an SVG gradient stop element.
44+
* @param {Object} stop The gradient stop definition.
45+
* @param {string|number} stop.offset The offset of the stop.
46+
* @param {string} stop.color The color of the stop.
47+
* @param {number} [stop.opacity] The opacity of the stop.
48+
* @param {number} index The index of the stop.
49+
* @returns {VNode}
50+
*/
51+
function createGradientStop (stop, index) {
52+
return h('stop', {
53+
key: `${index}-${stop.offset}`,
54+
offset: stop.offset,
55+
'stop-color': stop.color,
56+
...(stop.opacity !== undefined && { 'stop-opacity': stop.opacity })
57+
})
58+
}
59+
4260
/**
4361
* Converts a FontAwesome abstract element of an icon into a Vue VNode.
4462
* @param {AbstractElement | String} abstractElement The element to convert.
4563
* @param {Object} props The user-defined props.
4664
* @param {Object} attrs The user-defined native HTML attributes.
65+
* @param {Object} [gradientFill] Optional gradient fill config.
4766
* @returns {VNode}
4867
*/
49-
export default function convert (abstractElement, props = {}, attrs = {}) {
68+
export default function convert (abstractElement, props = {}, attrs = {}, gradientFill = null) {
5069
// If the abstract element is a string, we'll just return a string render function
5170
if (typeof abstractElement === 'string') {
5271
return abstractElement
5372
}
5473

5574
// Converting abstract element children into Vue VNodes
75+
// If a gradientFill (or fill attr) is provided, strip fill from child path elements
76+
// so the gradient/fill takes precedence over the icon's built-in fill
77+
const hasFillOverride = gradientFill || ('fill' in attrs)
5678
const children = (abstractElement.children || [])
57-
.map(child => convert(child))
79+
.map(child => {
80+
if (hasFillOverride && child.tag === 'path' && child.attributes && 'fill' in child.attributes) {
81+
return convert({ ...child, attributes: { ...child.attributes, fill: undefined } })
82+
}
83+
return convert(child)
84+
})
5885

5986
// Converting abstract element attributes into valid Vue format
6087
const mixins = Object.keys(abstractElement.attributes || {})
@@ -85,6 +112,26 @@ export default function convert (abstractElement, props = {}, attrs = {}) {
85112
// Now, we'll return the VNode
86113
const { class: _aClass = {}, style: aStyle = {}, ...otherAttrs } = attrs
87114

115+
// If a gradientFill is provided, inject the gradient element and set fill to reference it
116+
if (gradientFill) {
117+
const { type: gradientType, stops = [], id, ...gradientProps } = gradientFill
118+
const gradientTag = gradientType === 'linear' ? 'linearGradient' : 'radialGradient'
119+
const gradientVNode = h(gradientTag, { ...gradientProps, id }, stops.map(createGradientStop))
120+
121+
return h(
122+
abstractElement.tag,
123+
{
124+
...props,
125+
class: mixins.class,
126+
style: { ...mixins.style, ...aStyle },
127+
...mixins.attrs,
128+
...otherAttrs,
129+
fill: `url(#${id})`
130+
},
131+
[gradientVNode, ...children]
132+
)
133+
}
134+
88135
return h(
89136
abstractElement.tag,
90137
{

0 commit comments

Comments
 (0)