Skip to content

Commit b185cdb

Browse files
feat(icons): add stacking support (#4658)
Co-authored-by: Jacob Müller <jacob.mueller.elz@gmail.com>
1 parent a64d5cc commit b185cdb

File tree

15 files changed

+1922
-75
lines changed

15 files changed

+1922
-75
lines changed

docs/content/index.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@ export const directives = importAll(directivesContext)
1010

1111
const iconsContext = require.context('~/../src/icons', false, /package.json/)
1212
const icons = importAll(iconsContext) || {}
13-
// Since there are over 300 icons, we only return the first BIcon component, plus one
14-
// extra example icon component which we modify the icon name to be `BIcon{IconName}`
13+
// Since there are over 300 icons, we only return `BIcon` and `BIconstack` component, plus
14+
// one extra example icon component which we modify the icon name to be `BIcon{IconName}`
1515
// We sort the array to ensure `BIcon` appears first
1616
icons[''].components = icons[''].components
17-
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
18-
.slice(0, 2)
19-
.map(c => ({ ...c }))
20-
icons[''].components[1].component = 'BIcon{IconName}'
17+
.filter(c => c.component === 'BIconBlank' || !/^BIcon[A-Z]/.test(c.component))
18+
.sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0))
19+
.map(c => {
20+
c = { ...c }
21+
if (c.component === 'BIconBlank') {
22+
c.component = 'BIcon{IconName}'
23+
// We add a special `srcComponent` to grab the prop `$options` data from
24+
c.srcComponent = 'BIconBlank'
25+
}
26+
return c
27+
})
2128
export { icons }
2229

2330
const referenceContext = require.context('~/markdown/reference', true, /meta.json/)

docs/pages/docs/icons/index.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@ export default {
6161
computed: {
6262
componentMeta() {
6363
// `docs/content/index.js` massages the list of icon components
64-
// to include only `BIcon` and an example component
65-
const components = this.meta.components
66-
// Add in a special property or grabbing the component props
67-
// as `BIcon{IconName}` doesn't exist
68-
components[1].srcComponent = 'BIconBlank'
69-
return components
64+
// to include only `BIcon`, `BIconstack` and an example component
65+
// The example icon has a special `srcComponent` property that lists
66+
// `BIconBlank` as the component to grab the `$options.props` from
67+
return this.meta.components
7068
},
7169
importMeta() {
7270
return { ...this.meta, slug: 'icons', components: this.componentMeta }

scripts/create-icons.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ const iconsTemplateFn = _template(`// --- BEGIN AUTO-GENERATED FILE ---
4545
// @IconsVersion: <%= version %>
4646
// @Generated: <%= created %>
4747
//
48-
// This file is generated on each build. Do not edit this file.
49-
//
48+
// This file is generated on each build. Do not edit this file!
49+
5050
/*!
5151
* BootstrapVue Icons, generated from Bootstrap Icons <%= version %>
5252
*
@@ -77,14 +77,16 @@ const pluginTemplateFn = _template(`// --- BEGIN AUTO-GENERATED FILE ---
7777
// @IconsVersion: <%= version %>
7878
// @Generated: <%= created %>
7979
//
80-
// This file is generated on each build. Do not edit this file.
81-
//
80+
// This file is generated on each build. Do not edit this file!
8281
8382
import { pluginFactoryNoConfig } from '../utils/plugins'
8483
8584
// Icon helper component
8685
import { BIcon } from './icon'
8786
87+
// Icon stacking component
88+
import { BIconstack } from './iconstack'
89+
8890
import {
8991
// BootstrapVue custom icons
9092
BIconBlank,
@@ -105,6 +107,8 @@ export const IconsPlugin = /*#__PURE__*/ pluginFactoryNoConfig({
105107
components: {
106108
// Icon helper component
107109
BIcon,
110+
// Icon stacking component
111+
BIconstack,
108112
// BootstrapVue custom icon components
109113
BIconBlank,
110114
// Bootstrap icon components
@@ -128,8 +132,7 @@ const typesTemplateFn = _template(`// --- BEGIN AUTO-GENERATED FILE ---
128132
// @IconsVersion: <%= version %>
129133
// @Generated: <%= created %>
130134
//
131-
// This file is generated on each build. Do not edit this file.
132-
//
135+
// This file is generated on each build. Do not edit this file!
133136
134137
import Vue from 'vue'
135138
import { BvComponent } from '../'

src/icons/README.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ icons.
2929

3030
<div>
3131
<!-- Component rendered by docs/pages/docs/icons.index.js -->
32-
<!-- We use a `<div is="...">` to prevent marked loader from mangling the unknown tag-->
32+
<!-- We use a `<div is="...">` to prevent marked loader from mangling the unknown tag -->
3333
<div is="IconsTable"></div>
3434
</div>
3535

@@ -428,6 +428,71 @@ Shifting is applied after any rotation transforms. As with scaling, backgrounds
428428
affected. If you need to shift the border/background with the icon, use Bootstrap's margin
429429
[spacing utility classes](/docs/reference/utility-classes).
430430

431+
## Stacking icons
432+
433+
<span class="badge badge-info small">v2.3.0+</span>
434+
435+
Combine icons together via the use of the component `<b-iconstack>` and the `stacked` prop on
436+
individual icons (`<b-icon>` or `<b-icon-{icon-name}>`) to create complex icons:
437+
438+
```html
439+
<template>
440+
<div>
441+
<b-iconstack font-scale="5">
442+
<b-icon stacked icon="camera" variant="info" scale="0.75" shift-v="-0.25"></b-icon>
443+
<b-icon stacked icon="circle-slash" variant="danger"></b-icon>
444+
</b-iconstack>
445+
446+
<b-iconstack font-scale="5" rotate="90">
447+
<b-icon stacked icon="chevron-right" shift-h="-3" variant="danger"></b-icon>
448+
<b-icon stacked icon="chevron-right" shift-h="0" variant="success"></b-icon>
449+
<b-icon stacked icon="chevron-right" shift-h="3" variant="primary"></b-icon>
450+
</b-iconstack>
451+
452+
<b-iconstack font-scale="5">
453+
<b-icon stacked icon="circle-fill" variant="info"></b-icon>
454+
<b-icon stacked icon="bell-fill" scale="0.5" variant="white"></b-icon>
455+
<b-icon stacked icon="circle" variant="danger"></b-icon>
456+
</b-iconstack>
457+
458+
<b-iconstack font-scale="5" variant="white">
459+
<b-icon stacked icon="square-fill" variant="dark"></b-icon>
460+
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="2.5" shift-h="-2.5"></b-icon>
461+
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="2.5" shift-h="2.5" rotate="90"></b-icon>
462+
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="-2.5" shift-h="2.5" rotate="180"></b-icon>
463+
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="-2.5" shift-h="-2.5" rotate="270"></b-icon>
464+
</b-iconstack>
465+
466+
<b-iconstack font-scale="5">
467+
<b-icon stacked icon="square"></b-icon>
468+
<b-icon stacked icon="check"></b-icon>
469+
</b-iconstack>
470+
471+
<b-iconstack font-scale="5">
472+
<b-icon stacked icon="square"></b-icon>
473+
<b-icon stacked icon="dot" shift-h="-2.25" shift-v="3"></b-icon>
474+
<b-icon stacked icon="dot" shift-h="-2.25"></b-icon>
475+
<b-icon stacked icon="dot" shift-h="-2.25" shift-v="-3"></b-icon>
476+
<b-icon stacked icon="dot" shift-h="2.25" shift-v="3"></b-icon>
477+
<b-icon stacked icon="dot" shift-h="2.25"></b-icon>
478+
<b-icon stacked icon="dot" shift-h="2.25" shift-v="-3"></b-icon>
479+
</b-iconstack>
480+
</div>
481+
</template>
482+
483+
<!-- icons-stacking.vue -->
484+
```
485+
486+
`<b-iconstack>` supports the same `variant`, `font-size`, and transformation props available on
487+
individual icons.
488+
489+
Stacked icon notes:
490+
491+
- Remember to set the `stacked` prop on the inner icon components
492+
- The `font-scale` prop cannot be used on the inner icon components
493+
- The `width` and `height` attributes cannot be applied to the inner icon components
494+
- Stacked icons **cannot** be stacked inside another `<b-iconstack>`
495+
431496
## Using in components
432497

433498
Easily place icons as content in other components.

src/icons/helpers/make-icon.js

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import Vue from '../../utils/vue'
22
import { mergeData } from 'vue-functional-data-merge'
33
import identity from '../../utils/identity'
4-
import { kebabCase, pascalCase, trim } from '../../utils/string'
4+
import { isUndefinedOrNull } from '../../utils/inspect'
55
import { toFloat } from '../../utils/number'
6+
import { kebabCase, pascalCase, trim } from '../../utils/string'
67

78
// Common icon props (should be cloned/spread before using)
89
export const commonIconProps = {
@@ -52,28 +53,32 @@ const baseAttrs = {
5253

5354
// Shared private base component to reduce bundle/runtime size
5455
// @vue/component
55-
const BVIconBase = {
56+
export const BVIconBase = /*#__PURE__*/ Vue.extend({
5657
name: 'BVIconBase',
5758
functional: true,
5859
props: {
5960
content: {
6061
type: String
6162
},
63+
stacked: {
64+
type: Boolean,
65+
default: false
66+
},
6267
...commonIconProps
6368
},
64-
render(h, { data, props }) {
69+
render(h, { data, props, children }) {
6570
const fontScale = Math.max(toFloat(props.fontScale) || 1, 0) || 1
6671
const scale = Math.max(toFloat(props.scale) || 1, 0) || 1
6772
const rotate = toFloat(props.rotate) || 0
6873
const shiftH = toFloat(props.shiftH) || 0
6974
const shiftV = toFloat(props.shiftV) || 0
7075
const flipH = props.flipH
7176
const flipV = props.flipV
72-
// Compute the transforms. Note that order is important as
73-
// SVG transforms are applied in order from left to right
74-
// and we want flipping/scale to occur before rotation.
75-
// Note shifting is applied separately. Assumes that the
76-
// viewbox is `0 0 20 20` (`10 10` is the center)
77+
// Compute the transforms
78+
// Note that order is important as SVG transforms are applied in order from
79+
// left to right and we want flipping/scale to occur before rotation
80+
// Note shifting is applied separately
81+
// Assumes that the viewbox is `0 0 20 20` (`10 10` is the center)
7782
const hasScale = flipH || flipV || scale !== 1
7883
const hasTransforms = hasScale || rotate
7984
const hasShift = shiftH || shiftV
@@ -84,11 +89,19 @@ const BVIconBase = {
8489
hasTransforms ? 'translate(-10 -10)' : null
8590
].filter(identity)
8691

92+
// Handling stacked icons
93+
const isStacked = props.stacked
94+
const hasContent = !isUndefinedOrNull(props.content)
95+
8796
// We wrap the content in a `<g>` for handling the transforms (except shift)
88-
let $inner = h('g', {
89-
attrs: { transform: transforms.join(' ') || null },
90-
domProps: { innerHTML: props.content || '' }
91-
})
97+
let $inner = h(
98+
'g',
99+
{
100+
attrs: { transform: transforms.join(' ') || null },
101+
domProps: hasContent ? { innerHTML: props.content || '' } : {}
102+
},
103+
children
104+
)
92105

93106
// If needed, we wrap in an additional `<g>` in order to handle the shifting
94107
if (hasShift) {
@@ -103,28 +116,33 @@ const BVIconBase = {
103116
'svg',
104117
mergeData(
105118
{
119+
staticClass: 'b-icon bi',
106120
class: { [`text-${props.variant}`]: !!props.variant },
107121
attrs: baseAttrs,
108-
style: { fontSize: fontScale === 1 ? null : `${fontScale * 100}%` }
122+
style: isStacked ? {} : { fontSize: fontScale === 1 ? null : `${fontScale * 100}%` }
109123
},
110124
// Merge in user supplied data
111125
data,
126+
// If icon is stacked, null out some attrs
127+
isStacked ? { attrs: { width: null, height: null, role: null, alt: null } } : {},
112128
// These cannot be overridden by users
113129
{
114-
staticClass: 'b-icon bi',
115-
attrs: { xmlns: 'http://www.w3.org/2000/svg', fill: 'currentColor' }
130+
attrs: {
131+
xmlns: isStacked ? null : 'http://www.w3.org/2000/svg',
132+
fill: 'currentColor'
133+
}
116134
}
117135
),
118136
[$inner]
119137
)
120138
}
121-
}
139+
})
122140

123141
/**
124142
* Icon component generator function
125143
*
126144
* @param {string} icon name (minus the leading `BIcon`)
127-
* @param {string} raw innerHTML for SVG
145+
* @param {string} raw `innerHTML` for SVG
128146
* @return {VueComponent}
129147
*/
130148
export const makeIcon = (name, content) => {
@@ -137,7 +155,13 @@ export const makeIcon = (name, content) => {
137155
return Vue.extend({
138156
name: iconName,
139157
functional: true,
140-
props: { ...commonIconProps },
158+
props: {
159+
...commonIconProps,
160+
stacked: {
161+
type: Boolean,
162+
default: false
163+
}
164+
},
141165
render(h, { data, props }) {
142166
return h(
143167
BVIconBase,

src/icons/icon.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ export const BIcon = /*#__PURE__*/ Vue.extend({
1616
type: String,
1717
default: null
1818
},
19-
...commonIconProps
19+
...commonIconProps,
20+
stacked: {
21+
type: Boolean,
22+
default: false
23+
}
2024
},
2125
render(h, { data, props, parent }) {
2226
const icon = pascalCase(trim(props.icon || '')).replace(RX_ICON_PREFIX, '')

src/icons/icons.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// --- BEGIN AUTO-GENERATED FILE ---
22
//
33
// @IconsVersion: 1.0.0-alpha2
4-
// @Generated: 2020-01-01T12:00:00.000Z
5-
//
6-
// This file is generated on each build. Do not edit this file.
4+
// @Generated: 2020-01-22T07:06:51.693Z
75
//
6+
// This file is generated on each build. Do not edit this file!
87

98
import Vue from 'vue'
109
import { BvComponent } from '../'

src/icons/icons.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// --- BEGIN AUTO-GENERATED FILE ---
22
//
33
// @IconsVersion: 1.0.0-alpha2
4-
// @Generated: 2020-01-01T12:00:00.000Z
5-
//
6-
// This file is generated on each build. Do not edit this file.
4+
// @Generated: 2020-01-22T07:06:51.693Z
75
//
6+
// This file is generated on each build. Do not edit this file!
7+
88
/*!
99
* BootstrapVue Icons, generated from Bootstrap Icons 1.0.0-alpha2
1010
*

src/icons/iconstack.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Vue from '../utils/vue'
2+
import { mergeData } from 'vue-functional-data-merge'
3+
import { commonIconProps, BVIconBase } from './helpers/make-icon'
4+
5+
// @vue/component
6+
export const BIconstack = /*#__PURE__*/ Vue.extend({
7+
name: 'BIconstack',
8+
functional: true,
9+
props: { ...commonIconProps },
10+
render(h, { data, props, children }) {
11+
return h(
12+
BVIconBase,
13+
mergeData(data, { staticClass: 'b-iconstack', props: { ...props, stacked: false } }),
14+
children
15+
)
16+
}
17+
})

0 commit comments

Comments
 (0)