Skip to content

Commit

Permalink
feat: create TextareaField component
Browse files Browse the repository at this point in the history
  • Loading branch information
artursantiago committed Feb 6, 2025
1 parent ae081dd commit cce11c0
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,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'
137 changes: 137 additions & 0 deletions packages/ui/src/components/molecules/TextareaField/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
[data-fs-textarea-field] {
// --------------------------------------------------------
// Design Tokens for Input Field
// --------------------------------------------------------

// Default properties

--fs-label-placeholder-top-padding: 12px;
--fs-textarea-width: 100%; /* Fallback */
--fs-textarea-height: 100%; /* Fallback */
--fs-label-max-width: var(--fs-textarea-width);
--fs-label-max-height: calc(
var(--fs-textarea-height) - var(--fs-label-placeholder-top-padding)
);

--fs-textarea-field-padding: 18px var(--fs-spacing-2) 0;
--fs-textarea-field-color: var(--fs-color-text);
--fs-textarea-field-size: var(--fs-text-size-body);
--fs-textarea-field-border-color: var(--fs-border-color);

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

// Label
--fs-textarea-field-label-padding: 0 var(--fs-spacing-2);
--fs-textarea-field-label-color: var(--fs-color-text-light);
--fs-textarea-field-label-size: var(--fs-text-size-tiny);

// Button
--fs-textarea-field-button-height: var(--fs-control-tap-size);

// Error
--fs-textarea-field-error-message-size: var(--fs-text-size-legend);
--fs-textarea-field-error-message-line-height: 1.1;
--fs-textarea-field-error-message-margin-top: var(--fs-spacing-0);
--fs-textarea-field-error-message-color: var(--fs-color-danger-text);
--fs-textarea-field-error-border-color: var(--fs-color-danger-border);
--fs-textarea-field-error-box-shadow: 0 0 0 var(--fs-border-width)
var(--fs-textarea-field-error-border-color);
--fs-textarea-field-error-focus-ring: var(--fs-color-focus-ring-danger);

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

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

position: relative;
display: flex;
flex-flow: column;

[data-fs-label] {
transition:
var(--fs-textarea-field-transition-property)
var(--fs-textarea-field-transition-timing)
var(--fs-textarea-field-transition-function),
max-width 0s,
max-height 0s;
}

[data-fs-textarea] {
--fs-textarea-padding: var(--fs-textarea-field-padding);
padding: var(--fs-textarea-field-padding);
color: var(--fs-textarea-field-color);

&:placeholder-shown + label {
top: var(--fs-label-placeholder-top-padding);
overflow: hidden;
}

&::placeholder {
opacity: 0;
transition: inherit;
}

&:focus::placeholder {
opacity: 1;
}

&:not(:placeholder-shown) + label,
&:focus + label {
top: rem(6px);
left: var(--fs-border-width);
font-size: var(--fs-textarea-field-label-size);

white-space: nowrap;
}

&:disabled + label {
cursor: not-allowed;
}
}

[data-fs-label] {
position: absolute;
padding: var(--fs-textarea-field-label-padding);
font-size: var(--fs-textarea-field-size);
line-height: var(--fs-textarea-field-size);
color: var(--fs-textarea-field-label-color);

max-width: var(--fs-label-max-width);
max-height: var(--fs-label-max-height);
text-overflow: ellipsis;
overflow: hidden;
}

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

&[data-fs-textarea-field-error='true'] {
[data-fs-textarea] {
border-color: var(--fs-textarea-field-error-border-color);
@include input-focus-ring(
$outline: #{var(--fs-textarea-field-error-focus-ring)},
$border: #{var(--fs-textarea-field-error-border-color)}
);

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

[data-fs-textarea-field-error-message] {
margin-top: var(--fs-textarea-field-error-message-margin-top);
font-size: var(--fs-textarea-field-error-message-size);
line-height: var(--fs-textarea-field-error-message-line-height);
color: var(--fs-textarea-field-error-message-color);
}
}
}

0 comments on commit cce11c0

Please sign in to comment.