Skip to content

Commit

Permalink
feat(UsaCharacterCount): implement UsaCharacterCount component
Browse files Browse the repository at this point in the history
ISSUES CLOSED: #16
  • Loading branch information
patrickcate committed Feb 26, 2022
1 parent 27d366f commit 1ab70a6
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 4 deletions.
127 changes: 127 additions & 0 deletions src/components/UsaCharacterCount/UsaCharacterCount.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import UsaCharacterCount from './UsaCharacterCount.vue'
import UsaTextInput from '@/components/UsaTextInput'
import UsaTextarea from '@/components/UsaTextarea'
import { ref } from 'vue'

const defaultProps = {
maxlength: UsaCharacterCount.props.maxlength.default,
id: UsaCharacterCount.props.id.default,
}

export default {
component: UsaCharacterCount,
title: 'Components/UsaCharacterCount',
argTypes: {
maxlength: {
control: { type: 'number' },
},
id: {
control: { type: 'text' },
},
defaultSlot: {
control: { type: 'text' },
},
equalMessageSlot: {
control: { type: 'text' },
},
remainingMessageSlot: {
control: { type: 'text' },
},
overMessageSlot: {
control: { type: 'text' },
},
},
args: {
maxlength: defaultProps.maxlength,
id: defaultProps.id,
defaultSlot: '',
equalMessageSlot: '',
remainingMessageSlot: '',
overMessageSlot: '',
},
}

const DefaultTemplate = (args, { argTypes }) => ({
components: { UsaCharacterCount, UsaTextInput, UsaTextarea },
props: Object.keys(argTypes),
setup() {
return { ...args }
},
template: `<UsaCharacterCount :id="id" :maxlength="maxlength">
<template v-if="${!!args.defaultSlot}" #default>${
args.defaultSlot
}</template>
<template v-if="${!!args.equalMessageSlot}" #equal-message="{ maxlength }">${
args.equalMessageSlot
}</template>
<template v-if="${!!args.remainingMessageSlot}" #remaining-message="{ maxlength, charactersRemaining }">${
args.remainingMessageSlot
}</template>
<template v-if="${!!args.overMessageSlot}" #over-message="{ maxlength, charactersOver }">${
args.overMessageSlot
}</template>
</UsaCharacterCount>`,
})

export const DefaultCharacterCount = DefaultTemplate.bind({})
DefaultCharacterCount.args = {
...defaultProps,
maxlength: 25,
defaultSlot: '<div><em>UsaTextarea or UsaTextInput goes here</em></div>',
}
DefaultCharacterCount.storyName = 'Default'

export const DefaultTextInputCharacterCount = DefaultTemplate.bind({})
DefaultTextInputCharacterCount.args = {
...defaultProps,
maxlength: 25,
defaultSlot: '<UsaTextInput label="Text input"></UsaTextInput>',
}
DefaultTextInputCharacterCount.storyName = 'Text Input'

export const DefaultTextareaCharacterCount = DefaultTemplate.bind({})
DefaultTextareaCharacterCount.args = {
...defaultProps,
maxlength: 50,
defaultSlot: '<UsaTextarea label="Textarea"></UsaTextarea>',
}
DefaultTextareaCharacterCount.storyName = 'Textarea'

export const EqualMessageScopedSlotCharacterCount = DefaultTemplate.bind({})
EqualMessageScopedSlotCharacterCount.args = {
...defaultProps,
maxlength: 25,
defaultSlot: '<UsaTextInput label="Text input"></UsaTextInput>',
equalMessageSlot: 'You can enter up to {{ maxlength }} characters',
}
EqualMessageScopedSlotCharacterCount.storyName = 'Custom Count Message Slot'

export const RemainingMessageScopedSlotCharacterCount = DefaultTemplate.bind({})
RemainingMessageScopedSlotCharacterCount.args = {
...defaultProps,
maxlength: 25,
defaultSlot: `<UsaTextInput :model-value="'some test text'" label="Text input"></UsaTextInput>`,
remainingMessageSlot:
'{{ charactersRemaining }} out of {{ maxlength }} characters remaining',
}
RemainingMessageScopedSlotCharacterCount.storyName =
'Custom Remaining Message Slot'

export const OverMessageScopedSlotCharacterCount = DefaultTemplate.bind({})
OverMessageScopedSlotCharacterCount.args = {
...defaultProps,
maxlength: 20,
defaultSlot: `<UsaTextInput :model-value="'some really long test text'" label="Text input"></UsaTextInput>`,
overMessageSlot:
'{{ charactersOver }} over the {{ maxlength }} character max',
}
OverMessageScopedSlotCharacterCount.storyName = 'Custom Over Message Slot'

export const CustomIdCharacterCount = DefaultTemplate.bind({})
CustomIdCharacterCount.args = {
...defaultProps,
maxlength: 25,
id: 'custom-id',
defaultSlot: '<UsaTextInput label="Text input"></UsaTextInput>',
}
CustomIdCharacterCount.storyName = 'Custom ID'
248 changes: 248 additions & 0 deletions src/components/UsaCharacterCount/UsaCharacterCount.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import '@module/uswds/dist/css/uswds.min.css'
import { mount } from '@cypress/vue'
import { h } from 'vue'
import UsaCharacterCount from './UsaCharacterCount.vue'
import UsaTextInput from '@/components/UsaTextInput'
import UsaTextarea from '@/components/UsaTextarea'

describe('UsaCharacterCount', () => {
it('renders the component with text input', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 10,
},
slots: {
default: () => h(UsaTextInput),
},
}).as('wrapper')

cy.get('div.usa-character-count').should('exist')
cy.get('span.usa-hint')
.as('message')
.should('have.class', 'usa-character-count__message')
.and('have.attr', 'id')

cy.get('@message')
.should('not.have.class', 'usa-character-count__message--invalid')
.and('have.attr', 'aria-live')
.and('contain', 'polite')

cy.get('@message').should('contain', '10 characters allowed')

cy.get('.usa-input')
.as('input')
.should('have.class', 'usa-character-count__field')
.and('have.attr', 'maxlength')
.and('contain', 10)

cy.get('@input').type('12345')
cy.get('@message').should('contain', '5 characters left')

cy.get('@input').type('6789')
cy.get('@message').should('contain', '1 character left')

cy.get('@input').type('1')
cy.get('@message').should('contain', '0 characters left')

cy.get('@input').type('0')
cy.get('@message').should('contain', '0 characters left')

cy.get('@input').invoke('val', 12345678912).trigger('input')
cy.get('@message')
.should('have.class', 'usa-character-count__message--invalid')
.and('contain', '1 character over limit')

cy.get('@input').invoke('val', 123456789123).trigger('input')
cy.get('@message')
.should('have.class', 'usa-character-count__message--invalid')
.and('contain', '2 characters over limit')
})

it('renders the component with textarea form element', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 10,
},
slots: {
default: () => h(UsaTextarea),
},
}).as('wrapper')

cy.get('div.usa-character-count').should('exist')
cy.get('span.usa-hint')
.as('message')
.should('have.class', 'usa-character-count__message')
.and('have.attr', 'id')

cy.get('@message')
.should('not.have.class', 'usa-character-count__message--invalid')
.and('have.attr', 'aria-live')
.and('contain', 'polite')

cy.get('@message').should('contain', '10 characters allowed')

cy.get('.usa-textarea')
.as('textarea')
.should('have.class', 'usa-character-count__field')
.and('have.attr', 'maxlength')
.and('contain', 10)

cy.get('@textarea').type('12345')
cy.get('@message').should('contain', '5 characters left')

cy.get('@textarea').type('6789')
cy.get('@message').should('contain', '1 character left')

cy.get('@textarea').as('input').type('1')
cy.get('@message').should('contain', '0 characters left')

cy.get('@textarea').type('0')
cy.get('@message').should('contain', '0 characters left')

// Force value over maxlength.
cy.get('@textarea').invoke('val', 12345678912).trigger('input')
cy.get('@message')
.should('have.class', 'usa-character-count__message--invalid')
.and('contain', '1 character over limit')

// Force value over maxlength.
cy.get('@textarea').invoke('val', 123456789123).trigger('input')
cy.get('@message')
.should('have.class', 'usa-character-count__message--invalid')
.and('contain', '2 characters over limit')
})

it('character count for text input form element includes default value', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 10,
},
slots: {
default: () => h(UsaTextInput, { 'model-value': 12345 }),
},
}).as('wrapper')

cy.get('.usa-input').should('have.value', 12345)
cy.get('.usa-character-count__message').should(
'contain',
'5 characters left'
)
})

it('character count for textarea form element includes default value', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 10,
},
slots: {
default: () => h(UsaTextarea, { 'model-value': 12345 }),
},
}).as('wrapper')

cy.get('.usa-textarea').should('have.value', 12345)
cy.get('.usa-character-count__message').should(
'contain',
'5 characters left'
)
})

it('custom id is added to message element and referenced on text input form element', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 10,
id: 'test-id',
},
slots: {
default: () => h(UsaTextInput),
},
})

cy.get('.usa-hint').should('have.id', 'test-id')
cy.get('.usa-input')
.should('have.attr', 'aria-describedby')
.and('contain', 'test-id')
})

it('custom id is added to message element and referenced on textarea form element', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 10,
id: 'test-id',
},
slots: {
default: () => h(UsaTextarea),
},
})

cy.get('.usa-hint').should('have.id', 'test-id')
cy.get('.usa-textarea')
.should('have.attr', 'aria-describedby')
.and('contain', 'test-id')
})

it('uses custom slot content and scoped values', () => {
mount(UsaCharacterCount, {
props: {
maxlength: 5,
},
slots: {
default: () => h(UsaTextInput),
'equal-message': ({ maxlength }) =>
h(
'span',
{ class: 'equal-message' },
`equal, maxlength: ${maxlength}`
),
'remaining-message': ({ maxlength, charactersRemaining }) =>
h(
'span',
{ class: 'remaining-message' },
`under, maxlength: ${maxlength}, charactersRemaining: ${charactersRemaining}`
),
'over-message': ({ maxlength, charactersOver }) =>
h(
'span',
{ class: 'over-message' },
`over, maxlength: ${maxlength}, charactersOver: ${charactersOver}`
),
},
}).as('wrapper')

cy.get('span.remaining-message').should('not.exist')
cy.get('span.over-message').should('not.exist')

cy.get('span.equal-message').should('contain', 'equal, maxlength: 5')

cy.get('.usa-input').as('input').type(1)

cy.get('span.equal-message').should('not.exist')
cy.get('span.over-message').should('not.exist')

cy.get('span.remaining-message')
.as('remainingMessage')
.should('contain', 'under, maxlength: 5, charactersRemaining: 4')

// Force value over maxlength.
cy.get('@input').invoke('val', 123456).trigger('input')

cy.get('span.over-message').should(
'contain',
'over, maxlength: 5, charactersOver: 1'
)
})

it('console prints warning about invalid `maxlength` prop value', () => {
cy.stub(window.console, 'warn').as('consoleWarn')

mount(UsaCharacterCount, {
props: {
maxlength: 0,
},
slots: {
default: () => h(UsaTextInput),
},
})

cy.get('@consoleWarn').should('be.calledWith', `0 is not a valid maxlength`)
})
})
Loading

0 comments on commit 1ab70a6

Please sign in to comment.