-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(UsaCharacterCount): implement
UsaCharacterCount
component
ISSUES CLOSED: #16
- Loading branch information
1 parent
27d366f
commit 1ab70a6
Showing
8 changed files
with
532 additions
and
4 deletions.
There are no files selected for viewing
127 changes: 127 additions & 0 deletions
127
src/components/UsaCharacterCount/UsaCharacterCount.stories.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
248
src/components/UsaCharacterCount/UsaCharacterCount.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`) | ||
}) | ||
}) |
Oops, something went wrong.