From 899779f20015f719198a763136137eea01aa11ea Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 12 Jan 2020 07:24:54 -0400 Subject: [PATCH] fix(icons): make icon transform props work with IE11 (closes #4607) (#4608) --- src/icons/README.md | 6 +- src/icons/helpers/make-icon.js | 44 +++-- src/icons/icons.spec.js | 290 ++++++++++++++++++++++++++++++++- 3 files changed, 318 insertions(+), 22 deletions(-) diff --git a/src/icons/README.md b/src/icons/README.md index c6b1e0642af..e3759b2a23a 100644 --- a/src/icons/README.md +++ b/src/icons/README.md @@ -120,7 +120,7 @@ Vue.use(BootstrapVueIcons) ``` BootstrapVue icons SCSS/CSS does not depend on any Bootstrap SASS variables, mixins, functions or -CSS classes (other than the Bootstrap `text-{variant}` variant utility classes, if using the +CSS classes (other than the Bootstrap `text-{variant}` text color utility classes, if using the `variant` prop). Please note that the icons CSS is _also_ included in the main BootstrapVue SCSS/CSS files. @@ -299,9 +299,9 @@ With the use of Bootstrap's border and background ## Transforms -BootstrapVue icons provide several props for applying basic CSS transforms to the ``. All +BootstrapVue icons provide several props for applying basic SVG transforms to the ``. All transforms can be combined for added effect. Note that the transforms are applied to the `` -content, and not the `` bounding box. +_content_ and not the `` bounding box. ### Flipping diff --git a/src/icons/helpers/make-icon.js b/src/icons/helpers/make-icon.js index 7022c82a300..1fca99053c6 100644 --- a/src/icons/helpers/make-icon.js +++ b/src/icons/helpers/make-icon.js @@ -62,35 +62,43 @@ const BVIconBase = { ...commonIconProps }, render(h, { data, props }) { - const fontScale = toFloat(props.fontScale) || 1 - const scale = toFloat(props.scale) || 1 + const fontScale = Math.max(toFloat(props.fontScale) || 1, 0) || 1 + const scale = Math.max(toFloat(props.scale) || 1, 0) || 1 const rotate = toFloat(props.rotate) || 0 const shiftH = toFloat(props.shiftH) || 0 const shiftV = toFloat(props.shiftV) || 0 const flipH = props.flipH const flipV = props.flipV - // Compute the transforms. Note that order is important - // CSS transforms are applied in order from right to left - // and we want flipping to occur before rotation, and - // shifting is applied last + // Compute the transforms. Note that order is important as + // SVG transforms are applied in order from left to right + // and we want flipping/scale to occur before rotation. + // Note shifting is applied separately. Assumes that the + // viewbox is `0 0 20 20` (`10 10` is the center) + const hasScale = flipH || flipV || scale !== 1 + const hasTransforms = hasScale || rotate + const hasShift = shiftH || shiftV const transforms = [ - shiftH ? `translateX(${(100 * shiftH) / 16}%)` : null, - shiftV ? `translateY(${(-100 * shiftV) / 16}%)` : null, - rotate ? `rotate(${rotate}deg)` : null, - flipH || flipV || scale !== 1 - ? `scale(${(flipH ? -1 : 1) * scale}, ${(flipV ? -1 : 1) * scale})` - : null + hasTransforms ? 'translate(10 10)' : null, + hasScale ? `scale(${(flipH ? -1 : 1) * scale} ${(flipV ? -1 : 1) * scale})` : null, + rotate ? `rotate(${rotate})` : null, + hasTransforms ? 'translate(-10 -10)' : null ].filter(identity) - // We wrap the content in a `` for handling the transforms - const $inner = h('g', { - style: { - transform: transforms.join(' ') || null, - transformOrigin: transforms.length > 0 ? 'center' : null - }, + // We wrap the content in a `` for handling the transforms (except shift) + let $inner = h('g', { + attrs: { transform: transforms.join(' ') || null }, domProps: { innerHTML: props.content || '' } }) + // If needed, we wrap in an additional `` in order to handle the shifting + if (hasShift) { + $inner = h( + 'g', + { attrs: { transform: `translate(${(20 * shiftH) / 16} ${(-20 * shiftV) / 16})` } }, + [$inner] + ) + } + return h( 'svg', mergeData( diff --git a/src/icons/icons.spec.js b/src/icons/icons.spec.js index d62b2468f93..3861272b540 100644 --- a/src/icons/icons.spec.js +++ b/src/icons/icons.spec.js @@ -45,7 +45,11 @@ describe('icons', () => { expect(wrapper.attributes('height')).toBe('1em') expect(wrapper.attributes('viewBox')).toBe('0 0 20 20') expect(wrapper.attributes('fill')).toBe('currentColor') - expect(wrapper.find('path').exists()).toBe(true) + expect(wrapper.attributes('style')).not.toBeDefined() + expect(wrapper.element.style.fontSize).toEqual('') + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).not.toBeDefined() + expect(wrapper.find('svg > g > path').exists()).toBe(true) }) it('b-icon with empty icon name renders BIconBlank', async () => { @@ -81,6 +85,8 @@ describe('icons', () => { expect(wrapper.exists()).toBe(true) expect(wrapper.text()).toBe('') expect(wrapper.is('svg')).toBe(true) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).not.toBeDefined() expect(wrapper.find('svg > g').isEmpty()).toBe(true) }) @@ -98,6 +104,8 @@ describe('icons', () => { expect(wrapper.classes()).toContain('bi') expect(wrapper.classes()).toContain('bi-blank') expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).not.toBeDefined() expect(wrapper.find('svg > g').isEmpty()).toBe(true) }) @@ -121,6 +129,34 @@ describe('icons', () => { expect(wrapper.attributes('role')).toBe('img') expect(wrapper.attributes('alt')).toBe('icon') expect(wrapper.attributes('focusable')).toBe('false') + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).not.toBeDefined() + expect(wrapper.find('path').exists()).toBe(true) + }) + + it('b-icon font-scale prop works', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + fontScale: '1.25' + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.attributes('role')).toBe('img') + expect(wrapper.attributes('alt')).toBe('icon') + expect(wrapper.attributes('focusable')).toBe('false') + expect(wrapper.attributes('style')).toBeDefined() + expect(wrapper.element.style.fontSize).toEqual('125%') + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).not.toBeDefined() expect(wrapper.find('path').exists()).toBe(true) }) @@ -140,6 +176,258 @@ describe('icons', () => { expect(wrapper.classes()).toContain('bi-fake-icon-test') expect(wrapper.classes().length).toBe(3) expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).not.toBeDefined() expect(wrapper.find('svg > g > path.fake-path').exists()).toBe(true) }) + + it('b-icon rotate prop works', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + rotate: '45' + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) rotate(45) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon scale prop works', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + scale: '1.5' + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(1.5 1.5) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon flip-h prop works', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + flipH: true + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(-1 1) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon flip-v prop works', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + flipV: true + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(1 -1) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon flip-h prop works with flip-v prop', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + flipH: true, + flipV: true + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(-1 -1) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon scale prop works with flip-h prop', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + scale: '1.5', + flipH: true + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(-1.5 1.5) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon scale prop works with flip-v prop', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + scale: '1.5', + flipV: true + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(1.5 -1.5) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon scale prop works with flip-h and flip-v prop', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + scale: '1.5', + flipH: true, + flipV: true + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual( + 'translate(10 10) scale(-1.5 -1.5) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > path').exists()).toBe(true) + }) + + it('b-icon shift-h and shift-v props work', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + shiftH: 8, + shiftV: 16 + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual('translate(10 -20)') + expect(wrapper.find('svg > g > g').exists()).toBe(true) + expect(wrapper.find('svg > g > g').attributes('transform')).not.toBeDefined() + expect(wrapper.find('svg > g > g > path').exists()).toBe(true) + }) + + it('b-icon shift-h and shift-v props work with rotate prop', async () => { + const wrapper = mount(BIcon, { + localVue: localVue, + parentComponent: parentComponent, + propsData: { + icon: 'alert-circle-fill', + rotate: 45, + shiftH: 8, + shiftV: 16 + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.is('svg')).toBe(true) + expect(wrapper.classes()).toContain('b-icon') + expect(wrapper.classes()).toContain('bi') + expect(wrapper.classes()).toContain('bi-alert-circle-fill') + expect(wrapper.classes().length).toBe(3) + expect(wrapper.find('svg > g').exists()).toBe(true) + expect(wrapper.find('svg > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g').attributes('transform')).toEqual('translate(10 -20)') + expect(wrapper.find('svg > g > g').exists()).toBe(true) + expect(wrapper.find('svg > g > g').attributes('transform')).toBeDefined() + expect(wrapper.find('svg > g > g').attributes('transform')).toEqual( + 'translate(10 10) rotate(45) translate(-10 -10)' + ) + expect(wrapper.find('svg > g > g > path').exists()).toBe(true) + }) })