Skip to content

Commit

Permalink
feat(Textarea): add characterCounter (#3210)
Browse files Browse the repository at this point in the history
- [x] Implement screen reader support (relies on #3217)
- [x] Align styles
- [x] Align texts in counter
- [x] Add tests
- [x] Add visual test

A couple of visual snapshots needed to get update because a new margin
was added, to align elements below the checkbox.
  • Loading branch information
tujoworker authored Jan 18, 2024
1 parent 35fe238 commit 5c9dde9
Show file tree
Hide file tree
Showing 28 changed files with 172 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ export const TextareaExampleVertical = () => (
</Wrapper>
)

export const TextareaCharacterCounter = () => (
<Wrapper>
<ComponentBox data-visual-test="textarea-character-counter">
<Textarea
label="Count characters"
label_direction="vertical"
autoresize
value="Textarea value\nNewline"
status="Message to the user"
characterCounter
maxLength={40}
/>
</ComponentBox>
</Wrapper>
)

export const TextareaExampleStretched = () => (
<Wrapper>
<ComponentBox data-visual-test="textarea-stretch">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TextareaExampleStretched,
TextareaExampleAutoresize,
TextareaExampleMaxLength,
TextareaCharacterCounter,
TextareaExampleFormStatus,
TextareaExampleDisabled,
TextareaExampleSuffix,
Expand Down Expand Up @@ -40,6 +41,10 @@ import {

<TextareaExampleMaxLength />

### Character counter

<TextareaCharacterCounter />

### With FormStatus failure message

<TextareaExampleFormStatus />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ showTabs: true
| `label_sr_only` | _(optional)_ use `true` to make the label only readable by screen readers. |
| `autoresize` | _(optional)_ use `true` to make the Textarea grow and shrink depending on how many lines the user has filled. |
| `autoresize_max_rows` | _(optional)_ set a number to define how many rows the Textarea can auto grow. |
| `characterCounter` | _(optional)_ use `true` to show a character counter. You need to set a `maxLength={number}` in order to show the counter. |
| `status` | _(optional)_ text with a status message. The style defaults to an error message. You can use `true` to only get the status color, without a message. |
| `status_state` | _(optional)_ defines the state of the status. Currently, there are two statuses `[error, info]`. Defaults to `error`. |
| `status_props` | _(optional)_ use an object to define additional FormStatus properties. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import DataValueReadwriteProperties from '../../data-value-readwrite-properties.
| `trim` | `boolean` | _(optional)_ When `true`, it will trim leading and trailing whitespaces on blur, triggering onChange if the value changes. |
| `inputMode` | `string` | _(optional)_ Define a [inputmode](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode). |
| `autoresizeMaxRows` | `boolean` | _(optional)_ For `multiline`, set how many rows of text can be shown at max. |
| `characterCounter` | `boolean` | _(optional)_ True to show a character counter. You need to set a `maxLength={number}` in order to show the counter. |
| `characterCounter` | `boolean` | _(optional)_ True to show a character counter. You need to set a `maxLength={number}` as well as have `multiline` enabled in order to show the counter. |
| `minLength` | `number` | _(optional)_ Validation for minimum length of the text (number of characters). |
| `maxLength` | `number` | _(optional)_ Validation for maximum length of the text (number of characters). |
| `pattern` | `string` | _(optional)_ Validation based on regex pattern. |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/dnb-eufemia/src/components/textarea/Textarea.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export interface TextareaProps
* Use `true` to make the Textarea grow and shrink depending on how many lines the user has filled.
*/
autoresize?: boolean;
/**
* use `true` to show a character counter.
*/
characterCounter?: boolean;
/**
* Set a number to define how many rows the Textarea can auto grow.
*/
Expand Down
35 changes: 33 additions & 2 deletions packages/dnb-eufemia/src/components/textarea/Textarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import FormLabel from '../form-label/FormLabel'
import FormStatus from '../form-status/FormStatus'
import AriaLive from '../aria-live/AriaLive'
import {
isTrue,
makeUniqueId,
Expand Down Expand Up @@ -76,6 +77,7 @@ export default class Textarea extends React.PureComponent {
stretch: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
disabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
skeleton: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
characterCounter: PropTypes.bool,
autoresize: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
autoresize_max_rows: PropTypes.oneOfType([
PropTypes.string,
Expand Down Expand Up @@ -128,6 +130,7 @@ export default class Textarea extends React.PureComponent {
skeleton: null,
autoresize: null,
autoresize_max_rows: null,
characterCounter: null,
textarea_class: null,
class: null,
textarea_attributes: null,
Expand Down Expand Up @@ -240,7 +243,6 @@ export default class Textarea extends React.PureComponent {
const { value } = event.target
this.setState({
value,

textareaState: Textarea.hasValue(value) ? 'dirty' : 'initial',
})
dispatchCustomElementEvent(this, 'on_blur', { value, event })
Expand Down Expand Up @@ -334,6 +336,33 @@ export default class Textarea extends React.PureComponent {
getLineHeight() {
return parseFloat(getComputedStyle(this._ref.current).lineHeight) || 0
}
getCounter = () => {
const { characterCounter, maxLength } = this.props

if (characterCounter !== true || !maxLength) {
return null
}

const { value, textareaState } = this.state
const count = (value || '').length

const message = this.context
.getTranslation(this.props)
.Textarea.characterCounter.replace('%count', count)
.replace('%max', maxLength)

return (
<>
<span className="dnb-textarea__counter">{message}</span>
<AriaLive
disabled={count > 0 && textareaState === 'virgin'}
delay={2000}
>
{message}
</AriaLive>
</>
)
}
render() {
// use only the props from context, who are available here anyway
const props = extendPropsWithContextInClassComponent(
Expand Down Expand Up @@ -367,9 +396,9 @@ export default class Textarea extends React.PureComponent {
textarea_attributes,
class: _className,
className,

autoresize,
autoresize_max_rows, //eslint-disable-line
characterCounter, //eslint-disable-line
id: _id, //eslint-disable-line
children, //eslint-disable-line
value: _value, //eslint-disable-line
Expand Down Expand Up @@ -539,6 +568,8 @@ export default class Textarea extends React.PureComponent {
</Suffix>
)}
</span>

{this.getCounter()}
</span>
</span>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ describe.each(['ui', 'sbanken'])('Textarea for %s', (themeName) => {
expect(screenshot).toMatchImageSnapshot()
})

it('have to match character counter', async () => {
const screenshot = await makeScreenshot({
style,
selector: '[data-visual-test="textarea-character-counter"]',
// Only for screenshot testing - make textarea having same width on linux chromium
styleSelector: '[data-visual-test="textarea-default"] textarea',
})
expect(screenshot).toMatchImageSnapshot()
})

it('have to match the default error textarea style', async () => {
const screenshot = await makeScreenshot({
style,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,34 @@ describe('Textarea component', () => {
expect(ref.current.tagName).toBe('TEXTAREA')
expect(ref.current).toBeInstanceOf(HTMLTextAreaElement)
})

it('should render characterCounter', async () => {
const { rerender } = render(
<Textarea maxLength={8} characterCounter value="foo" />
)

const counter = document.querySelector('.dnb-textarea__counter')
const textarea = document.querySelector('textarea')
const ariaLive = document.querySelector('.dnb-aria-live')

expect(counter).toHaveTextContent('3 av 8 gjenstående tegn')
expect(ariaLive).toHaveTextContent('')

await userEvent.type(textarea, 'bar')

expect(counter).toHaveTextContent('6 av 8 gjenstående tegn')
expect(ariaLive).toHaveTextContent('6 av 8 gjenstående tegn')

rerender(
<Textarea maxLength={8} characterCounter value="foo" lang="en-GB" />
)

expect(counter).toHaveTextContent('6 of 8 characters remaining')

await userEvent.type(textarea, 'baz')

expect(ariaLive).toHaveTextContent('8 of 8 characters remaining')
})
})

describe('Textarea scss', () => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ button .dnb-form-status__text {
.dnb-textarea__inner {
display: inline-flex;
flex-direction: column;
margin: var(--textarea-padding-width);
margin: 0 var(--textarea-padding-width);
}
.dnb-textarea__shell {
display: inline-flex;
Expand All @@ -175,10 +175,17 @@ button .dnb-form-status__text {
}
.dnb-textarea__row {
display: inline-flex;
margin: var(--textarea-padding-width) 0;
}
.dnb-textarea__suffix.dnb-suffix {
padding-left: 1rem;
}
.dnb-textarea__counter {
margin-top: 0.5rem;
margin-left: -0.5rem;
font-size: var(--font-size-small);
color: var(--color-black-55);
}
.dnb-textarea__textarea {
position: relative;
z-index: 2;
Expand Down Expand Up @@ -265,7 +272,7 @@ html:not([data-visual-test]) .dnb-textarea__textarea {
}
.dnb-textarea__inner > .dnb-form-status {
order: 2;
margin: 1rem 0 0 calc(0px - var(--textarea-padding-width));
margin: 0.5rem 0 0 calc(0px - var(--textarea-padding-width));
}
.dnb-textarea:not(.dnb-textarea--vertical) .dnb-form-label {
margin-top: 0.5rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
display: inline-flex;
flex-direction: column;

margin: var(--textarea-padding-width);
margin: 0 var(--textarea-padding-width);
}

&__shell {
Expand All @@ -58,12 +58,22 @@

&__row {
display: inline-flex;

margin: var(--textarea-padding-width) 0;
}

&__suffix.dnb-suffix {
padding-left: 1rem;
}

&__counter {
margin-top: 0.5rem;
margin-left: -0.5rem;

font-size: var(--font-size-small);
color: var(--color-black-55);
}

&__textarea {
position: relative;
z-index: 2;
Expand Down Expand Up @@ -138,7 +148,7 @@

&__inner > .dnb-form-status {
order: 2;
margin: 1rem 0 0 calc(1px - 1px - var(--textarea-padding-width));
margin: 0.5rem 0 0 calc(1px - 1px - var(--textarea-padding-width));
}

&:not(#{&}--vertical) .dnb-form-label {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type Props = FieldHelpProps &
autoComplete?: HTMLInputElement['autocomplete']
inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode']
autoresizeMaxRows?: number
characterCounter?: boolean
mask?: InputMaskedProps['mask']
// Validation
minLength?: number
Expand Down Expand Up @@ -140,6 +141,7 @@ function StringComponent(props: Props) {
clear,
autoresize = true,
autoresizeMaxRows = 6,
characterCounter,
mask,
width,
handleFocus,
Expand Down Expand Up @@ -193,6 +195,8 @@ function StringComponent(props: Props) {
{...sharedProps}
autoresize={autoresize}
autoresize_max_rows={autoresizeMaxRows}
characterCounter={characterCounter}
maxLength={characterCounter ? props.maxLength : undefined}
/>
) : mask ? (
<InputMasked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
fireEvent,
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Provider } from '../../../../../shared'
import * as DataContext from '../../../DataContext'
import * as Field from '../..'
import { FieldBlock } from '../../..'
Expand Down Expand Up @@ -805,6 +806,48 @@ describe('Field.String', () => {
})
})

it('should render characterCounter', async () => {
const { rerender } = render(
<Provider>
<Field.String
multiline
maxLength={8}
characterCounter
value="foo"
/>
</Provider>
)

const counter = document.querySelector('.dnb-textarea__counter')
const textarea = document.querySelector('textarea')
const ariaLive = document.querySelector('.dnb-aria-live')

expect(counter).toHaveTextContent('3 av 8 gjenstående tegn')
expect(ariaLive).toHaveTextContent('')

await userEvent.type(textarea, 'bar')

expect(counter).toHaveTextContent('6 av 8 gjenstående tegn')
expect(ariaLive).toHaveTextContent('6 av 8 gjenstående tegn')

rerender(
<Provider locale="en-GB">
<Field.String
multiline
maxLength={8}
characterCounter
value="foo"
/>
</Provider>
)

expect(counter).toHaveTextContent('6 of 8 characters remaining')

await userEvent.type(textarea, 'baz')

expect(ariaLive).toHaveTextContent('8 of 8 characters remaining')
})

it('gets valid ref element', () => {
const id = 'unique'
let ref: React.RefObject<HTMLInputElement>
Expand Down
3 changes: 3 additions & 0 deletions packages/dnb-eufemia/src/shared/locales/en-GB.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export default {
'en-GB': {
Textarea: {
characterCounter: '%count of %max characters remaining',
},
TimelineItem: {
alt_label_completed: 'Complete',
alt_label_current: 'Current',
Expand Down
3 changes: 3 additions & 0 deletions packages/dnb-eufemia/src/shared/locales/nb-NO.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export default {
'nb-NO': {
Textarea: {
characterCounter: '%count av %max gjenstående tegn',
},
TimelineItem: {
alt_label_completed: 'Utført',
alt_label_current: 'Nåværende',
Expand Down

0 comments on commit 5c9dde9

Please sign in to comment.