// // Copyright 2023 DXOS.org // import { type IconWeight } from '@phosphor-icons/react'; import { Root as CheckboxPrimitive, type CheckboxProps as CheckboxPrimitiveProps } from '@radix-ui/react-checkbox'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import React, { type ComponentPropsWithRef, forwardRef, type ForwardRefExoticComponent, useCallback } from 'react'; import { InputRoot, type InputRootProps, PinInput as PinInputPrimitive, type PinInputProps as PinInputPrimitiveProps, TextInput as TextInputPrimitive, type TextInputProps as TextInputPrimitiveProps, TextArea as TextAreaPrimitive, type TextAreaProps as TextAreaPrimitiveProps, useInputContext, INPUT_NAME, type InputScopedProps, Description as DescriptionPrimitive, DescriptionAndValidation as DescriptionAndValidationPrimitive, type DescriptionAndValidationProps as DescriptionAndValidationPrimitiveProps, type DescriptionProps as DescriptionPrimitiveProps, Label as LabelPrimitive, type LabelProps as LabelPrimitiveProps, Validation as ValidationPrimitive, type ValidationProps as ValidationPrimitiveProps, } from '@dxos/react-input'; import { mx } from '@dxos/react-ui-theme'; import { type Density, type Elevation, type ClassNameValue, type Size } from '@dxos/react-ui-types'; import { useDensityContext, useElevationContext, useThemeContext } from '../../hooks'; import { type ThemedClassName } from '../../util'; import { Icon } from '../Icon'; type InputVariant = 'default' | 'subdued'; type InputSharedProps = Partial<{ density: Density; elevation: Elevation; variant: InputVariant }>; type LabelProps = ThemedClassName & { srOnly?: boolean }; const Label = forwardRef(({ srOnly, classNames, children, ...props }, forwardedRef) => { const { tx } = useThemeContext(); return ( {children} ); }); type DescriptionProps = ThemedClassName & { srOnly?: boolean }; const Description = forwardRef( ({ srOnly, classNames, children, ...props }, forwardedRef) => { const { tx } = useThemeContext(); return ( {children} ); }, ); type ValidationProps = ThemedClassName & { srOnly?: boolean }; const Validation = forwardRef>( ({ __inputScope, srOnly, classNames, children, ...props }, forwardedRef) => { const { tx } = useThemeContext(); const { validationValence } = useInputContext(INPUT_NAME, __inputScope); return ( {children} ); }, ); type DescriptionAndValidationProps = ThemedClassName & { srOnly?: boolean }; const DescriptionAndValidation = forwardRef( ({ srOnly, classNames, children, ...props }, forwardedRef) => { const { tx } = useThemeContext(); return ( {children} ); }, ); type PinInputProps = InputSharedProps & Omit & { segmentClassName?: ClassNameValue; inputClassName?: ClassNameValue; }; const PinInput = forwardRef( ( { density: propsDensity, elevation: propsElevation, segmentClassName: propsSegmentClassName, inputClassName, variant, ...props }, forwardedRef, ) => { const { hasIosKeyboard } = useThemeContext(); const { tx } = useThemeContext(); const density = useDensityContext(propsDensity); const elevation = useElevationContext(propsElevation); const segmentClassName = useCallback( ({ focused, validationValence }: Parameters>[0]) => tx( 'input.input', 'input--pin-segment', { variant: 'static', focused, disabled: props.disabled, density, elevation, validationValence, }, propsSegmentClassName, ), [tx, props.disabled, elevation, propsElevation, density], ); return ( ); }, ); // TODO(burdon): Implement inline icon within button: e.g., https://www.radix-ui.com/themes/playground#text-field type TextInputProps = InputSharedProps & ThemedClassName; const TextInput = forwardRef>( ({ __inputScope, classNames, density: propsDensity, elevation: propsElevation, variant, ...props }, forwardedRef) => { const { hasIosKeyboard } = useThemeContext(); const themeContextValue = useThemeContext(); const density = useDensityContext(propsDensity); const elevation = useElevationContext(propsElevation); const { validationValence } = useInputContext(INPUT_NAME, __inputScope); const { tx } = themeContextValue; return ( ); }, ); type TextAreaProps = InputSharedProps & ThemedClassName; const TextArea = forwardRef>( ({ __inputScope, classNames, density: propsDensity, elevation: propsElevation, variant, ...props }, forwardedRef) => { const { hasIosKeyboard } = useThemeContext(); const { tx } = useThemeContext(); const density = useDensityContext(propsDensity); const elevation = useElevationContext(propsElevation); const { validationValence } = useInputContext(INPUT_NAME, __inputScope); return ( ); }, ); type CheckboxProps = ThemedClassName> & { size?: Size; weight?: IconWeight }; const Checkbox: ForwardRefExoticComponent = forwardRef< HTMLButtonElement, InputScopedProps >( ( { __inputScope, checked: propsChecked, defaultChecked: propsDefaultChecked, onCheckedChange: propsOnCheckedChange, size, weight = 'bold', classNames, ...props }, forwardedRef, ) => { const [checked, onCheckedChange] = useControllableState({ prop: propsChecked, defaultProp: propsDefaultChecked, onChange: propsOnCheckedChange, }); const { id, validationValence, descriptionId, errorMessageId } = useInputContext(INPUT_NAME, __inputScope); const { tx } = useThemeContext(); return ( ); }, ); type SwitchProps = ThemedClassName< Omit, 'children' | 'onChange'> & { onCheckedChange?: (checked: boolean) => void } >; const Switch = forwardRef>( ( { __inputScope, checked: propsChecked, defaultChecked: propsDefaultChecked, onCheckedChange: propsOnCheckedChange, classNames, ...props }, forwardedRef, ) => { const [checked, onCheckedChange] = useControllableState({ prop: propsChecked, defaultProp: propsDefaultChecked ?? false, onChange: propsOnCheckedChange, }); const { id, validationValence, descriptionId, errorMessageId } = useInputContext(INPUT_NAME, __inputScope); return ( { onCheckedChange(event.target.checked); }} id={id} aria-describedby={descriptionId} {...props} {...(validationValence === 'error' && { 'aria-invalid': 'true' as const, 'aria-errormessage': errorMessageId, })} ref={forwardedRef} /> ); }, ); export const Input = { Root: InputRoot, PinInput, TextInput, TextArea, Checkbox, Switch, Label, Description, Validation, DescriptionAndValidation, }; export type { InputVariant, InputRootProps, InputSharedProps, PinInputProps, TextInputProps, TextAreaProps, CheckboxProps, SwitchProps, LabelProps, DescriptionProps, ValidationProps, DescriptionAndValidationProps, };