Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add textareafield component #2641

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/components/src/atoms/Textarea/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { TextareaHTMLAttributes } from 'react'
import React, { forwardRef } from 'react'

export interface TextareaProps
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
/**
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
*/
testId?: string
/**
* Controls the resize property of the textare (e.g.: none, vertical, horizontal, both).
* Default is 'both'.
*/
resize?: 'none' | 'vertical' | 'horizontal' | 'both'
}

const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
function Textarea(
{ testId = 'fs-textarea', resize = 'both', ...otherProps },
ref
) {
return (
<textarea
ref={ref}
data-fs-textarea
data-fs-textarea-resize={resize}
data-testid={testId}
{...otherProps}
/>
)
}
)
export default Textarea
2 changes: 2 additions & 0 deletions packages/components/src/atoms/Textarea/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './Textarea'
export type { TextareaProps } from './Textarea'
4 changes: 4 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export { default as Icon } from './atoms/Icon'
export type { IconProps } from './atoms/Icon'
export { default as Input } from './atoms/Input'
export type { InputProps } from './atoms/Input'
export { default as Textarea } from './atoms/Textarea'
export type { TextareaProps } from './atoms/Textarea'
export { default as Label } from './atoms/Label'
export type { LabelProps } from './atoms/Label'
export { default as Link } from './atoms/Link'
Expand Down Expand Up @@ -106,6 +108,8 @@ export type {
} from './molecules/Gift'
export { default as InputField } from './molecules/InputField'
export type { InputFieldProps } from './molecules/InputField'
export { default as TextareaField } from './molecules/TextareaField'
export type { TextareaFieldProps } from './molecules/TextareaField'
export { default as LinkButton } from './molecules/LinkButton'
export type { LinkButtonProps } from './molecules/LinkButton'
export { default as Modal, ModalHeader, ModalBody } from './molecules/Modal'
Expand Down
100 changes: 100 additions & 0 deletions packages/components/src/molecules/TextareaField/TextareaField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { MutableRefObject } from 'react'
import React, { useEffect, useRef } from 'react'

import type { TextareaProps } from '../..'
import { Textarea, Label } from '../..'

type DefaultProps = {
/**
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
*/
testId?: string
/**
* ID to identify textarea and corresponding label.
*/
id: string
/**
* The text displayed to identify textarea text.
*/
label: string
/**
* The error message is displayed when an error occurs.
*/
error?: string
/**
* Component's ref.
*/
textareaRef?: MutableRefObject<HTMLTextAreaElement | null>
/**
* Specifies that the whole textarea component should be disabled.
*/
disabled?: boolean
}

export type TextareaFieldProps = DefaultProps &
Omit<TextareaProps, 'disabled' | 'onSubmit'>

const TextareaField = ({
id,
label,
error,
placeholder = ' ', // initializes with an empty space to style float label using `placeholder-shown`
textareaRef,
disabled,
value,
testId = 'fs-textarea-field',
...otherProps
}: TextareaFieldProps) => {
const shouldDisplayError = !disabled && error && error !== ''
const textareaInternalRef = useRef<HTMLTextAreaElement | null>(null)
const ref = textareaRef || textareaInternalRef

useEffect(() => {
const textarea = ref?.current
if (!textarea) return

const updateSize = () => {
textarea.parentElement?.style.setProperty(
'--fs-textarea-width',
`${textarea.offsetWidth}px`
)
textarea.parentElement?.style.setProperty(
'--fs-textarea-height',
`${textarea.offsetHeight}px`
)
}

updateSize()

const resizeObserver = new ResizeObserver(updateSize)
resizeObserver.observe(textarea)

return () => {
resizeObserver.disconnect()
}
}, [ref])

return (
<div
data-fs-textarea-field
data-fs-textarea-field-error={error && error !== ''}
data-testid={testId}
>
<Textarea
id={id}
value={value}
ref={ref}
disabled={disabled}
placeholder={placeholder}
{...otherProps}
/>
<Label htmlFor={id}>{label}</Label>

{shouldDisplayError && (
<span data-fs-textarea-field-error-message>{error}</span>
)}
</div>
)
}

export default TextareaField
2 changes: 2 additions & 0 deletions packages/components/src/molecules/TextareaField/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './TextareaField'
export type { TextareaFieldProps } from './TextareaField'
38 changes: 0 additions & 38 deletions packages/ui/src/components/atoms/TextArea/TextArea.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions packages/ui/src/components/atoms/TextArea/index.ts

This file was deleted.

86 changes: 86 additions & 0 deletions packages/ui/src/components/atoms/Textarea/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[data-fs-textarea] {
// --------------------------------------------------------
// Design Tokens for Textarea
// --------------------------------------------------------

// Default properties
--fs-textarea-padding: var(--fs-spacing-1) var(--fs-spacing-2);
--fs-textarea-height: calc(var(--fs-control-tap-size) * 3);

--fs-textarea-bkg-color: var(--fs-color-body-bkg);
--fs-textarea-box-shadow: none;
--fs-textarea-box-shadow-hover: 0 0 0 var(--fs-border-width) var(--fs-border-color-active);

--fs-textarea-border-radius: var(--fs-border-radius);
--fs-textarea-border-width: var(--fs-border-width);
--fs-textarea-border-color: var(--fs-border-color);
--fs-textarea-border-color-hover: var(--fs-border-color-active);

--fs-textarea-text-color: var(--fs-color-text);
--fs-textarea-text-size: var(--fs-text-size-body);

--fs-textarea-line-height: 1.25;

--fs-textarea-transition-function: var(--fs-transition-function);
--fs-textarea-transition-property: var(--fs-transition-property);
--fs-textarea-transition-timing: var(--fs-transition-timing);

// Disabled properties
--fs-textarea-disabled-bkg-color: var(--fs-color-disabled-bkg);
--fs-textarea-disabled-text-color: var(--fs-color-disabled-text);
--fs-textarea-disabled-border-width: var(--fs-border-width);
--fs-textarea-disabled-border-color: var(--fs-border-color);

// --------------------------------------------------------
// Structural Styles
// --------------------------------------------------------

width: 100%;
height: var(--fs-textarea-height);
padding: var(--fs-textarea-padding);
font-size: var(--fs-textarea-text-size);
line-height: var(--fs-textarea-line-height);
color: var(--fs-textarea-text-color);
background-color: var(--fs-textarea-bkg-color);
border: var(--fs-textarea-border-width) solid var(--fs-textarea-border-color);
border-radius: var(--fs-textarea-border-radius);
box-shadow: var(--fs-textarea-box-shadow);
transition:
var(--fs-textarea-transition-property) var(--fs-textarea-transition-timing) var(--fs-textarea-transition-function),
width 0s,
height 0s;

@include input-focus-ring;

&:hover:not(:disabled):not(:focus-visible):not(:focus) {
border-color: var(--fs-textarea-border-color-hover);
box-shadow: var(--fs-textarea-box-shadow-hover);
}

&:disabled {
color: var(--fs-textarea-disabled-text-color);
cursor: not-allowed;
background-color: var(--fs-textarea-disabled-bkg-color);
border: var(--fs-textarea-disabled-border-width) solid var(--fs-textarea-disabled-border-color);
}

// --------------------------------------------------------
// Variants Styles
// --------------------------------------------------------

&[data-fs-textarea-resize="none"] {
resize: none;
}

&[data-fs-textarea-resize="vertical"] {
resize: vertical;
}

&[data-fs-textarea-resize="horizontal"] {
resize: horizontal;
}

&[data-fs-textarea-resize="both"] {
resize: both;
}
}
Loading
Loading