Merged in feat/use-new-input-component (pull request #3324)

feat(SW-3659): Use new input component

* Use new input component

* Update error formatter

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Update Input stories

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Update Storybook logo

* Add some new demo icon input story

* Fix the clear content button position

* Fix broken password input icon

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Add aria-hidden to required asterisk

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component


Approved-by: Bianca Widstam
Approved-by: Matilda Landström
This commit is contained in:
Rasmus Langvad
2025-12-18 15:42:09 +00:00
parent 40e1efa81f
commit b9a62b5280
34 changed files with 520 additions and 1113 deletions

View File

@@ -12,36 +12,165 @@ import { InputLabel } from '../InputLabel'
import styles from './input.module.css'
import type { InputProps } from './types'
import { IconButton } from '../IconButton'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography'
import type { InputProps } from './types'
import { clearInput, useInputHasValue } from './utils'
const InputComponent = forwardRef(function AriaInputWithLabelComponent(
{ label, ...props }: InputProps,
forwardedRef: ForwardedRef<HTMLInputElement>
{
label,
labelPosition = 'floating',
leftIcon,
rightIcon,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onRightIconClick,
showClearContentIcon,
placeholder,
id,
required,
'data-validation-state': validationState,
...props
}: InputProps & { 'data-validation-state'?: string },
ref: ForwardedRef<HTMLInputElement>
) {
const ref = useRef<HTMLInputElement>(null)
// Create an internal ref that we can access
const internalRef = useRef<HTMLInputElement>(null)
// Unique id is required for multiple inputs of same name appearing multiple times
// on same page. This will inherited by parent label element.
// Shouldn't really be needed if we don't set id though.
const uniqueId = useId()
const inputId = `${uniqueId}-${props.name}`
// 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()
useImperativeHandle(forwardedRef, () => ref.current as HTMLInputElement)
// 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}
variant="Muted"
emphasis
onPress={onClearContent}
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
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 (
<AriaLabel className={styles.container}>
<Typography variant="Body/Paragraph/mdRegular">
<AriaInput
{...props}
placeholder={props.placeholder}
className={cx(styles.input, props.className)}
ref={ref}
id={inputId}
/>
</Typography>
<InputLabel required={props.required}>{label}</InputLabel>
</AriaLabel>
<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}
variant="Muted"
emphasis
onPress={onClearContent}
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
aria-label="Clear content"
>
<MaterialIcon icon="cancel" />
</IconButton>
</div>
)}
{rightIcon && !(showClearContentIcon && hasValue) && (
<div className={styles.rightIconContainer}>{rightIcon}</div>
)}
</div>
)
})