Skip to content

Commit

Permalink
feat: textarea base component (#2880)
Browse files Browse the repository at this point in the history
* 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
AdamPawlinski committed Jul 6, 2023
1 parent 06bb45b commit aa9c2f3
Show file tree
Hide file tree
Showing 30 changed files with 978 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .changeset/two-ads-fix.md
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
127 changes: 127 additions & 0 deletions apps/docs/components/components/textarea.md
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>
:::
1 change: 1 addition & 0 deletions apps/preview/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"swr": "^2.0.3"
},
"devDependencies": {
"@frsource/autoresize-textarea": "^1.3.76",
"@storefront-ui/eslint-config": "workspace:*",
"@storefront-ui/example-style": "workspace:*",
"@storefront-ui/react": "workspace:*",
Expand Down
152 changes: 152 additions & 0 deletions apps/preview/next/pages/examples/SfTextarea.tsx
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 apps/preview/next/pages/showcases/Textarea/TextareaAutoresize.tsx
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 apps/preview/next/pages/showcases/Textarea/TextareaCharacters.tsx
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;
Loading

0 comments on commit aa9c2f3

Please sign in to comment.