From 3093c28700d6aec7b12de779e53a9b4c475c3cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Mon, 22 Jan 2024 09:36:02 +0100 Subject: [PATCH] feat(TextCounter): add new fragment used in Textarea (#3250) This is to compliment PR #3210 and make it possible to easier use it in other cases as well. --- .../components/fragments/text-counter.mdx | 15 ++ .../fragments/text-counter/Examples.tsx | 77 +++++++++ .../fragments/text-counter/demos.mdx | 19 +++ .../fragments/text-counter/info.mdx | 11 ++ .../fragments/text-counter/properties.mdx | 13 ++ .../uilib/components/textarea/properties.mdx | 2 +- .../forms/base-fields/String/properties.mdx | 44 ++--- .../src/components/textarea/Textarea.d.ts | 2 +- .../src/components/textarea/Textarea.js | 51 +++--- .../textarea/__tests__/Textarea.test.tsx | 12 +- ...n-have-to-match-character-counter.snap.png | Bin 13129 -> 12804 bytes ...i-have-to-match-character-counter.snap.png | Bin 13601 -> 13309 bytes .../__snapshots__/Textarea.test.tsx.snap | 4 +- .../textarea/style/dnb-textarea.scss | 5 +- .../Field/String/__tests__/String.test.tsx | 12 +- .../dnb-eufemia/src/fragments/TextCounter.ts | 14 ++ packages/dnb-eufemia/src/fragments/index.ts | 3 +- packages/dnb-eufemia/src/fragments/lib.ts | 5 +- .../fragments/text-counter/TextCounter.tsx | 65 ++++++++ .../__tests__/TextCounter.screenshot.test.ts | 30 ++++ .../__tests__/TextCounter.test.tsx | 155 ++++++++++++++++++ ...ve-to-character-counter-downwards.snap.png | Bin 0 -> 2620 bytes ...have-to-character-counter-upwards.snap.png | Bin 0 -> 2786 bytes ...ve-to-character-counter-downwards.snap.png | Bin 0 -> 2812 bytes ...have-to-character-counter-upwards.snap.png | Bin 0 -> 2975 bytes .../src/fragments/text-counter/index.ts | 6 + .../src/fragments/text-counter/style.ts | 6 + .../text-counter/style/dnb-text-counter.scss | 3 + .../src/fragments/text-counter/style/index.js | 6 + .../dnb-text-counter-theme-sbanken.scss | 6 + .../themes/dnb-text-counter-theme-ui.scss | 6 + .../fragments/text-counter/style/themes/ui.js | 6 + .../dnb-eufemia/src/shared/locales/en-GB.js | 5 +- .../dnb-eufemia/src/shared/locales/nb-NO.js | 5 +- .../src/style/dnb-ui-fragments.scss | 1 + .../eiendom-theme-components.scss | 1 + .../sbanken-theme-components.scss | 1 + .../themes/theme-ui/ui-theme-components.scss | 1 + 38 files changed, 510 insertions(+), 82 deletions(-) create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/Examples.tsx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/demos.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/info.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/properties.mdx create mode 100644 packages/dnb-eufemia/src/fragments/TextCounter.ts create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/TextCounter.tsx create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/__tests__/TextCounter.screenshot.test.ts create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/__tests__/TextCounter.test.tsx create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/__tests__/__image_snapshots__/textcounter-for-sbanken-have-to-character-counter-downwards.snap.png create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/__tests__/__image_snapshots__/textcounter-for-sbanken-have-to-character-counter-upwards.snap.png create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/__tests__/__image_snapshots__/textcounter-for-ui-have-to-character-counter-downwards.snap.png create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/__tests__/__image_snapshots__/textcounter-for-ui-have-to-character-counter-upwards.snap.png create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/index.ts create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/style.ts create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/style/dnb-text-counter.scss create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/style/index.js create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/style/themes/dnb-text-counter-theme-sbanken.scss create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/style/themes/dnb-text-counter-theme-ui.scss create mode 100644 packages/dnb-eufemia/src/fragments/text-counter/style/themes/ui.js diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter.mdx new file mode 100644 index 00000000000..2d3c8bf3692 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter.mdx @@ -0,0 +1,15 @@ +--- +title: 'TextCounter' +description: 'The TextCounter is a component designed to provide real-time character count feedback in text input fields.' +showTabs: true +theme: 'sbanken' +status: null +hideTabs: + - title: Events +--- + +import Info from './text-counter/info' +import Demos from './text-counter/demos' + + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/Examples.tsx new file mode 100644 index 00000000000..646d77d0f15 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/Examples.tsx @@ -0,0 +1,77 @@ +/** + * UI lib Component Example + * + */ + +import React from 'react' +import ComponentBox from '../../../../../shared/tags/ComponentBox' +import { TextCounter } from '@dnb/eufemia/src/fragments' +import { Field, Form } from '@dnb/eufemia/src/extensions/forms' +import { Flex } from '@dnb/eufemia/src' +import type { TextCounterProps } from '@dnb/eufemia/src/fragments/text-counter/TextCounter' + +export function CountCharactersDown() { + return ( + + + + ) +} + +export function CountCharactersUp() { + return ( + + + + ) +} + +export function CountCharactersInteractive() { + return ( + + {() => { + const Counter = () => { + const { data } = Form.useData('text-counter-up', initialData) + return ( + + + + + + + + ) + } + + const variant: TextCounterProps['variant'] = 'down' + const initialData = { + max: 10, + variant, + text: 'Count me!', + } + + return ( + + + + ) + }} + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/demos.mdx new file mode 100644 index 00000000000..4a7a14bbad9 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/demos.mdx @@ -0,0 +1,19 @@ +--- +showTabs: true +--- + +import * as Examples from './Examples' + +## Demos + +### Count characters downwards + + + +### Count characters upwards + + + +### Interactive + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/info.mdx new file mode 100644 index 00000000000..801a11e4d64 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/info.mdx @@ -0,0 +1,11 @@ +--- +showTabs: true +--- + +## Description + +The `TextCounter` is a component designed to provide real-time character count feedback in text input fields. + +It provides the correct text translations and color and a visual indicator of the remaining characters. + +It is used in the [Textarea](/uilib/components/textarea/) component. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/properties.mdx new file mode 100644 index 00000000000..e4a6acc4241 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/text-counter/properties.mdx @@ -0,0 +1,13 @@ +--- +showTabs: true +--- + +## Properties + +| Properties | Description | +| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `text` | _(required)_ The text to count characters from. | +| `max` | _(required)_ The maximum number of characters allowed. | +| `variant` | _(optional)_ The counting variant. Can be either `up` (counts up from zero) or `down` (counts down from max). Default is `down`. | +| `bypassAriaLive` | _(optional)_ If true, AriaLive is bypassed. This is useful to signal that a given input field value should not be read out. | +| [Space](/uilib/layout/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/textarea/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/textarea/properties.mdx index 42e982a95b0..f4789faa603 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/textarea/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/textarea/properties.mdx @@ -16,7 +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. | +| `characterCounter` | _(optional)_ use `up` or `down` or `true` (down) 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. | diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/String/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/String/properties.mdx index 662f70ea97a..0668d223dbb 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/String/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/base-fields/String/properties.mdx @@ -6,28 +6,28 @@ import DataValueReadwriteProperties from '../../data-value-readwrite-properties. ### Component-specific props -| Property | Type | Description | -| --------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | `string` | _(optional)_ Input DOM element type. | -| `multiline` | `boolean` | _(optional)_ True to be able to write in multiple lines (switching from input-element to textarea-element). | -| `leftIcon` | `string` | _(optional)_ For icon at the left side of the text input. | -| `rightIcon` | `string` | _(optional)_ For icon at the right side of the text input. | -| `inputClassName` | `string` | _(optional)_ Class name set on the <input> DOM element. | -| `innerRef` | `React.ref` | _(optional)_ by providing a React.ref we can get the internally used input element (DOM). E.g. `innerRef={myRef}` by using `React.createRef()` or `React.useRef()`. | -| `clear` | `boolean` | _(optional)_ True to have a clickable clear-icon for removing the active value. | -| `autoresize` | `boolean` | _(optional)_ For `multiline`, set `true` to expand when writing longer texts. | -| `autoComplete` | `on` or `string` | _(optional)_ For HTML `autocomplete` [attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete). | -| `capitalize` | `boolean` | _(optional)_ When set to `true`, it will capitalize the first letter of every word, transforming the rest to lowercase. | -| `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}` 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. | -| `width` | `string` or `false` | _(optional)_ `false` for no width (use browser default), `small`, `medium` or `large` for predefined standard widths, `stretch` for fill available width. | -| `help` | `object` | _(optional)_ Provide a help button. Object consisting of `title` and `contents`. | -| [Space](/uilib/layout/space/properties) | Various | _(optional)_ Spacing properties like `top` or `bottom` are supported. | +| Property | Type | Description | +| --------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `string` | _(optional)_ Input DOM element type. | +| `multiline` | `boolean` | _(optional)_ True to be able to write in multiple lines (switching from input-element to textarea-element). | +| `leftIcon` | `string` | _(optional)_ For icon at the left side of the text input. | +| `rightIcon` | `string` | _(optional)_ For icon at the right side of the text input. | +| `inputClassName` | `string` | _(optional)_ Class name set on the <input> DOM element. | +| `innerRef` | `React.ref` | _(optional)_ by providing a React.ref we can get the internally used input element (DOM). E.g. `innerRef={myRef}` by using `React.createRef()` or `React.useRef()`. | +| `clear` | `boolean` | _(optional)_ True to have a clickable clear-icon for removing the active value. | +| `autoresize` | `boolean` | _(optional)_ For `multiline`, set `true` to expand when writing longer texts. | +| `autoComplete` | `on` or `string` | _(optional)_ For HTML `autocomplete` [attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete). | +| `capitalize` | `boolean` | _(optional)_ When set to `true`, it will capitalize the first letter of every word, transforming the rest to lowercase. | +| `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` or `string` | _(optional)_ Use `up` or `down` or `true` (down) 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. | +| `width` | `string` or `false` | _(optional)_ `false` for no width (use browser default), `small`, `medium` or `large` for predefined standard widths, `stretch` for fill available width. | +| `help` | `object` | _(optional)_ Provide a help button. Object consisting of `title` and `contents`. | +| [Space](/uilib/layout/space/properties) | Various | _(optional)_ Spacing properties like `top` or `bottom` are supported. | ## Properties diff --git a/packages/dnb-eufemia/src/components/textarea/Textarea.d.ts b/packages/dnb-eufemia/src/components/textarea/Textarea.d.ts index f3c57763dd4..eea0da863d6 100644 --- a/packages/dnb-eufemia/src/components/textarea/Textarea.d.ts +++ b/packages/dnb-eufemia/src/components/textarea/Textarea.d.ts @@ -88,7 +88,7 @@ export interface TextareaProps */ autoresize?: boolean; /** - * use `true` to show a character counter. + * Use `up` or `down` or `true` (down) to show a character counter. You need to set a `maxLength={number}` in order to show the counter. */ characterCounter?: boolean; /** diff --git a/packages/dnb-eufemia/src/components/textarea/Textarea.js b/packages/dnb-eufemia/src/components/textarea/Textarea.js index a92aff028ca..bb6971138cd 100644 --- a/packages/dnb-eufemia/src/components/textarea/Textarea.js +++ b/packages/dnb-eufemia/src/components/textarea/Textarea.js @@ -8,7 +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 TextCounter from '../../fragments/text-counter/TextCounter' import { isTrue, makeUniqueId, @@ -77,7 +77,10 @@ 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, + characterCounter: PropTypes.oneOfType([ + PropTypes.oneOf(['down', 'up']), + PropTypes.bool, + ]), autoresize: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), autoresize_max_rows: PropTypes.oneOfType([ PropTypes.string, @@ -336,33 +339,6 @@ 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 ( - <> - {message} - 0 && textareaState === 'virgin'} - delay={2000} - > - {message} - - - ) - } render() { // use only the props from context, who are available here anyway const props = extendPropsWithContextInClassComponent( @@ -397,8 +373,9 @@ export default class Textarea extends React.PureComponent { class: _className, className, autoresize, + characterCounter, + maxLength, autoresize_max_rows, //eslint-disable-line - characterCounter, //eslint-disable-line id: _id, //eslint-disable-line children, //eslint-disable-line value: _value, //eslint-disable-line @@ -427,8 +404,9 @@ export default class Textarea extends React.PureComponent { role: 'textbox', value: hasValue ? value : '', id, - disabled: isTrue(disabled) || isTrue(skeleton), name: id, + maxLength, + disabled: isTrue(disabled) || isTrue(skeleton), 'aria-placeholder': placeholder, ...attributes, ...textareaAttributes, @@ -569,7 +547,16 @@ export default class Textarea extends React.PureComponent { )} - {this.getCounter()} + {characterCounter && maxLength > 0 && ( + + )} ) diff --git a/packages/dnb-eufemia/src/components/textarea/__tests__/Textarea.test.tsx b/packages/dnb-eufemia/src/components/textarea/__tests__/Textarea.test.tsx index a1fbdd0a525..46880335265 100644 --- a/packages/dnb-eufemia/src/components/textarea/__tests__/Textarea.test.tsx +++ b/packages/dnb-eufemia/src/components/textarea/__tests__/Textarea.test.tsx @@ -302,27 +302,27 @@ describe('Textarea component', () => {