Skip to content

Commit

Permalink
feat(icons): add stacking support (#4658)
Browse files Browse the repository at this point in the history
Co-authored-by: Jacob Müller <jacob.mueller.elz@gmail.com>
  • Loading branch information
tmorehouse and jacobmllr95 authored Jan 22, 2020
1 parent a64d5cc commit b185cdb
Show file tree
Hide file tree
Showing 15 changed files with 1,922 additions and 75 deletions.
19 changes: 13 additions & 6 deletions docs/content/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ export const directives = importAll(directivesContext)

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

const referenceContext = require.context('~/markdown/reference', true, /meta.json/)
Expand Down
10 changes: 4 additions & 6 deletions docs/pages/docs/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,10 @@ export default {
computed: {
componentMeta() {
// `docs/content/index.js` massages the list of icon components
// to include only `BIcon` and an example component
const components = this.meta.components
// Add in a special property or grabbing the component props
// as `BIcon{IconName}` doesn't exist
components[1].srcComponent = 'BIconBlank'
return components
// to include only `BIcon`, `BIconstack` and an example component
// The example icon has a special `srcComponent` property that lists
// `BIconBlank` as the component to grab the `$options.props` from
return this.meta.components
},
importMeta() {
return { ...this.meta, slug: 'icons', components: this.componentMeta }
Expand Down
15 changes: 9 additions & 6 deletions scripts/create-icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ const iconsTemplateFn = _template(`// --- BEGIN AUTO-GENERATED FILE ---
// @IconsVersion: <%= version %>
// @Generated: <%= created %>
//
// This file is generated on each build. Do not edit this file.
//
// This file is generated on each build. Do not edit this file!
/*!
* BootstrapVue Icons, generated from Bootstrap Icons <%= version %>
*
Expand Down Expand Up @@ -77,14 +77,16 @@ const pluginTemplateFn = _template(`// --- BEGIN AUTO-GENERATED FILE ---
// @IconsVersion: <%= version %>
// @Generated: <%= created %>
//
// This file is generated on each build. Do not edit this file.
//
// This file is generated on each build. Do not edit this file!
import { pluginFactoryNoConfig } from '../utils/plugins'
// Icon helper component
import { BIcon } from './icon'
// Icon stacking component
import { BIconstack } from './iconstack'
import {
// BootstrapVue custom icons
BIconBlank,
Expand All @@ -105,6 +107,8 @@ export const IconsPlugin = /*#__PURE__*/ pluginFactoryNoConfig({
components: {
// Icon helper component
BIcon,
// Icon stacking component
BIconstack,
// BootstrapVue custom icon components
BIconBlank,
// Bootstrap icon components
Expand All @@ -128,8 +132,7 @@ const typesTemplateFn = _template(`// --- BEGIN AUTO-GENERATED FILE ---
// @IconsVersion: <%= version %>
// @Generated: <%= created %>
//
// This file is generated on each build. Do not edit this file.
//
// This file is generated on each build. Do not edit this file!
import Vue from 'vue'
import { BvComponent } from '../'
Expand Down
67 changes: 66 additions & 1 deletion src/icons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ icons.

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

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

## Stacking icons

<span class="badge badge-info small">v2.3.0+</span>

Combine icons together via the use of the component `<b-iconstack>` and the `stacked` prop on
individual icons (`<b-icon>` or `<b-icon-{icon-name}>`) to create complex icons:

```html
<template>
<div>
<b-iconstack font-scale="5">
<b-icon stacked icon="camera" variant="info" scale="0.75" shift-v="-0.25"></b-icon>
<b-icon stacked icon="circle-slash" variant="danger"></b-icon>
</b-iconstack>

<b-iconstack font-scale="5" rotate="90">
<b-icon stacked icon="chevron-right" shift-h="-3" variant="danger"></b-icon>
<b-icon stacked icon="chevron-right" shift-h="0" variant="success"></b-icon>
<b-icon stacked icon="chevron-right" shift-h="3" variant="primary"></b-icon>
</b-iconstack>

<b-iconstack font-scale="5">
<b-icon stacked icon="circle-fill" variant="info"></b-icon>
<b-icon stacked icon="bell-fill" scale="0.5" variant="white"></b-icon>
<b-icon stacked icon="circle" variant="danger"></b-icon>
</b-iconstack>

<b-iconstack font-scale="5" variant="white">
<b-icon stacked icon="square-fill" variant="dark"></b-icon>
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="2.5" shift-h="-2.5"></b-icon>
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="2.5" shift-h="2.5" rotate="90"></b-icon>
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="-2.5" shift-h="2.5" rotate="180"></b-icon>
<b-icon stacked icon="arrow-up-short" scale="0.5" shift-v="-2.5" shift-h="-2.5" rotate="270"></b-icon>
</b-iconstack>

<b-iconstack font-scale="5">
<b-icon stacked icon="square"></b-icon>
<b-icon stacked icon="check"></b-icon>
</b-iconstack>

<b-iconstack font-scale="5">
<b-icon stacked icon="square"></b-icon>
<b-icon stacked icon="dot" shift-h="-2.25" shift-v="3"></b-icon>
<b-icon stacked icon="dot" shift-h="-2.25"></b-icon>
<b-icon stacked icon="dot" shift-h="-2.25" shift-v="-3"></b-icon>
<b-icon stacked icon="dot" shift-h="2.25" shift-v="3"></b-icon>
<b-icon stacked icon="dot" shift-h="2.25"></b-icon>
<b-icon stacked icon="dot" shift-h="2.25" shift-v="-3"></b-icon>
</b-iconstack>
</div>
</template>

<!-- icons-stacking.vue -->
```

`<b-iconstack>` supports the same `variant`, `font-size`, and transformation props available on
individual icons.

Stacked icon notes:

- Remember to set the `stacked` prop on the inner icon components
- The `font-scale` prop cannot be used on the inner icon components
- The `width` and `height` attributes cannot be applied to the inner icon components
- Stacked icons **cannot** be stacked inside another `<b-iconstack>`

## Using in components

Easily place icons as content in other components.
Expand Down
60 changes: 42 additions & 18 deletions src/icons/helpers/make-icon.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Vue from '../../utils/vue'
import { mergeData } from 'vue-functional-data-merge'
import identity from '../../utils/identity'
import { kebabCase, pascalCase, trim } from '../../utils/string'
import { isUndefinedOrNull } from '../../utils/inspect'
import { toFloat } from '../../utils/number'
import { kebabCase, pascalCase, trim } from '../../utils/string'

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

// Shared private base component to reduce bundle/runtime size
// @vue/component
const BVIconBase = {
export const BVIconBase = /*#__PURE__*/ Vue.extend({
name: 'BVIconBase',
functional: true,
props: {
content: {
type: String
},
stacked: {
type: Boolean,
default: false
},
...commonIconProps
},
render(h, { data, props }) {
render(h, { data, props, children }) {
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 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)
// 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
Expand All @@ -84,11 +89,19 @@ const BVIconBase = {
hasTransforms ? 'translate(-10 -10)' : null
].filter(identity)

// Handling stacked icons
const isStacked = props.stacked
const hasContent = !isUndefinedOrNull(props.content)

// We wrap the content in a `<g>` for handling the transforms (except shift)
let $inner = h('g', {
attrs: { transform: transforms.join(' ') || null },
domProps: { innerHTML: props.content || '' }
})
let $inner = h(
'g',
{
attrs: { transform: transforms.join(' ') || null },
domProps: hasContent ? { innerHTML: props.content || '' } : {}
},
children
)

// If needed, we wrap in an additional `<g>` in order to handle the shifting
if (hasShift) {
Expand All @@ -103,28 +116,33 @@ const BVIconBase = {
'svg',
mergeData(
{
staticClass: 'b-icon bi',
class: { [`text-${props.variant}`]: !!props.variant },
attrs: baseAttrs,
style: { fontSize: fontScale === 1 ? null : `${fontScale * 100}%` }
style: isStacked ? {} : { fontSize: fontScale === 1 ? null : `${fontScale * 100}%` }
},
// Merge in user supplied data
data,
// If icon is stacked, null out some attrs
isStacked ? { attrs: { width: null, height: null, role: null, alt: null } } : {},
// These cannot be overridden by users
{
staticClass: 'b-icon bi',
attrs: { xmlns: 'http://www.w3.org/2000/svg', fill: 'currentColor' }
attrs: {
xmlns: isStacked ? null : 'http://www.w3.org/2000/svg',
fill: 'currentColor'
}
}
),
[$inner]
)
}
}
})

/**
* Icon component generator function
*
* @param {string} icon name (minus the leading `BIcon`)
* @param {string} raw innerHTML for SVG
* @param {string} raw `innerHTML` for SVG
* @return {VueComponent}
*/
export const makeIcon = (name, content) => {
Expand All @@ -137,7 +155,13 @@ export const makeIcon = (name, content) => {
return Vue.extend({
name: iconName,
functional: true,
props: { ...commonIconProps },
props: {
...commonIconProps,
stacked: {
type: Boolean,
default: false
}
},
render(h, { data, props }) {
return h(
BVIconBase,
Expand Down
6 changes: 5 additions & 1 deletion src/icons/icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export const BIcon = /*#__PURE__*/ Vue.extend({
type: String,
default: null
},
...commonIconProps
...commonIconProps,
stacked: {
type: Boolean,
default: false
}
},
render(h, { data, props, parent }) {
const icon = pascalCase(trim(props.icon || '')).replace(RX_ICON_PREFIX, '')
Expand Down
5 changes: 2 additions & 3 deletions src/icons/icons.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// --- BEGIN AUTO-GENERATED FILE ---
//
// @IconsVersion: 1.0.0-alpha2
// @Generated: 2020-01-01T12:00:00.000Z
//
// This file is generated on each build. Do not edit this file.
// @Generated: 2020-01-22T07:06:51.693Z
//
// This file is generated on each build. Do not edit this file!

import Vue from 'vue'
import { BvComponent } from '../'
Expand Down
6 changes: 3 additions & 3 deletions src/icons/icons.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// --- BEGIN AUTO-GENERATED FILE ---
//
// @IconsVersion: 1.0.0-alpha2
// @Generated: 2020-01-01T12:00:00.000Z
//
// This file is generated on each build. Do not edit this file.
// @Generated: 2020-01-22T07:06:51.693Z
//
// This file is generated on each build. Do not edit this file!

/*!
* BootstrapVue Icons, generated from Bootstrap Icons 1.0.0-alpha2
*
Expand Down
17 changes: 17 additions & 0 deletions src/icons/iconstack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Vue from '../utils/vue'
import { mergeData } from 'vue-functional-data-merge'
import { commonIconProps, BVIconBase } from './helpers/make-icon'

// @vue/component
export const BIconstack = /*#__PURE__*/ Vue.extend({
name: 'BIconstack',
functional: true,
props: { ...commonIconProps },
render(h, { data, props, children }) {
return h(
BVIconBase,
mergeData(data, { staticClass: 'b-iconstack', props: { ...props, stacked: false } }),
children
)
}
})
Loading

0 comments on commit b185cdb

Please sign in to comment.