Merged in feat/SW-3655-input-component (pull request #3296)
feat: (SW-3655) new Input and FormInput components * First version new Input and FormInput components * Handle aria-describedby with react-aria instead of manually add it * Update breaking unit and stories tests * Merge branch 'master' into feat/SW-3655-input-component * Update example form * Merge branch 'master' into feat/SW-3655-input-component * New lock file Approved-by: Linus Flood
This commit is contained in:
174
packages/design-system/lib/components/InputNew/Input.tsx
Normal file
174
packages/design-system/lib/components/InputNew/Input.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { cx } from 'class-variance-authority'
|
||||
import {
|
||||
type ForwardedRef,
|
||||
forwardRef,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { Input as AriaInput, Label as AriaLabel } from 'react-aria-components'
|
||||
|
||||
import { InputLabel } from '../InputLabel'
|
||||
|
||||
import styles from './input.module.css'
|
||||
|
||||
import type { InputProps } from './types'
|
||||
import { Typography } from '../Typography'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { IconButton } from '../IconButton'
|
||||
import { clearInput, useInputHasValue } from './utils'
|
||||
|
||||
const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
||||
{
|
||||
label,
|
||||
labelPosition = 'floating',
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
onRightIconClick,
|
||||
showClearContentIcon,
|
||||
placeholder,
|
||||
id,
|
||||
required,
|
||||
'data-validation-state': validationState,
|
||||
...props
|
||||
}: InputProps & { 'data-validation-state'?: string },
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
// Create an internal ref that we can access
|
||||
const internalRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Generate a unique ID for the input
|
||||
// This is used to ensure the input is properly associated with the label
|
||||
// when the label is positioned above the input
|
||||
const generatedId = useId()
|
||||
|
||||
// Forward the ref properly
|
||||
useImperativeHandle(ref, () => internalRef.current!, [])
|
||||
|
||||
// Track whether input has a value (for showing/hiding clear button)
|
||||
const hasValue = useInputHasValue(props.value, internalRef)
|
||||
|
||||
const onClearContent = () => {
|
||||
clearInput({
|
||||
inputRef: internalRef,
|
||||
onChange: props.onChange,
|
||||
value: props.value,
|
||||
})
|
||||
}
|
||||
// When labelPosition is 'top', restructure to have label outside container
|
||||
// We need an ID for proper label-input association
|
||||
if (labelPosition === 'top') {
|
||||
const inputId = id || generatedId
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputLabel
|
||||
required={required}
|
||||
className={cx(
|
||||
styles.labelAbove,
|
||||
leftIcon && styles.labelAboveWithLeftIcon
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<div className={styles.inputContainer}>
|
||||
{leftIcon && (
|
||||
<div className={styles.leftIconContainer}>{leftIcon}</div>
|
||||
)}
|
||||
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className={cx(
|
||||
styles.container,
|
||||
leftIcon && styles.containerWithLeftIcon,
|
||||
rightIcon && styles.containerWithRightIcon
|
||||
)}
|
||||
data-validation-state={validationState}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<AriaInput
|
||||
{...props}
|
||||
id={inputId}
|
||||
required={required}
|
||||
// Avoid duplicating label text in placeholder when label is positioned above
|
||||
// Screen readers would announce the label twice (once as label, once as placeholder)
|
||||
// Only use placeholder if explicitly provided, otherwise use empty string
|
||||
placeholder={placeholder ?? ''}
|
||||
className={cx(
|
||||
styles.input,
|
||||
styles.inputTopLabel,
|
||||
props.className
|
||||
)}
|
||||
ref={internalRef}
|
||||
/>
|
||||
</Typography>
|
||||
</label>
|
||||
{showClearContentIcon && hasValue && (
|
||||
<div className={styles.rightIconContainer}>
|
||||
<IconButton
|
||||
className={styles.rightIconButton}
|
||||
theme="Black"
|
||||
onClick={onClearContent}
|
||||
aria-label="Clear content"
|
||||
>
|
||||
<MaterialIcon icon="cancel" />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
{rightIcon && !(showClearContentIcon && hasValue) && (
|
||||
<div className={styles.rightIconContainer}>{rightIcon}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Floating label (default behavior) - label inside container
|
||||
return (
|
||||
<div className={styles.inputContainer}>
|
||||
{leftIcon && <div className={styles.leftIconContainer}>{leftIcon}</div>}
|
||||
<AriaLabel
|
||||
className={cx(
|
||||
styles.container,
|
||||
leftIcon && styles.containerWithLeftIcon,
|
||||
rightIcon && styles.containerWithRightIcon
|
||||
)}
|
||||
data-validation-state={validationState}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<AriaInput
|
||||
{...props}
|
||||
id={id}
|
||||
required={required}
|
||||
// For floating labels, only set placeholder if explicitly provided
|
||||
// The label itself acts as the placeholder, so we don't want to duplicate it
|
||||
// This ensures the label only floats when focused or has value
|
||||
placeholder={placeholder}
|
||||
className={cx(styles.input, props.className)}
|
||||
ref={internalRef}
|
||||
/>
|
||||
</Typography>
|
||||
<InputLabel required={required}>{label}</InputLabel>
|
||||
</AriaLabel>
|
||||
{showClearContentIcon && hasValue && (
|
||||
<div className={styles.rightIconContainer}>
|
||||
<IconButton
|
||||
className={styles.rightIconButton}
|
||||
theme="Black"
|
||||
onClick={onClearContent}
|
||||
aria-label="Clear content"
|
||||
>
|
||||
<MaterialIcon icon="cancel" />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
{rightIcon && !(showClearContentIcon && hasValue) && (
|
||||
<div className={styles.rightIconContainer}>{rightIcon}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export const Input = InputComponent as React.ForwardRefExoticComponent<
|
||||
InputProps & React.RefAttributes<HTMLInputElement>
|
||||
>
|
||||
Reference in New Issue
Block a user