import { useTheme } from '../../../context/ThemeContext' import { type KeyboardEvent, useRef, useState, useId } from 'react' import Block from '../../Primitives/Block/Block' import { Tag, TagShape, TagSize } from '../../Tags' import InputFooter from '../utils/InputFooter/InputFooter' import InputLabels from '../utils/InputLabels/InputLabels' import { TextInputSize } from '../TextInput/types' import PrimitiveInput from '../../Primitives/PrimitiveInput/PrimitiveInput' import type { MultiValueInputProps } from './types' import type { MultiValueInputTokensType } from './multiValueInput.tokens' import { X } from 'lucide-react' import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens' import { FOUNDATION_THEME } from '../../../tokens' import { useErrorShake } from '../../common/useErrorShake' import { getErrorShakeStyle, errorShakeAnimation, } from '../../common/error.animations' import styled from 'styled-components' import PrimitiveButton from '../../Primitives/PrimitiveButton/PrimitiveButton' const Wrapper = styled(Block)` ${errorShakeAnimation} ` const ContentContainer = styled(Block)<{ hasLeftSlot: boolean hasRightSlot: boolean slotOffset: number }>` position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; width: 100%; ${({ hasLeftSlot, slotOffset }) => hasLeftSlot && `padding-left: ${slotOffset}px;`} ${({ hasRightSlot, slotOffset }) => hasRightSlot && `padding-right: ${slotOffset}px;`} ` const MultiValueInput = ({ value = '', label, sublabel, helpIconHintText, disabled, required, error, errorMessage, hintText, tags = [], onTagAdd, onTagRemove, onChange, size = TextInputSize.MEDIUM, onFocus, onBlur, name, id: providedId, leftSlot, rightSlot, ...rest }: MultiValueInputProps) => { const multiValueInputTokens = useResponsiveTokens('MULTI_VALUE_INPUT') const { theme: blendTheme } = useTheme() // primary[50] reads as a near-white halo on a dark surface const focusRingColor = blendTheme === 'dark' ? FOUNDATION_THEME.colors.primary[800] : FOUNDATION_THEME.colors.primary[50] const [isFocused, setIsFocused] = useState(false) const shouldShake = useErrorShake(error || false) const inputRef = useRef(null) // Generate unique IDs for accessibility (WCAG 4.1.2, 3.3.1, 3.3.2) const generatedId = useId() const inputId = providedId || generatedId const errorId = `${inputId}-error` const hintId = `${inputId}-hint` // Construct aria-describedby to link hint and error messages (WCAG 3.3.1, 3.3.2) const ariaDescribedBy = [ hintText && !error ? hintId : null, error && errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || undefined const addTag = (value: string) => { const trimmedValue = value.trim() if (trimmedValue && !tags.includes(trimmedValue)) { onTagAdd?.(trimmedValue) } } const removeTag = (tagToRemove: string) => { onTagRemove?.(tagToRemove) inputRef.current?.focus() } const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() addTag(value) } else if (e.key === 'Backspace' && value === '' && tags.length > 0) { const lastTag = tags[tags.length - 1] if (lastTag) { removeTag(lastTag) } } } const handleContainerClick = () => { inputRef.current?.focus() } const paddingX = multiValueInputTokens.inputContainer.padding.x[size] const paddingY = multiValueInputTokens.inputContainer.padding.y[size] const slotWidth = 16 const slotGap = 8 const wrappedRowPadding = slotWidth + slotGap const slotTop = tags.length > 0 ? FOUNDATION_THEME.unit[7] : FOUNDATION_THEME.unit[3] return ( {leftSlot && ( {leftSlot} )} {tags?.map((tag) => ( { e.stopPropagation() removeTag(tag) }} onKeyDown={(e) => { if ( e.key === 'Enter' || e.key === ' ' ) { e.preventDefault() e.stopPropagation() removeTag(tag) } }} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', minWidth: '24px', minHeight: '24px', }} tabIndex={0} > ) } export default MultiValueInput