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" } ] }