Skip to content

Commit

Permalink
feat(UsaIcon): implement UsaIcon component
Browse files Browse the repository at this point in the history
ISSUES CLOSED: #174
  • Loading branch information
patrickcate committed May 15, 2022
1 parent 978a765 commit ffb9401
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 0 deletions.
83 changes: 83 additions & 0 deletions src/components/UsaIcon/UsaIcon.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import UsaIcon from './UsaIcon.vue'

const defaultProps = {
// Name is required.
name: 'flag',
size: UsaIcon.props.size.default,
ariaHidden: UsaIcon.props.ariaHidden.default,
role: UsaIcon.props.role.default,
focusable: UsaIcon.props.focusable.default,
}

export default {
component: UsaIcon,
title: 'Components/UsaIcon',
argTypes: {
name: {
control: { type: 'text' },
},
size: {
options: ['', '3', '4', '5', '6', '7', '8', '9'],
control: {
type: 'select',
},
},
ariaHidden: {
control: { type: 'boolean' },
},
role: {
control: { type: 'text' },
},
focusable: {
control: { type: 'boolean' },
},
titleSlot: {
control: { type: 'text' },
},
},
args: {
name: defaultProps.name,
size: defaultProps.size,
ariaHidden: defaultProps.ariaHidden,
role: defaultProps.role,
focusable: defaultProps.focusable,
titleSlot: '',
},
}

const DefaultTemplate = (args, { argTypes }) => ({
components: { UsaIcon },
props: Object.keys(argTypes),
setup() {
return { ...args }
},
template: `<UsaIcon
:name="name"
:size="size"
:aria-hidden="ariaHidden"
:role="role"
:focusable="focusable"
>
<template v-if="${!!args.titleSlot}" #title>${args.titleSlot}</template>
</UsaIcon>`,
})

export const DefaultIcon = DefaultTemplate.bind({})
DefaultIcon.args = {
...defaultProps,
}
DefaultIcon.storyName = 'Default'

export const CustomSizeIcon = DefaultTemplate.bind({})
CustomSizeIcon.args = {
...defaultProps,
size: '9',
}
CustomSizeIcon.storyName = 'Custom Size'

export const TitleSlotIcon = DefaultTemplate.bind({})
TitleSlotIcon.args = {
...defaultProps,
titleSlot: '<title>Flag icon</title>',
}
TitleSlotIcon.storyName = 'Title Slot'
79 changes: 79 additions & 0 deletions src/components/UsaIcon/UsaIcon.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import '@module/uswds/dist/css/uswds.min.css'
import { mount } from '@cypress/vue'
import { h } from 'vue'
import UsaIcon from './UsaIcon.vue'

describe('UsaIcon', () => {
it('renders the component', () => {
mount(UsaIcon, {
props: {
name: 'flag',
},
})

cy.get('svg.usa-icon')
.should('have.attr', 'aria-hidden', 'true')
.and('have.attr', 'role', 'img')
.and('have.attr', 'focusable', 'false')

cy.get('svg > use').should(
'have.attr',
'xlink:href',
'/assets/img/sprite.svg#flag'
)
})

it('attribute values match prop values', () => {
mount(UsaIcon, {
props: {
name: 'github',
ariaHidden: false,
role: 'presentation',
focusable: true,
size: 3,
},
global: {
provide: {
'vueUswds.svgSpritePath': '/test.svg',
},
},
})

cy.get('.usa-icon')
.should('have.attr', 'aria-hidden', 'false')
.and('have.attr', 'role', 'presentation')
.and('have.attr', 'focusable', 'true')
.and('have.class', 'usa-icon--size-3')

cy.get('svg > use').should('have.attr', 'xlink:href', '/test.svg#github')
})

it('custom slot content is used', () => {
mount(UsaIcon, {
props: {
name: 'bug_report',
},
slots: {
title: () => h('title', {}, 'custom title slot'),
},
})

cy.get('svg title').should('contain', 'custom title slot')
})

it('warns in console about invalid `size` prop', () => {
cy.stub(window.console, 'warn').as('consoleWarn')

mount(UsaIcon, {
props: {
name: 'zoom_out_map',
size: 10,
},
})

cy.get('@consoleWarn').should(
'be.calledWith',
`'10' is not a valid icon size`
)
})
})
61 changes: 61 additions & 0 deletions src/components/UsaIcon/UsaIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup>
import { computed, inject } from 'vue'
import { SVG_SPRITE_PATH } from '@/utils/constants.js'
const svgSpritePath = inject('vueUswds.svgSpritePath', SVG_SPRITE_PATH)
const props = defineProps({
name: {
type: String,
required: true,
},
size: {
type: [String, Number],
default: '',
validator(size) {
const isValidSize = ['', '3', '4', '5', '6', '7', '8', '9'].includes(
`${size}`
)
if (!isValidSize) {
console.warn(`'${size}' is not a valid icon size`)
}
return isValidSize
},
},
ariaHidden: {
type: Boolean,
default: true,
},
role: {
type: String,
default: 'img',
},
focusable: {
type: Boolean,
default: false,
},
})
const classes = computed(() => [
{
[`usa-icon--size-${props.size}`]: !!props.size,
},
])
const iconHref = computed(() => `${svgSpritePath}#${props.name}`)
</script>

<template>
<svg
class="usa-icon"
:class="classes"
:aria-hidden="ariaHidden"
:role="role"
:focusable="focusable"
>
<slot name="title"></slot>
<use v-bind="{ 'xlink:href': iconHref }"></use>
</svg>
</template>
4 changes: 4 additions & 0 deletions src/components/UsaIcon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import UsaIcon from './UsaIcon.vue'

export { UsaIcon }
export default UsaIcon
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ export { default as UsaTag } from './UsaTag'
export { default as UsaTextarea } from './UsaTextarea'
export { default as UsaTextInput } from './UsaTextInput'
export { default as UsaValidation } from './UsaValidation'
export { default as UsaIcon } from './UsaIcon'

0 comments on commit ffb9401

Please sign in to comment.