From 4d781766aac460e2775d82aa93a2eb3a52383268 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 14 Sep 2023 09:03:24 +0200 Subject: [PATCH 01/17] feat: :sparkles: New `Textfield` component --- .../components/form/Textfield/Textfield.mdx | 10 +++ .../form/Textfield/Textfield.module.css | 27 +++++++ .../form/Textfield/Textfield.stories.tsx | 19 +++++ .../components/form/Textfield/Textfield.tsx | 73 +++++++++++++++++++ .../src/components/form/Textfield/index.ts | 1 + .../components/form/Textfield/useTextfield.ts | 51 +++++++++++++ 6 files changed, 181 insertions(+) create mode 100644 packages/react/src/components/form/Textfield/Textfield.mdx create mode 100644 packages/react/src/components/form/Textfield/Textfield.module.css create mode 100644 packages/react/src/components/form/Textfield/Textfield.stories.tsx create mode 100644 packages/react/src/components/form/Textfield/Textfield.tsx create mode 100644 packages/react/src/components/form/Textfield/index.ts create mode 100644 packages/react/src/components/form/Textfield/useTextfield.ts diff --git a/packages/react/src/components/form/Textfield/Textfield.mdx b/packages/react/src/components/form/Textfield/Textfield.mdx new file mode 100644 index 0000000000..8c80ac7304 --- /dev/null +++ b/packages/react/src/components/form/Textfield/Textfield.mdx @@ -0,0 +1,10 @@ +import { Meta, Canvas, Story, Controls, Primary } from '@storybook/blocks'; +import { Information } from '../../../../../../docs-components'; +import * as TextfieldStories from './Textfield.stories'; + + + +# Textfield + + + diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css new file mode 100644 index 0000000000..88b9bf8e14 --- /dev/null +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -0,0 +1,27 @@ +.textfield { + --fds-inner-focus-border-color: var(--fds-semantic-border-focus-boxshadow); + --fds-outer-focus-border-color: var(--fds-semantic-border-focus-outline); + --fds-focus-border-width: 3px; + + display: grid; + grid-auto-rows: 1fr; + grid-auto-flow: row; +} + +.input { +} + +.label { +} + +.description { +} + +.disabled { +} + +.readonly { +} + +.padlock { +} diff --git a/packages/react/src/components/form/Textfield/Textfield.stories.tsx b/packages/react/src/components/form/Textfield/Textfield.stories.tsx new file mode 100644 index 0000000000..70ed83c4de --- /dev/null +++ b/packages/react/src/components/form/Textfield/Textfield.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Textfield } from '.'; + +type Story = StoryObj; + +export default { + title: 'Felles/Textfield', + component: Textfield, +} as Meta; + +export const Preview: Story = { + args: { + label: 'Label', + description: 'Description', + disabled: false, + readOnly: false, + }, +}; diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx new file mode 100644 index 0000000000..c76de137db --- /dev/null +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -0,0 +1,73 @@ +import type { InputHTMLAttributes, ReactNode } from 'react'; +import React, { forwardRef } from 'react'; +import cn from 'classnames'; +import { PadlockLockedFillIcon } from '@navikt/aksel-icons'; + +import { omit } from '../../../utils'; +import { Label, Paragraph } from '../../Typography'; +import type { FormFieldProps } from '../useFormField'; + +import { useTextfield } from './useTextfield'; +import classes from './Textfield.module.css'; + +export type TextfieldProps = { + label?: ReactNode; + size?: 'small' | 'medium' | 'large'; +} & Omit & + Omit, 'size'>; + +export const Textfield = forwardRef( + (props, ref) => { + const { label, description, ...rest } = props; + const { + inputProps, + descriptionId, + size = 'medium', + readOnly, + } = useTextfield(props); + + return ( + + + + {description && ( + + {description} + + )} + + ); + }, +); diff --git a/packages/react/src/components/form/Textfield/index.ts b/packages/react/src/components/form/Textfield/index.ts new file mode 100644 index 0000000000..4ece73e0b9 --- /dev/null +++ b/packages/react/src/components/form/Textfield/index.ts @@ -0,0 +1 @@ +export * from './Textfield'; diff --git a/packages/react/src/components/form/Textfield/useTextfield.ts b/packages/react/src/components/form/Textfield/useTextfield.ts new file mode 100644 index 0000000000..d4f639b4e9 --- /dev/null +++ b/packages/react/src/components/form/Textfield/useTextfield.ts @@ -0,0 +1,51 @@ +import type { InputHTMLAttributes } from 'react'; +import { useContext } from 'react'; + +import type { FormField } from '../useFormField'; +import { useFormField } from '../useFormField'; +import { FieldsetContext } from '../Fieldset'; + +import type { TextfieldProps } from './Textfield'; + +type UseCheckbox = (props: TextfieldProps) => FormField & { + inputProps?: Pick< + InputHTMLAttributes, + | 'readOnly' + | 'type' + | 'name' + | 'required' + | 'defaultChecked' + | 'checked' + | 'onClick' + | 'onChange' + >; +}; +/** Handles props for `Switch` in context with `Checkbox.Group` (and `Fieldset`) */ +export const useTextfield: UseCheckbox = (props) => { + const fieldset = useContext(FieldsetContext); + const { inputProps, readOnly, ...rest } = useFormField(props, 'textfield'); + + return { + ...rest, + readOnly, + size: fieldset?.size ?? props.size, + inputProps: { + ...inputProps, + readOnly, + onClick: (e) => { + if (readOnly) { + e.preventDefault(); + return; + } + props?.onClick?.(e); + }, + onChange: (e) => { + if (readOnly) { + e.preventDefault(); + return; + } + props?.onChange?.(e); + }, + }, + }; +}; From bd1e441513d84e84e532d841180e4aff9de2f5d5 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 14 Sep 2023 14:21:16 +0200 Subject: [PATCH 02/17] sizes and some style --- .../form/Textfield/Textfield.module.css | 31 +++++++--- .../components/form/Textfield/Textfield.tsx | 56 +++++++++++-------- .../components/form/Textfield/useTextfield.ts | 13 +---- 3 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index 88b9bf8e14..dd68c86762 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -1,27 +1,42 @@ .textfield { - --fds-inner-focus-border-color: var(--fds-semantic-border-focus-boxshadow); - --fds-outer-focus-border-color: var(--fds-semantic-border-focus-outline); - --fds-focus-border-width: 3px; - display: grid; - grid-auto-rows: 1fr; - grid-auto-flow: row; + gap: var(--fds-spacing-2); } .input { + font-family: inherit; + border-radius: var(--fds-border_radius-medium); + min-height: 2.9em; + appearance: none; + width: 100%; + padding: 0 var(--fds-spacing-3); + border: solid 1px var(--fds-semantic-border-action-default); } .label { + min-width: min-content; + display: inline-flex; + flex-direction: row; + gap: var(--fds-spacing-1); + align-items: center; } .description { + color: var(--fds-semantic-text-neutral-subtle); } -.disabled { +.disabled > .input, +.disabled > .label, +.disabled > .description { + color: var(--fds-semantic-border-neutral-subtle); } -.readonly { +.readonly > .input, +.readonly > .label { + cursor: default; } .padlock { + height: 1.2rem; + width: 1.2rem; } diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx index c76de137db..45ec6f34e1 100644 --- a/packages/react/src/components/form/Textfield/Textfield.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -4,15 +4,16 @@ import cn from 'classnames'; import { PadlockLockedFillIcon } from '@navikt/aksel-icons'; import { omit } from '../../../utils'; -import { Label, Paragraph } from '../../Typography'; +import { Label, Paragraph, ErrorMessage } from '../../Typography'; import type { FormFieldProps } from '../useFormField'; import { useTextfield } from './useTextfield'; import classes from './Textfield.module.css'; +import utilityClasses from './../../../utils/utility.module.css'; export type TextfieldProps = { label?: ReactNode; - size?: 'small' | 'medium' | 'large'; + size?: 'xsmall' | 'small' | 'medium' | 'large'; } & Omit & Omit, 'size'>; @@ -22,6 +23,8 @@ export const Textfield = forwardRef( const { inputProps, descriptionId, + hasError, + errorId, size = 'medium', readOnly, } = useTextfield(props); @@ -37,26 +40,22 @@ export const Textfield = forwardRef( rest.className, )} > - - + {label && ( + + )} {description && ( ( {description} )} + +
+ {hasError && {props.error}} +
); }, diff --git a/packages/react/src/components/form/Textfield/useTextfield.ts b/packages/react/src/components/form/Textfield/useTextfield.ts index d4f639b4e9..d92d6628fa 100644 --- a/packages/react/src/components/form/Textfield/useTextfield.ts +++ b/packages/react/src/components/form/Textfield/useTextfield.ts @@ -7,21 +7,14 @@ import { FieldsetContext } from '../Fieldset'; import type { TextfieldProps } from './Textfield'; -type UseCheckbox = (props: TextfieldProps) => FormField & { +type UseTextfield = (props: TextfieldProps) => FormField & { inputProps?: Pick< InputHTMLAttributes, - | 'readOnly' - | 'type' - | 'name' - | 'required' - | 'defaultChecked' - | 'checked' - | 'onClick' - | 'onChange' + 'readOnly' | 'type' | 'name' | 'required' | 'onClick' | 'onChange' >; }; /** Handles props for `Switch` in context with `Checkbox.Group` (and `Fieldset`) */ -export const useTextfield: UseCheckbox = (props) => { +export const useTextfield: UseTextfield = (props) => { const fieldset = useContext(FieldsetContext); const { inputProps, readOnly, ...rest } = useFormField(props, 'textfield'); From 05572f18ff7b05cca28f607d062ba102472a8f5a Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 18 Sep 2023 08:09:54 +0200 Subject: [PATCH 03/17] tweaked style --- .../react/src/components/form/Textfield/Textfield.module.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index dd68c86762..d30d534e5d 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -4,13 +4,14 @@ } .input { + font: inherit; font-family: inherit; border-radius: var(--fds-border_radius-medium); - min-height: 2.9em; + min-height: 2.5em; appearance: none; width: 100%; padding: 0 var(--fds-spacing-3); - border: solid 1px var(--fds-semantic-border-action-default); + border: solid 1px var(--fds-semantic-border-action-dark); } .label { From c79e1629d5f4c4b671b119a3105272318935edd6 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 18 Sep 2023 08:56:06 +0200 Subject: [PATCH 04/17] wip fixes --- .../form/Textfield/Textfield.module.css | 30 +++++++++++++++++-- .../components/form/Textfield/Textfield.tsx | 23 +++++++++----- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index d30d534e5d..eb7f55bd0c 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -6,14 +6,28 @@ .input { font: inherit; font-family: inherit; - border-radius: var(--fds-border_radius-medium); min-height: 2.5em; appearance: none; - width: 100%; padding: 0 var(--fds-spacing-3); +} + +.field { + display: flex; + align-items: stretch; + border-radius: var(--fds-border_radius-medium); border: solid 1px var(--fds-semantic-border-action-dark); } +.field > *:first-child { + border-top-left-radius: var(--fds-border_radius-medium); + border-bottom-left-radius: var(--fds-border_radius-medium); +} + +.field > *:last-child { + border-top-right-radius: var(--fds-border_radius-medium); + border-bottom-right-radius: var(--fds-border_radius-medium); +} + .label { min-width: min-content; display: inline-flex; @@ -41,3 +55,15 @@ height: 1.2rem; width: 1.2rem; } + +.adornment { + color: var(--fds-semantic-border-neutral-default); + background: var(--fds-semantic-surface-neutral-subtle); + padding: 5px var(--fds-spacing-3); + display: flex; + align-items: center; +} + +.prefix { + border-top-left-radius: var(--fds-border_radius-medium); +} diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx index 45ec6f34e1..48c330a931 100644 --- a/packages/react/src/components/form/Textfield/Textfield.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -8,18 +8,20 @@ import { Label, Paragraph, ErrorMessage } from '../../Typography'; import type { FormFieldProps } from '../useFormField'; import { useTextfield } from './useTextfield'; -import classes from './Textfield.module.css'; +import classes, { prefix } from './Textfield.module.css'; import utilityClasses from './../../../utils/utility.module.css'; export type TextfieldProps = { label?: ReactNode; size?: 'xsmall' | 'small' | 'medium' | 'large'; + prefix?: string; + sufix?: string; } & Omit & Omit, 'size'>; export const Textfield = forwardRef( (props, ref) => { - const { label, description, ...rest } = props; + const { label, description, sufix, prefix, ...rest } = props; const { inputProps, descriptionId, @@ -66,12 +68,17 @@ export const Textfield = forwardRef( {description} )} - +
+ {prefix && {prefix}} + + {sufix && {sufix}} +
+
Date: Mon, 18 Sep 2023 12:04:17 +0200 Subject: [PATCH 05/17] wip --- .../form/Textfield/Textfield.module.css | 88 +++++++++++++------ .../form/Textfield/Textfield.stories.tsx | 1 + .../components/form/Textfield/Textfield.tsx | 35 ++++++-- 3 files changed, 94 insertions(+), 30 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index eb7f55bd0c..283d69f7fd 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -1,31 +1,33 @@ .textfield { + --fdsc-textfield-border: var(--fds-semantic-border-action-dark); + display: grid; gap: var(--fds-spacing-2); } +.adornment { + color: var(--fds-semantic-border-neutral-default); + background: var(--fds-semantic-surface-neutral-subtle); + padding: var(--fds-spacing-3); + border-radius: var(--fds-border_radius-medium); + border: solid 1px var(--fdsc-textfield-border); + box-sizing: border-box; + display: inline-block; +} + .input { font: inherit; font-family: inherit; + display: block; + position: relative; + flex: 0 1 auto; min-height: 2.5em; appearance: none; padding: 0 var(--fds-spacing-3); -} - -.field { - display: flex; - align-items: stretch; + width: 100%; + border: solid 1px var(--fdsc-textfield-border); border-radius: var(--fds-border_radius-medium); - border: solid 1px var(--fds-semantic-border-action-dark); -} - -.field > *:first-child { - border-top-left-radius: var(--fds-border_radius-medium); - border-bottom-left-radius: var(--fds-border_radius-medium); -} - -.field > *:last-child { - border-top-right-radius: var(--fds-border_radius-medium); - border-bottom-right-radius: var(--fds-border_radius-medium); + box-sizing: border-box; } .label { @@ -51,19 +53,55 @@ cursor: default; } -.padlock { - height: 1.2rem; - width: 1.2rem; +.error > .input:not(:focus-visible) { + box-shadow: inset 0 0 0 2px var(--fds-semantic-border-danger-default); } -.adornment { - color: var(--fds-semantic-border-neutral-default); - background: var(--fds-semantic-surface-neutral-subtle); - padding: 5px var(--fds-spacing-3); - display: flex; - align-items: center; +@media (hover: hover) and (pointer: fine) { + .input:not(:focus-visible):hover { + box-shadow: inset 0 0 0 2px var(--fds-semantic-border-action-hover); + } +} + +.inputSufix { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.inputPrefix { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } .prefix { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.sufix { + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.field { + display: flex; + align-items: stretch; + border-radius: var(--fds-border_radius-medium); +} + +.field > *:first-child { border-top-left-radius: var(--fds-border_radius-medium); + border-bottom-left-radius: var(--fds-border_radius-medium); +} + +.field > *:last-child { + border-top-right-radius: var(--fds-border_radius-medium); + border-bottom-right-radius: var(--fds-border_radius-medium); +} + +.padlock { + height: 1.2rem; + width: 1.2rem; } diff --git a/packages/react/src/components/form/Textfield/Textfield.stories.tsx b/packages/react/src/components/form/Textfield/Textfield.stories.tsx index 70ed83c4de..6ee2d5ab59 100644 --- a/packages/react/src/components/form/Textfield/Textfield.stories.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.stories.tsx @@ -15,5 +15,6 @@ export const Preview: Story = { description: 'Description', disabled: false, readOnly: false, + error: '', }, }; diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx index 48c330a931..38232d5718 100644 --- a/packages/react/src/components/form/Textfield/Textfield.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -8,7 +8,7 @@ import { Label, Paragraph, ErrorMessage } from '../../Typography'; import type { FormFieldProps } from '../useFormField'; import { useTextfield } from './useTextfield'; -import classes, { prefix } from './Textfield.module.css'; +import classes from './Textfield.module.css'; import utilityClasses from './../../../utils/utility.module.css'; export type TextfieldProps = { @@ -68,15 +68,40 @@ export const Textfield = forwardRef( {description} )} -
- {prefix && {prefix}} +
+ {prefix && ( + + )} - {sufix && {sufix}} + {sufix && ( + + )}
Date: Mon, 18 Sep 2023 12:41:24 +0200 Subject: [PATCH 06/17] wip --- .../react/src/components/form/Textfield/Textfield.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index 283d69f7fd..7fb977507d 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -10,7 +10,7 @@ background: var(--fds-semantic-surface-neutral-subtle); padding: var(--fds-spacing-3); border-radius: var(--fds-border_radius-medium); - border: solid 1px var(--fdsc-textfield-border); + border: solid 1px var(--fds-semantic-border-neutral-default); box-sizing: border-box; display: inline-block; } @@ -25,7 +25,7 @@ appearance: none; padding: 0 var(--fds-spacing-3); width: 100%; - border: solid 1px var(--fdsc-textfield-border); + border: solid 1px var(--fds-semantic-border-action-dark); border-radius: var(--fds-border_radius-medium); box-sizing: border-box; } From 1476d40f3c2b1400537f922e4aa40488d887bd61 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 19 Sep 2023 08:20:29 +0200 Subject: [PATCH 07/17] wip styles --- .../form/Textfield/Textfield.module.css | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index 7fb977507d..7501ebcf4c 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -1,6 +1,4 @@ .textfield { - --fdsc-textfield-border: var(--fds-semantic-border-action-dark); - display: grid; gap: var(--fds-spacing-2); } @@ -17,17 +15,14 @@ .input { font: inherit; - font-family: inherit; - display: block; - position: relative; flex: 0 1 auto; - min-height: 2.5em; + min-height: 2.25em; + width: 100%; + height: 100%; appearance: none; padding: 0 var(--fds-spacing-3); - width: 100%; border: solid 1px var(--fds-semantic-border-action-dark); border-radius: var(--fds-border_radius-medium); - box-sizing: border-box; } .label { @@ -42,24 +37,28 @@ color: var(--fds-semantic-text-neutral-subtle); } -.disabled > .input, -.disabled > .label, -.disabled > .description { - color: var(--fds-semantic-border-neutral-subtle); +.disabled { + opacity: 0.3; } -.readonly > .input, -.readonly > .label { - cursor: default; +.disabled .input { + cursor: not-allowed; +} + +.readonly .input { + background: var(--fds-semantic-surface-neutral-subtle); + border-color: var(--fds-semantic-border-neutral-default); } .error > .input:not(:focus-visible) { - box-shadow: inset 0 0 0 2px var(--fds-semantic-border-danger-default); + border-color: var(--fds-semantic-border-danger-default); + box-shadow: inset 0 0 0 1px var(--fds-semantic-border-danger-default); } @media (hover: hover) and (pointer: fine) { - .input:not(:focus-visible):hover { - box-shadow: inset 0 0 0 2px var(--fds-semantic-border-action-hover); + .input:not(:focus-visible, :disabled):hover { + border-color: var(--fds-semantic-border-action-hover); + box-shadow: inset 0 0 0 1px var(--fds-semantic-border-action-hover); } } From 5620c6e2040a47edc9ad46bed4bbc6d0531806c0 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 19 Sep 2023 08:39:48 +0200 Subject: [PATCH 08/17] wip --- .../form/Textfield/Textfield.module.css | 5 +++-- .../src/components/form/Textfield/Textfield.tsx | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/form/Textfield/Textfield.module.css b/packages/react/src/components/form/Textfield/Textfield.module.css index 7501ebcf4c..7efacfa2f3 100644 --- a/packages/react/src/components/form/Textfield/Textfield.module.css +++ b/packages/react/src/components/form/Textfield/Textfield.module.css @@ -15,10 +15,11 @@ .input { font: inherit; + position: relative; + box-sizing: border-box; flex: 0 1 auto; - min-height: 2.25em; + min-height: 2.5em; width: 100%; - height: 100%; appearance: none; padding: 0 var(--fds-spacing-3); border: solid 1px var(--fds-semantic-border-action-dark); diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx index 38232d5718..3607986dad 100644 --- a/packages/react/src/components/form/Textfield/Textfield.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -16,12 +16,25 @@ export type TextfieldProps = { size?: 'xsmall' | 'small' | 'medium' | 'large'; prefix?: string; sufix?: string; + type?: + | 'text' + | 'password' + | 'date' + | 'datetime-local' + | 'email' + | 'month' + | 'number' + | 'search' + | 'tel' + | 'time' + | 'url' + | 'week'; } & Omit & Omit, 'size'>; export const Textfield = forwardRef( (props, ref) => { - const { label, description, sufix, prefix, ...rest } = props; + const { label, description, sufix, prefix, style, ...rest } = props; const { inputProps, descriptionId, @@ -35,6 +48,7 @@ export const Textfield = forwardRef( Date: Tue, 19 Sep 2023 16:20:02 +0200 Subject: [PATCH 09/17] wip on charactercount --- .../src/components/form/CharacterCounter.tsx | 65 +++++++++++++++++++ .../components/form/Textfield/Textfield.mdx | 2 + .../form/Textfield/Textfield.stories.tsx | 21 +++++- .../components/form/Textfield/Textfield.tsx | 43 +++++++++++- 4 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 packages/react/src/components/form/CharacterCounter.tsx diff --git a/packages/react/src/components/form/CharacterCounter.tsx b/packages/react/src/components/form/CharacterCounter.tsx new file mode 100644 index 0000000000..b409bf0bf7 --- /dev/null +++ b/packages/react/src/components/form/CharacterCounter.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import utilityClasses from '../../utils/utility.module.css'; +import { ErrorMessage, Paragraph } from '../Typography'; + +export type CharacterLimitProps = Omit< + CharacterCounterProps, + 'id' | 'value' | 'size' +>; + +type CharacterCounterProps = { + /** The message indicating the remaining character limit. */ + label: (count: number) => string; + /** The description of the maximum character limit for screen readers. */ + srLabel: string; + /** The maximum allowed character count. */ + maxCount: number; + /** The current value. */ + value: string; + /** The ID of the element that describes the maximum character limit for accessibility purposes. */ + id: string; + /** Text size */ + size?: 'xsmall' | 'small' | 'medium' | 'large'; +}; + +// const defaultLabel: CharacterCounterProps['label'] = (count) => +// count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`; + +// const defaultSrLabel = (maxCount: number) => +// `Tekstfelt med plass til ${maxCount} tegn.`; + +export const CharacterCounter = ({ + label, + srLabel, + maxCount, + value, + id, + size, +}: CharacterCounterProps): JSX.Element => { + const currentCount = maxCount - value.length; + const hasExceededLimit = value.length > maxCount; + + return ( + <> + + {srLabel} + + + {label(currentCount)} + + + ); +}; diff --git a/packages/react/src/components/form/Textfield/Textfield.mdx b/packages/react/src/components/form/Textfield/Textfield.mdx index 8c80ac7304..7775207e3b 100644 --- a/packages/react/src/components/form/Textfield/Textfield.mdx +++ b/packages/react/src/components/form/Textfield/Textfield.mdx @@ -8,3 +8,5 @@ import * as TextfieldStories from './Textfield.stories'; + + diff --git a/packages/react/src/components/form/Textfield/Textfield.stories.tsx b/packages/react/src/components/form/Textfield/Textfield.stories.tsx index 6ee2d5ab59..41c220a41d 100644 --- a/packages/react/src/components/form/Textfield/Textfield.stories.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.stories.tsx @@ -15,6 +15,25 @@ export const Preview: Story = { description: 'Description', disabled: false, readOnly: false, - error: '', + error: 'Annen feilmelding her', + characterLimit: { + label: (count) => + count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`, + maxCount: 20, + srLabel: `Tekstfelt med plass til ${20} tegn.`, + }, + }, +}; + +export const WithCharacterCounter: Story = { + args: { + label: 'Label', + error: 'Annen feilmelding her', + characterLimit: { + label: (count) => + count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`, + maxCount: 20, + srLabel: `Tekstfelt med plass til ${20} tegn.`, + }, }, }; diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx index 3607986dad..a8a1009270 100644 --- a/packages/react/src/components/form/Textfield/Textfield.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -1,11 +1,13 @@ import type { InputHTMLAttributes, ReactNode } from 'react'; -import React, { forwardRef } from 'react'; +import React, { useState, useId, forwardRef } from 'react'; import cn from 'classnames'; import { PadlockLockedFillIcon } from '@navikt/aksel-icons'; import { omit } from '../../../utils'; import { Label, Paragraph, ErrorMessage } from '../../Typography'; import type { FormFieldProps } from '../useFormField'; +import type { CharacterLimitProps } from '../CharacterCounter'; +import { CharacterCounter } from '../CharacterCounter'; import { useTextfield } from './useTextfield'; import classes from './Textfield.module.css'; @@ -29,12 +31,26 @@ export type TextfieldProps = { | 'time' | 'url' | 'week'; + /** + * The characterLimit function calculates remaining characters. + * Provide a `label` function that takes count as parameter and returns a message. + * Use `srLabel` to describe `maxCount` for screen readers. + */ + characterLimit?: CharacterLimitProps; } & Omit & Omit, 'size'>; export const Textfield = forwardRef( (props, ref) => { - const { label, description, sufix, prefix, style, ...rest } = props; + const { + label, + description, + sufix, + prefix, + style, + characterLimit, + ...rest + } = props; const { inputProps, descriptionId, @@ -44,6 +60,15 @@ export const Textfield = forwardRef( readOnly, } = useTextfield(props); + const [inputValue, setInputValue] = useState(props.defaultValue); + const characterLimitId = `charactercount-${useId()}`; + const hasCharacterLimit = characterLimit != null; + + const describedBy = cn( + inputProps['aria-describedby'], + hasCharacterLimit && characterLimitId, + ); + return ( ( sufix && classes.inputSufix, )} ref={ref} + aria-describedby={describedBy} + onChange={(e) => { + inputProps?.onChange?.(e); + setInputValue(e.target.value); + }} /> {sufix && ( ( )}
- + {hasCharacterLimit && ( + + )}
Date: Wed, 20 Sep 2023 09:56:52 +0200 Subject: [PATCH 10/17] more stuff --- .../src/components/form/CharacterCounter.tsx | 19 ++++----- .../components/form/Textfield/Textfield.mdx | 1 + .../form/Textfield/Textfield.stories.tsx | 38 +++++++++++------- .../components/form/Textfield/Textfield.tsx | 39 ++++++++++++++++--- 4 files changed, 69 insertions(+), 28 deletions(-) diff --git a/packages/react/src/components/form/CharacterCounter.tsx b/packages/react/src/components/form/CharacterCounter.tsx index b409bf0bf7..36ecd460db 100644 --- a/packages/react/src/components/form/CharacterCounter.tsx +++ b/packages/react/src/components/form/CharacterCounter.tsx @@ -1,7 +1,7 @@ import React from 'react'; import utilityClasses from '../../utils/utility.module.css'; -import { ErrorMessage, Paragraph } from '../Typography'; +import { ErrorMessage } from '../Typography'; export type CharacterLimitProps = Omit< CharacterCounterProps, @@ -10,9 +10,9 @@ export type CharacterLimitProps = Omit< type CharacterCounterProps = { /** The message indicating the remaining character limit. */ - label: (count: number) => string; + label?: (count: number) => string; /** The description of the maximum character limit for screen readers. */ - srLabel: string; + srLabel?: string; /** The maximum allowed character count. */ maxCount: number; /** The current value. */ @@ -23,15 +23,15 @@ type CharacterCounterProps = { size?: 'xsmall' | 'small' | 'medium' | 'large'; }; -// const defaultLabel: CharacterCounterProps['label'] = (count) => -// count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`; +const defaultLabel: CharacterCounterProps['label'] = (count) => + count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`; -// const defaultSrLabel = (maxCount: number) => -// `Tekstfelt med plass til ${maxCount} tegn.`; +const defaultSrLabel = (maxCount: number) => + `Tekstfelt med plass til ${maxCount} tegn.`; export const CharacterCounter = ({ - label, - srLabel, + label = defaultLabel, + srLabel: propsSrLabel, maxCount, value, id, @@ -39,6 +39,7 @@ export const CharacterCounter = ({ }: CharacterCounterProps): JSX.Element => { const currentCount = maxCount - value.length; const hasExceededLimit = value.length > maxCount; + const srLabel = propsSrLabel ? propsSrLabel : defaultSrLabel(maxCount); return ( <> diff --git a/packages/react/src/components/form/Textfield/Textfield.mdx b/packages/react/src/components/form/Textfield/Textfield.mdx index 7775207e3b..b4718e6517 100644 --- a/packages/react/src/components/form/Textfield/Textfield.mdx +++ b/packages/react/src/components/form/Textfield/Textfield.mdx @@ -10,3 +10,4 @@ import * as TextfieldStories from './Textfield.stories'; + diff --git a/packages/react/src/components/form/Textfield/Textfield.stories.tsx b/packages/react/src/components/form/Textfield/Textfield.stories.tsx index 41c220a41d..77d926dc9b 100644 --- a/packages/react/src/components/form/Textfield/Textfield.stories.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.stories.tsx @@ -1,4 +1,7 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj, StoryFn } from '@storybook/react'; +import React, { useState } from 'react'; + +import { Button, Paragraph } from '../..'; import { Textfield } from '.'; @@ -12,28 +15,37 @@ export default { export const Preview: Story = { args: { label: 'Label', - description: 'Description', disabled: false, readOnly: false, - error: 'Annen feilmelding her', - characterLimit: { - label: (count) => - count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`, - maxCount: 20, - srLabel: `Tekstfelt med plass til ${20} tegn.`, - }, + description: '', }, }; export const WithCharacterCounter: Story = { args: { label: 'Label', - error: 'Annen feilmelding her', characterLimit: { label: (count) => - count > -1 ? `${count} tegn igjen` : `${Math.abs(count)} tegn for mye.`, - maxCount: 20, - srLabel: `Tekstfelt med plass til ${20} tegn.`, + count > -1 + ? `${count} character Left` + : `${Math.abs(count)} characters to many.`, + maxCount: 5, + srLabel: `Field with room for ${5} characters.`, }, }, }; + +export const Controlled: StoryFn = () => { + const [value, setValue] = useState(); + return ( + <> + Du har skrevet inn: {value} + setValue(e.target.value)} + /> + + + ); +}; diff --git a/packages/react/src/components/form/Textfield/Textfield.tsx b/packages/react/src/components/form/Textfield/Textfield.tsx index a8a1009270..84cda9d1d8 100644 --- a/packages/react/src/components/form/Textfield/Textfield.tsx +++ b/packages/react/src/components/form/Textfield/Textfield.tsx @@ -14,32 +14,51 @@ import classes from './Textfield.module.css'; import utilityClasses from './../../../utils/utility.module.css'; export type TextfieldProps = { + /** Label */ label?: ReactNode; + /** Visually hides `label` and `description` (still available for screen readers) */ + hideLabel?: boolean; + /** Changes field size and paddings */ size?: 'xsmall' | 'small' | 'medium' | 'large'; + /** Prefix for field. */ prefix?: string; + /** Sufix for field. */ sufix?: string; + /** Supported `input` types */ type?: - | 'text' - | 'password' | 'date' | 'datetime-local' | 'email' + | 'file' | 'month' | 'number' + | 'password' | 'search' | 'tel' + | 'text' | 'time' | 'url' | 'week'; /** - * The characterLimit function calculates remaining characters. + * The characterLimit function calculates remaining characters based on `maxCount` + * * Provide a `label` function that takes count as parameter and returns a message. + * * Use `srLabel` to describe `maxCount` for screen readers. + * + * Defaults to Norwegian if no labels are provided. */ characterLimit?: CharacterLimitProps; } & Omit & Omit, 'size'>; +/** Text input field + * + * @example + * ```tsx + * + * ``` + */ export const Textfield = forwardRef( (props, ref) => { const { @@ -49,8 +68,10 @@ export const Textfield = forwardRef( prefix, style, characterLimit, + hideLabel, ...rest } = props; + const { inputProps, descriptionId, @@ -61,7 +82,7 @@ export const Textfield = forwardRef( } = useTextfield(props); const [inputValue, setInputValue] = useState(props.defaultValue); - const characterLimitId = `charactercount-${useId()}`; + const characterLimitId = `textfield-charactercount-${useId()}`; const hasCharacterLimit = characterLimit != null; const describedBy = cn( @@ -83,7 +104,10 @@ export const Textfield = forwardRef( > {label && (