-
Notifications
You must be signed in to change notification settings - Fork 457
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: textarea base component (#2880)
* feat: init component, prepare react version * feat: classes, types * feat: react blocks * feat: blocks examples and autoresize implementation * feat: vue part, tests and docs * feat: vue blocks * feat: vue and react fixes, docs, add thumbnail * chore: changset note * fix: tests * fix: styles, examples * fix: tests, add typography classes * fix: after CR * fix: focus visible
- Loading branch information
1 parent
06bb45b
commit aa9c2f3
Showing
30 changed files
with
978 additions
and
0 deletions.
There are no files selected for viewing
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,7 @@ | ||
--- | ||
'@storefront-ui/react': minor | ||
'@storefront-ui/vue': minor | ||
'@storefront-ui/shared': minor | ||
--- | ||
|
||
Added textarea base component |
Binary file added
BIN
+29 KB
apps/docs/components/.vuepress/public/thumbnails/components/Textarea.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 @@ | ||
--- | ||
layout: AtomLayout | ||
hideBreadcrumbs: true | ||
--- | ||
|
||
# Textarea component | ||
|
||
::: slot usage | ||
|
||
The Textarea is a multi-line text input control allows users to enter any combination of letters, numbers, or symbols. It adds default styles to the native `<textarea>` HTML tag, providing a consistent and visually appealing appearance out of the box. The component supports autoresizing based on the content entered by the user and provides the option to display a character count. | ||
|
||
### Textarea in disabled state | ||
|
||
`SfTextarea` comes with out-of-the-box styles for a disabled Textarea. | ||
|
||
<Showcase showcase-name="Textarea/TextareaDisabled"> | ||
|
||
<!-- vue --> | ||
<<<../../preview/nuxt/pages/showcases/Textarea/TextareaDisabled.vue | ||
<!-- end vue --> | ||
<!-- react --> | ||
<<<../../preview/next/pages/showcases/Textarea/TextareaDisabled.tsx#source | ||
<!-- end react --> | ||
</Showcase> | ||
|
||
### Readonly Textarea | ||
|
||
`SfTextarea` comes with out-of-the-box styles for a readonly Textarea. | ||
|
||
<Showcase showcase-name="Textarea/TextareaReadonly"> | ||
|
||
<!-- vue --> | ||
<<<../../preview/nuxt/pages/showcases/Textarea/TextareaReadonly.vue | ||
<!-- end vue --> | ||
<!-- react --> | ||
<<<../../preview/next/pages/showcases/Textarea/TextareaReadonly.tsx#source | ||
<!-- end react --> | ||
</Showcase> | ||
|
||
### Invalid State | ||
|
||
If you pass the `invalid` prop, the Textarea will be styled to indicate an invalid state. | ||
|
||
<Showcase showcase-name="Textarea/TextareaInvalid" style="min-height: 200px;"> | ||
|
||
<!-- vue --> | ||
<<<../../preview/nuxt/pages/showcases/Textarea/TextareaInvalid.vue | ||
<!-- end vue --> | ||
<!-- react --> | ||
<<<../../preview/next/pages/showcases/Textarea/TextareaInvalid.tsx#source | ||
<!-- end react --> | ||
</Showcase> | ||
|
||
### Textarea with characters counter | ||
|
||
The Textarea component provides the option to display a character count, allowing users to track the number of characters they have entered. This feature can be helpful when there are character limits or restrictions for the input. | ||
|
||
<Showcase showcase-name="Textarea/TextareaCharacters"> | ||
|
||
<!-- vue --> | ||
<<<../../preview/nuxt/pages/showcases/Textarea/TextareaCharacters.vue | ||
<!-- end vue --> | ||
<!-- react --> | ||
<<<../../preview/next/pages/showcases/Textarea/TextareaCharacters.tsx#source | ||
<!-- end react --> | ||
</Showcase> | ||
|
||
### Textarea with autoresize | ||
|
||
The Textarea component supports autoresizing based on the content entered by the user. As the user types or deletes text, the height of the textarea adjusts automatically to fit the content, eliminating the need for scrollbars. In the example below we use [`@frsource/autoresize-textarea`](https://www.frsource.org/autoresize-textarea/) library to provide this feature. | ||
|
||
<Showcase showcase-name="Textarea/TextareaAutoresize"> | ||
|
||
<!-- vue --> | ||
<<<../../preview/nuxt/pages/showcases/Textarea/TextareaAutoresize.vue | ||
<!-- end vue --> | ||
<!-- react --> | ||
<<<../../preview/next/pages/showcases/Textarea/TextareaAutoresize.tsx#source | ||
<!-- end react --> | ||
</Showcase> | ||
|
||
|
||
## Accessibility notes | ||
|
||
Textarea is multi-line input, so Return or Enter key inserts a line break. | ||
|
||
## Playground | ||
|
||
<Generate style="height: 600px;"/> | ||
|
||
::: | ||
|
||
::: slot api | ||
|
||
## Props | ||
|
||
| Prop name | Type | Default value | Possible values | | ||
| ------------ | -------- | ------------- | -------------------------------------- | | ||
| `size` | `SfInputSize` | `'base'` | `'sm'`, `'base'`, `'lg'` | | ||
| `invalid` | `boolean` | `false` | | ||
<!-- react --> | ||
| `className` | `string` | | | ||
<!-- end react --> | ||
|
||
<!-- vue --> | ||
|
||
## Events | ||
|
||
| Event name | Trigger | | ||
| ----------------- | ----------------------------- | | ||
| `update:modelValue` | triggers v-model update event | | ||
|
||
<!-- end vue --> | ||
|
||
::: | ||
|
||
::: slot source | ||
<SourceCode> | ||
|
||
<!-- vue --> | ||
<<<../../../packages/sfui/frameworks/vue/components/SfTextarea/SfTextarea.vue | ||
<!-- end vue --> | ||
<!-- react --> | ||
<<<../../../packages/sfui/frameworks/react/components/SfTextarea/SfTextarea.tsx | ||
<!-- end react --> | ||
</SourceCode> | ||
::: |
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
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,152 @@ | ||
import { SfTextarea, SfTextareaSize } from '@storefront-ui/react'; | ||
import classNames from 'classnames'; | ||
import type { ChangeEvent } from 'react'; | ||
import { prepareControls } from '../../components/utils/Controls'; | ||
import ComponentExample from '../../components/utils/ComponentExample'; | ||
import { ExamplePageLayout } from '../examples'; | ||
|
||
function Example() { | ||
const { state, controls } = prepareControls( | ||
[ | ||
{ | ||
type: 'select', | ||
modelName: 'size', | ||
propDefaultValue: 'SfInputSize.base', | ||
propType: 'SfInputSize', | ||
options: Object.keys(SfTextareaSize), | ||
}, | ||
{ | ||
type: 'text', | ||
propType: 'string', | ||
modelName: 'label', | ||
}, | ||
{ | ||
type: 'text', | ||
propType: 'string', | ||
modelName: 'placeholder', | ||
}, | ||
{ | ||
type: 'text', | ||
propType: 'string', | ||
modelName: 'helpText', | ||
}, | ||
{ | ||
type: 'text', | ||
propType: 'string', | ||
modelName: 'requiredText', | ||
}, | ||
{ | ||
type: 'text', | ||
propType: 'string', | ||
modelName: 'errorText', | ||
}, | ||
{ | ||
type: 'text', | ||
propType: 'number', | ||
modelName: 'characterLimit', | ||
}, | ||
{ | ||
type: 'boolean', | ||
propType: 'boolean', | ||
modelName: 'disabled', | ||
}, | ||
{ | ||
type: 'boolean', | ||
propType: 'boolean', | ||
modelName: 'required', | ||
}, | ||
{ | ||
type: 'boolean', | ||
propType: 'boolean', | ||
modelName: 'invalid', | ||
}, | ||
{ | ||
type: 'boolean', | ||
propType: 'boolean', | ||
modelName: 'readonly', | ||
}, | ||
], | ||
{ | ||
size: SfTextareaSize.base, | ||
disabled: false, | ||
required: false, | ||
invalid: false, | ||
readonly: undefined, | ||
placeholder: 'Write something about yourself', | ||
helpText: 'Do not include personal or financial information.', | ||
requiredText: 'Required text', | ||
errorText: 'Error message', | ||
label: 'Description', | ||
characterLimit: 12, | ||
value: '', | ||
}, | ||
); | ||
|
||
function onChange(event: ChangeEvent<HTMLTextAreaElement>) { | ||
state.set({ value: event.target.value }); | ||
} | ||
const isAboveLimit = state.get.characterLimit ? state.get.value.length > state.get.characterLimit : false; | ||
const charsCount = state.get.characterLimit ? state.get.characterLimit - state.get.value.length : null; | ||
|
||
const getCharacterLimitClass = () => (isAboveLimit ? 'text-negative-700 font-medium' : 'text-neutral-500'); | ||
|
||
return ( | ||
<ComponentExample controls={{ state, controls }}> | ||
<label> | ||
<span | ||
className={classNames('typography-text-sm font-medium', { | ||
'cursor-not-allowed text-disabled-500': state.get.disabled, | ||
})} | ||
> | ||
{state.get.label} | ||
</span> | ||
<SfTextarea | ||
name={state.get.label} | ||
size={state.get.size} | ||
value={state.get.value} | ||
invalid={state.get.invalid} | ||
placeholder={state.get.placeholder} | ||
disabled={state.get.disabled} | ||
readOnly={state.get.readonly} | ||
onChange={onChange} | ||
className="w-full" | ||
/> | ||
</label> | ||
<div className="flex justify-between"> | ||
<div> | ||
{state.get.invalid && !state.get.disabled && ( | ||
<p className="typography-text-sm text-negative-700 font-medium mt-0.5">{state.get.errorText}</p> | ||
)} | ||
{state.get.helpText && ( | ||
<p | ||
className={classNames( | ||
'typography-text-xs mt-0.5', | ||
state.get.disabled ? 'text-disabled-500' : 'text-neutral-500', | ||
)} | ||
> | ||
{state.get.helpText} | ||
</p> | ||
)} | ||
{state.get.requiredText && state.get.required ? ( | ||
<p className="mt-1 typography-text-sm font-normal text-neutral-500 before:content-['*']"> | ||
{state.get.requiredText} | ||
</p> | ||
) : null} | ||
</div> | ||
{state.get.characterLimit && !state.get.readonly ? ( | ||
<p | ||
className={classNames( | ||
'typography-text-xs mt-0.5', | ||
state.get.disabled ? 'text-disabled-500' : getCharacterLimitClass(), | ||
)} | ||
> | ||
{charsCount} | ||
</p> | ||
) : null} | ||
</div> | ||
</ComponentExample> | ||
); | ||
} | ||
|
||
Example.getLayout = ExamplePageLayout; | ||
export default Example; |
32 changes: 32 additions & 0 deletions
32
apps/preview/next/pages/showcases/Textarea/TextareaAutoresize.tsx
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,32 @@ | ||
/* eslint-disable jsx-a11y/label-has-associated-control */ | ||
|
||
import { ShowcasePageLayout } from '../../showcases'; | ||
// #region source | ||
import { SfTextarea } from '@storefront-ui/react'; | ||
import { attach } from '@frsource/autoresize-textarea'; | ||
import { useEffect, useRef } from 'react'; | ||
|
||
export default function TextareaAutoresize() { | ||
const textareaRef = useRef<HTMLTextAreaElement>(null); | ||
useEffect(() => { | ||
if (textareaRef.current) { | ||
attach(textareaRef.current); | ||
} | ||
}, []); | ||
return ( | ||
<> | ||
<label> | ||
<span className="typography-text-sm font-medium">Description</span> | ||
<SfTextarea ref={textareaRef} className="w-full h-max-[500px]" size="sm" aria-label="Label size sm" /> | ||
</label> | ||
<div className="flex justify-between mt-0.5"> | ||
<div> | ||
<p className="typography-text-xs text-neutral-500">Do not include personal or financial information.</p> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
// #endregion source | ||
TextareaAutoresize.getLayout = ShowcasePageLayout; |
61 changes: 61 additions & 0 deletions
61
apps/preview/next/pages/showcases/Textarea/TextareaCharacters.tsx
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,61 @@ | ||
/* eslint-disable jsx-a11y/label-has-associated-control */ | ||
import { ShowcasePageLayout } from '../../showcases'; | ||
// #region source | ||
import { SfTextarea } from '@storefront-ui/react'; | ||
import classNames from 'classnames'; | ||
import { ChangeEvent, useState } from 'react'; | ||
|
||
export default function TextareaWithLimit() { | ||
const characterLimit = 25; | ||
const disabled = false; | ||
const readonly = false; | ||
const invalid = false; | ||
const helpText = 'Help text'; | ||
const errorText = 'Error'; | ||
|
||
const [value, setValue] = useState(''); | ||
|
||
function onChange(event: ChangeEvent<HTMLTextAreaElement>) { | ||
setValue(event?.target.value); | ||
} | ||
const isAboveLimit = characterLimit ? value.length > characterLimit : false; | ||
const charsCount = characterLimit ? characterLimit - value.length : null; | ||
|
||
const getCharacterLimitClass = () => (isAboveLimit ? 'text-negative-700 font-medium' : 'text-neutral-500'); | ||
|
||
return ( | ||
<> | ||
<label> | ||
<span className="text-sm font-medium">Description</span> | ||
<SfTextarea | ||
value={value} | ||
placeholder="Write something about yourself..." | ||
invalid={invalid} | ||
disabled={disabled} | ||
onChange={onChange} | ||
className="w-full mt-0.5" | ||
/> | ||
</label> | ||
<div className="flex justify-between mt-0.5"> | ||
<div> | ||
{invalid && !disabled && ( | ||
<p className="typography-text-sm text-negative-700 font-medium mt-0.5">{errorText}</p> | ||
)} | ||
{helpText && ( | ||
<p className={classNames('typography-text-xs', disabled ? 'text-disabled-500' : 'text-neutral-500')}> | ||
{helpText} | ||
</p> | ||
)} | ||
</div> | ||
{characterLimit && !readonly ? ( | ||
<p className={classNames('typography-text-xs', disabled ? 'text-disabled-500' : getCharacterLimitClass())}> | ||
{charsCount} | ||
</p> | ||
) : null} | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
// #endregion source | ||
TextareaWithLimit.getLayout = ShowcasePageLayout; |
Oops, something went wrong.