diff --git a/src/components/avatar/README.md b/src/components/avatar/README.md
index d7e915f75f3..b7f1acbf958 100644
--- a/src/components/avatar/README.md
+++ b/src/components/avatar/README.md
@@ -52,11 +52,27 @@ components.
## Avatar types
-The avatar content can be either a an image, an icon, or short text string. Avatar content defaults
+The avatar content can be either a short text string, an image, or an icon. Avatar content defaults
to the [`'person-fill'` icon](/docs/icons) when no other content is specified.
-You can also supply custom content via the default slot, although you may need to apply additional
-styling on the content.
+### Text content
+
+You can specify a short string as the content of an avatar via the `text` prop. The string should be
+short (1 to 3 characters), and will be transformed via CSS to be all uppercase. The font size will
+be scaled relative to the [`size` prop setting](#sizing).
+
+```html
+
+
+
+
+
+
+
+
+
+
+```
### Image content
@@ -80,11 +96,7 @@ and will be sized to show the avatar's [variant background](#variants) around th
- When using a module bundler and project relative image URLs, please refer to the
[Component img src resolving](/docs/reference/images) reference section for additional details.
-- The `src` prop takes precedence over the `icon` and `text` props.
-- 2.11.0+ If the image fails to load, the avatar will
- fallback to the value of the `icon` or `text` props. If neither the `icon` or `text` props are
- provided, then the default avatar icon will be shown. Also, when the image fails to load, the
- `img-error` event will be emitted.
+- The `src` prop takes precedence over the `text` prop.
### Icon content
@@ -109,29 +121,10 @@ prop should be set to a valid icon name. Icons will scale respective to the [`si
- When providing a BootstrapVue icon name, you _must_ ensure that you have registered the
corresponding icon component (either locally to your component/page, or globally), if not using
the full [`BootstrapVueIcons` plugin](/docs/icons).
-- The `icon` prop takes precedence over the `text` prop.
+- The `icon` prop takes precedence over the `text` and `src` props.
- If the `text`, `src`, or `icon` props are not provided _and_ the [default slot](#custom-content)
has no content, then the `person-fill` icon will be used.
-### Text content
-
-You can specify a short string as the content of an avatar via the `text` prop. The string should be
-short (1 to 3 characters), and will be transformed via CSS to be all uppercase. The font size will
-be scaled relative to the [`size` prop setting](#sizing).
-
-```html
-
-
-
-
-
-
-
-
-
-
-```
-
### Custom content
Use the `default` slot to render custom content in the avatar, for finer grained control of its
diff --git a/src/components/avatar/avatar.js b/src/components/avatar/avatar.js
index 01e66068ab1..0724fa1347c 100644
--- a/src/components/avatar/avatar.js
+++ b/src/components/avatar/avatar.js
@@ -1,3 +1,4 @@
+import { mergeData } from 'vue-functional-data-merge'
import Vue from '../../utils/vue'
import pluckProps from '../../utils/pluck-props'
import { getComponentConfig } from '../../utils/config'
@@ -7,7 +8,6 @@ import { BButton } from '../button/button'
import { BLink } from '../link/link'
import { BIcon } from '../../icons/icon'
import { BIconPersonFill } from '../../icons/icons'
-import normalizeSlotMixin from '../../mixins/normalize-slot'
// --- Constants ---
const NAME = 'BAvatar'
@@ -135,69 +135,30 @@ const computeSize = value => {
// @vue/component
export const BAvatar = /*#__PURE__*/ Vue.extend({
name: NAME,
- mixins: [normalizeSlotMixin],
+ functional: true,
props,
- data() {
- return {
- localSrc: this.src || null
- }
- },
- computed: {
- computedSize() {
- return computeSize(this.size)
- },
- fontSize() {
- const size = this.computedSize
- return size ? `calc(${size} * ${FONT_SIZE_SCALE})` : null
- }
- },
- watch: {
- src(newSrc, oldSrc) {
- if (newSrc !== oldSrc) {
- this.localSrc = newSrc || null
- }
- }
- },
- methods: {
- onImgError() {
- this.localSrc = null
- this.$emit('img-error')
- },
- onClick(evt) {
- this.$emit('click', evt)
- }
- },
- render(h) {
- const {
- variant,
- disabled,
- square,
- icon,
- localSrc: src,
- text,
- fontSize,
- computedSize: size,
- button: isButton,
- buttonType: type
- } = this
- const isBLink = !isButton && (this.href || this.to)
+ render(h, { props, data, children }) {
+ const { variant, disabled, square, icon, src, text, button: isButton, buttonType: type } = props
+ const isBLink = !isButton && (props.href || props.to)
const tag = isButton ? BButton : isBLink ? BLink : 'span'
- const rounded = square ? false : this.rounded === '' ? true : this.rounded || 'circle'
- const alt = this.alt || null
- const ariaLabel = this.ariaLabel || null
+ const rounded = square ? false : props.rounded === '' ? true : props.rounded || 'circle'
+ const size = computeSize(props.size)
+ const alt = props.alt || null
+ const ariaLabel = props.ariaLabel || null
let $content = null
- if (this.hasNormalizedSlot('default')) {
+ if (children) {
// Default slot overrides props
- $content = this.normalizeSlot('default')
- } else if (src) {
- $content = h('img', { attrs: { src, alt }, on: { error: this.onImgError } })
+ $content = children
} else if (icon) {
$content = h(BIcon, {
props: { icon },
attrs: { 'aria-hidden': 'true', alt }
})
+ } else if (src) {
+ $content = h('img', { attrs: { src, alt } })
} else if (text) {
+ const fontSize = size ? `calc(${size} * ${FONT_SIZE_SCALE})` : null
$content = h('span', { style: { fontSize } }, text)
} else {
// Fallback default avatar content
@@ -218,10 +179,9 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({
},
style: { width: size, height: size },
attrs: { 'aria-label': ariaLabel },
- props: isButton ? { variant, disabled, type } : isBLink ? pluckProps(linkProps, this) : {},
- on: isBLink || isButton ? { click: this.onClick } : {}
+ props: isButton ? { variant, disabled, type } : isBLink ? pluckProps(linkProps, props) : {}
}
- return h(tag, componentData, [$content])
+ return h(tag, mergeData(data, componentData), [$content])
}
})
diff --git a/src/components/avatar/avatar.spec.js b/src/components/avatar/avatar.spec.js
index d93a3e47dc5..ccd187e7d0a 100644
--- a/src/components/avatar/avatar.spec.js
+++ b/src/components/avatar/avatar.spec.js
@@ -1,19 +1,16 @@
import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
import { BIconPerson } from '../../icons/icons'
import { BAvatar } from './avatar'
-import { waitNT } from '../../../tests/utils'
describe('avatar', () => {
it('should have expected default structure', async () => {
const wrapper = mount(BAvatar)
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('span')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('badge-secondary')
expect(wrapper.classes()).not.toContain('disabled')
expect(wrapper.attributes('href')).not.toBeDefined()
expect(wrapper.attributes('type')).not.toBeDefined()
- wrapper.destroy()
})
it('should have expected structure when prop `button` set', async () => {
@@ -22,7 +19,6 @@ describe('avatar', () => {
button: true
}
})
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('button')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('btn-secondary')
@@ -33,17 +29,6 @@ describe('avatar', () => {
expect(wrapper.text()).toEqual('')
expect(wrapper.find('.b-icon').exists()).toBe(true)
expect(wrapper.find('img').exists()).toBe(false)
-
- expect(wrapper.emitted('click')).toBeUndefined()
-
- wrapper.trigger('click')
- await waitNT(wrapper.vm)
-
- expect(wrapper.emitted('click')).not.toBeUndefined()
- expect(wrapper.emitted('click').length).toBe(1)
- expect(wrapper.emitted('click')[0][0]).toBeInstanceOf(Event)
-
- wrapper.destroy()
})
it('should have expected structure when prop `href` set', async () => {
@@ -52,7 +37,6 @@ describe('avatar', () => {
href: '#foo'
}
})
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('a')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('badge-secondary')
@@ -64,17 +48,6 @@ describe('avatar', () => {
expect(wrapper.text()).toEqual('')
expect(wrapper.find('.b-icon').exists()).toBe(true)
expect(wrapper.find('img').exists()).toBe(false)
-
- expect(wrapper.emitted('click')).toBeUndefined()
-
- wrapper.trigger('click')
- await waitNT(wrapper.vm)
-
- expect(wrapper.emitted('click')).not.toBeUndefined()
- expect(wrapper.emitted('click').length).toBe(1)
- expect(wrapper.emitted('click')[0][0]).toBeInstanceOf(Event)
-
- wrapper.destroy()
})
it('should have expected structure when prop `text` set', async () => {
@@ -83,7 +56,6 @@ describe('avatar', () => {
text: 'BV'
}
})
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('span')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('badge-secondary')
@@ -93,7 +65,6 @@ describe('avatar', () => {
expect(wrapper.text()).toContain('BV')
expect(wrapper.find('.b-icon').exists()).toBe(false)
expect(wrapper.find('img').exists()).toBe(false)
- wrapper.destroy()
})
it('should have expected structure when default slot used', async () => {
@@ -105,7 +76,6 @@ describe('avatar', () => {
default: 'BAR'
}
})
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('span')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('badge-secondary')
@@ -116,17 +86,14 @@ describe('avatar', () => {
expect(wrapper.text()).not.toContain('FOO')
expect(wrapper.find('.b-icon').exists()).toBe(false)
expect(wrapper.find('img').exists()).toBe(false)
- wrapper.destroy()
})
it('should have expected structure when prop `src` set', async () => {
const wrapper = mount(BAvatar, {
propsData: {
- src: '/foo/bar',
- text: 'BV'
+ src: '/foo/bar'
}
})
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('span')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('badge-secondary')
@@ -137,31 +104,9 @@ describe('avatar', () => {
expect(wrapper.find('.b-icon').exists()).toBe(false)
expect(wrapper.find('img').exists()).toBe(true)
expect(wrapper.find('img').attributes('src')).toEqual('/foo/bar')
- expect(wrapper.text()).not.toContain('BV')
-
- wrapper.setProps({
- src: '/foo/baz'
- })
- await waitNT(wrapper.vm)
-
- expect(wrapper.find('img').exists()).toBe(true)
- expect(wrapper.find('img').attributes('src')).toEqual('/foo/baz')
- expect(wrapper.text()).not.toContain('BV')
- expect(wrapper.emitted('img-error')).not.toBeDefined()
- expect(wrapper.text()).not.toContain('BV')
-
- // Fake an image error
- wrapper.find('img').trigger('error')
- await waitNT(wrapper.vm)
- expect(wrapper.emitted('img-error')).toBeDefined()
- expect(wrapper.emitted('img-error').length).toBe(1)
- expect(wrapper.find('img').exists()).toBe(false)
- expect(wrapper.text()).toContain('BV')
-
- wrapper.destroy()
})
- it('should have expected structure when prop `icon` set', async () => {
+ it('should have expected structure when prop `src` set', async () => {
const localVue = new CreateLocalVue()
localVue.component('BIconPerson', BIconPerson)
const wrapper = mount(BAvatar, {
@@ -170,7 +115,6 @@ describe('avatar', () => {
icon: 'person'
}
})
- expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('span')).toBe(true)
expect(wrapper.classes()).toContain('b-avatar')
expect(wrapper.classes()).toContain('badge-secondary')
@@ -181,40 +125,31 @@ describe('avatar', () => {
const $icon = wrapper.find('.b-icon')
expect($icon.exists()).toBe(true)
expect($icon.classes()).toContain('bi-person')
- wrapper.destroy()
})
it('`size` prop should work as expected', async () => {
const wrapper1 = mount(BAvatar)
expect(wrapper1.attributes('style')).toEqual('width: 2.5em; height: 2.5em;')
- wrapper1.destroy()
const wrapper2 = mount(BAvatar, { propsData: { size: 'sm' } })
expect(wrapper2.attributes('style')).toEqual('width: 1.5em; height: 1.5em;')
- wrapper2.destroy()
const wrapper3 = mount(BAvatar, { propsData: { size: 'md' } })
expect(wrapper3.attributes('style')).toEqual('width: 2.5em; height: 2.5em;')
- wrapper3.destroy()
const wrapper4 = mount(BAvatar, { propsData: { size: 'lg' } })
expect(wrapper4.attributes('style')).toEqual('width: 3.5em; height: 3.5em;')
- wrapper4.destroy()
const wrapper5 = mount(BAvatar, { propsData: { size: 20 } })
expect(wrapper5.attributes('style')).toEqual('width: 20px; height: 20px;')
- wrapper5.destroy()
const wrapper6 = mount(BAvatar, { propsData: { size: '24.5' } })
expect(wrapper6.attributes('style')).toEqual('width: 24.5px; height: 24.5px;')
- wrapper6.destroy()
const wrapper7 = mount(BAvatar, { propsData: { size: '5em' } })
expect(wrapper7.attributes('style')).toEqual('width: 5em; height: 5em;')
- wrapper7.destroy()
const wrapper8 = mount(BAvatar, { propsData: { size: '36px' } })
expect(wrapper8.attributes('style')).toEqual('width: 36px; height: 36px;')
- wrapper8.destroy()
})
})
diff --git a/src/components/avatar/package.json b/src/components/avatar/package.json
index e65d10ddafa..e1ec1db2022 100644
--- a/src/components/avatar/package.json
+++ b/src/components/avatar/package.json
@@ -62,18 +62,13 @@
"events": [
{
"event": "click",
- "description": "Emitted when the avatar is clicked when rendered as a button or link. Not emitted if not a button or link",
+ "description": "Emitted when the avatar is clicked (when rendered as a button or link)",
"args": [
{
"arg": "evt",
"description": "Native event object"
}
]
- },
- {
- "event": "img-error",
- "version": "2.11.0",
- "description": "Emitted if an image `src` is provided and the image fails to load"
}
]
}