From 71166398a537e2a752e31299ba54c0cea39d32c1 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Tue, 12 May 2026 16:44:39 -0300 Subject: [PATCH 01/10] feat: consolidate input and dropdown updates into one signed commit Unifies the input and dropdown design updates into a single signed commit so the history is easier to audit and verify. Co-authored-by: Cursor --- packages/mobile/src/controls/InputLabel.tsx | 5 +- packages/mobile/src/controls/InputStack.tsx | 20 +++- packages/mobile/src/controls/NativeInput.tsx | 6 +- packages/mobile/src/controls/Select.tsx | 4 +- packages/mobile/src/controls/TextInput.tsx | 9 +- .../__stories__/TextInput.stories.tsx | 70 +++++------- .../src/hooks/useInputBorderAnimation.tsx | 2 +- .../src/alpha/select/DefaultSelectControl.tsx | 6 +- packages/web/src/controls/InputLabel.tsx | 2 +- packages/web/src/controls/InputStack.tsx | 27 ++++- packages/web/src/controls/NativeInput.tsx | 6 +- packages/web/src/controls/NativeTextArea.tsx | 2 +- packages/web/src/controls/SelectStack.tsx | 3 + packages/web/src/controls/SelectTrigger.tsx | 3 +- packages/web/src/controls/TextInput.tsx | 15 +-- .../__stories__/TextInput.stories.tsx | 102 ++++++++++++------ .../src/controls/__tests__/TextInput.test.tsx | 2 +- 17 files changed, 174 insertions(+), 110 deletions(-) diff --git a/packages/mobile/src/controls/InputLabel.tsx b/packages/mobile/src/controls/InputLabel.tsx index 221411575e..62f7bb3380 100644 --- a/packages/mobile/src/controls/InputLabel.tsx +++ b/packages/mobile/src/controls/InputLabel.tsx @@ -4,6 +4,9 @@ import { Text } from '../typography/Text'; import type { HelperTextProps } from './HelperText'; -export const InputLabel = memo(function InputLabel({ color, ...props }: HelperTextProps) { +export const InputLabel = memo(function InputLabel({ + color = 'fgMuted', + ...props +}: HelperTextProps) { return ; }); diff --git a/packages/mobile/src/controls/InputStack.tsx b/packages/mobile/src/controls/InputStack.tsx index a0cd2e0a50..d1dd992280 100644 --- a/packages/mobile/src/controls/InputStack.tsx +++ b/packages/mobile/src/controls/InputStack.tsx @@ -95,7 +95,7 @@ const variantColorMap: Record = { positive: 'fgPositive', negative: 'fgNegative', foreground: 'fg', - foregroundMuted: 'fgMuted', + foregroundMuted: 'bgLine', secondary: 'bgSecondary', }; @@ -127,6 +127,17 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) { } = mergedProps; const theme = useTheme(); const [inputAreaSize, onInputAreaLayout] = useLayout(); + const resolvedInputBackground = useMemo(() => { + if (variant === 'secondary') { + return 'bgSecondary'; + } + + if (variant === 'negative' && inputBackground === 'bg') { + return 'bgNegativeWash'; + } + + return inputBackground; + }, [variant, inputBackground]); const inputAreaStyle: ViewStyle = useMemo(() => { const inputBorderRadius: ViewStyle = { @@ -149,13 +160,12 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) { variant === 'secondary' ? 'transparent' : theme.color[ - variant === 'foregroundMuted' || !variant ? 'bgLineHeavy' : variantColorMap[variant] + variant === 'foregroundMuted' || !variant ? 'bgLine' : variantColorMap[variant] ], borderWidth: theme.borderWidth[borderWidth], flexDirection: 'row', flexGrow: 1, - backgroundColor: - variant === 'secondary' ? theme.color.bgSecondary : theme.color[inputBackground], + backgroundColor: theme.color[resolvedInputBackground], borderRadius: theme.borderRadius[borderRadius], overflow: 'hidden', ...inputBorderRadius, @@ -168,7 +178,7 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) { theme.borderWidth, theme.borderRadius, borderWidth, - inputBackground, + resolvedInputBackground, borderRadius, ]); diff --git a/packages/mobile/src/controls/NativeInput.tsx b/packages/mobile/src/controls/NativeInput.tsx index 16473d7a97..a525bb0f8e 100644 --- a/packages/mobile/src/controls/NativeInput.tsx +++ b/packages/mobile/src/controls/NativeInput.tsx @@ -11,6 +11,8 @@ import type { TextBaseProps } from '../typography/Text'; import type { TextInputBaseProps } from './TextInput'; +const defaultNativeInputBorderRadius: ThemeVars.BorderRadius = 400; + export type NativeInputProps = { /** * Text Align Input @@ -90,13 +92,15 @@ export const NativeInput = memo( ...containerSpacing, ...(!disabled && editableInputAddonProps.readOnly && { - backgroundColor: theme.color.bgSecondary, + backgroundColor: theme.color.bgSecondaryWash, + borderRadius: theme.borderRadius[defaultNativeInputBorderRadius], }), }; }, [ containerSpacing, theme.space, theme.color, + theme.borderRadius, compact, editableInputAddonProps.readOnly, disabled, diff --git a/packages/mobile/src/controls/Select.tsx b/packages/mobile/src/controls/Select.tsx index 5bba5dbcd8..00a1d3c41b 100644 --- a/packages/mobile/src/controls/Select.tsx +++ b/packages/mobile/src/controls/Select.tsx @@ -29,6 +29,7 @@ import { useSelect } from './useSelect'; const selectTriggerMinHeight = 56; const selectTriggerCompactMinHeight = 40; const selectTriggerInsideLabelMinHeight = 24; +const selectTriggerBorderRadius = 400; const variantColorMap: Record = { primary: 'fgPrimary', @@ -180,6 +181,7 @@ export const Select = memo( = { secondary: 'bgSecondary', }; +const defaultTextInputBorderRadius: InputStackBaseProps['borderRadius'] = 400; + export const TextInput = memo( forwardRef((_props: TextInputProps, ref: ForwardedRef) => { const mergedProps = useComponentConfig('TextInput', _props); @@ -151,7 +152,7 @@ export const TextInput = memo( compact, suffix = '', accessibilityLabel, - borderRadius, + borderRadius = defaultTextInputBorderRadius, enableColorSurge = false, helperTextErrorIconAccessibilityLabel = 'error', bordered = true, @@ -163,7 +164,7 @@ export const TextInput = memo( } = mergedProps; const theme = useTheme(); const [focused, setFocused] = useState(false); - const focusedVariant = useInputVariant(focused, variant); + const focusedVariant = variant; const internalRef = useRef(null); const refs = useMergeRefs(ref, internalRef); const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle( @@ -235,7 +236,7 @@ export const TextInput = memo( const readOnlyInputBackground = useMemo(() => { if (!disabled && editableInputAddonProps.readOnly) { - return 'bgSecondary'; + return 'bgSecondaryWash'; } return undefined; }, [disabled, editableInputAddonProps.readOnly]); diff --git a/packages/mobile/src/controls/__stories__/TextInput.stories.tsx b/packages/mobile/src/controls/__stories__/TextInput.stories.tsx index 32fe0b8e0c..f80e656334 100644 --- a/packages/mobile/src/controls/__stories__/TextInput.stories.tsx +++ b/packages/mobile/src/controls/__stories__/TextInput.stories.tsx @@ -436,50 +436,32 @@ const InputScreen = () => { placeholder="Placeholder" /> - - - - - } - /> - - - } - label="Username" - labelVariant="inside" - placeholder="john.doe@coinbase.com" - /> - - - } - label="Username" - labelVariant="inside" - placeholder="john.doe@coinbase.com" - start={} - /> - - - - - - + + + + + Figma inside-label states + + + + Default + + + + Filled + + + + Read only + + + + Negative + + + + + = { positive: 'bgPositive', negative: 'bgNegative', foreground: 'bgInverse', - foregroundMuted: 'bgLineHeavy', + foregroundMuted: 'bgLine', secondary: 'bgSecondary', }; diff --git a/packages/web/src/alpha/select/DefaultSelectControl.tsx b/packages/web/src/alpha/select/DefaultSelectControl.tsx index 2c93074c3f..3f7a09a7ab 100644 --- a/packages/web/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/web/src/alpha/select/DefaultSelectControl.tsx @@ -26,6 +26,7 @@ import { const LABEL_VARIANT_INSIDE_HEIGHT = 32; const COMPACT_HEIGHT = 40; const DEFAULT_HEIGHT = 56; +const selectControlBorderRadius = 400; const noFocusOutlineCss = css` &:focus, @@ -246,7 +247,7 @@ const DefaultSelectControlComponent = memo( setOpen((s) => !s)} tabIndex={-1}> {label} @@ -491,6 +492,7 @@ const DefaultSelectControlComponent = memo( ; export const InputLabel = memo(function InputLabel({ - color = 'fg', + color = 'fgMuted', disabled = false, ...props }: InputLabelProps) { diff --git a/packages/web/src/controls/InputStack.tsx b/packages/web/src/controls/InputStack.tsx index 3081d0c944..2dfc81f3f7 100644 --- a/packages/web/src/controls/InputStack.tsx +++ b/packages/web/src/controls/InputStack.tsx @@ -66,7 +66,7 @@ const variantColorMap: Record = { positive: 'bgPositive', negative: 'bgNegative', foreground: 'bgInverse', - foregroundMuted: 'bgLineHeavy', + foregroundMuted: 'bgLine', secondary: 'bgSecondary', }; @@ -204,12 +204,19 @@ export const InputStack = memo( return 'transparent'; } + if (variant === 'secondary') { + return 'transparent'; + } + + if (variant === 'foreground' || variant === 'foregroundMuted') { + return 'var(--color-bgInverse)'; + } + if (variant === 'positive' || variant === 'negative') { return `var(--color-${variantColorMap[variant]})`; } - // all variants except for positive/negative receive the primary focus color - return 'var(--color-bgPrimary)'; + return `var(--color-${variantColorMap[variant]})`; }, [disableFocusedStyle, variant]); const borderColorUnfocused = useMemo(() => { @@ -229,6 +236,18 @@ export const InputStack = memo( }; }, [borderColorUnfocused, borderColorFocused, focusedBorderWidth, inputBorderRadius]); + const resolvedInputBackground = useMemo(() => { + if (variant === 'secondary') { + return 'bgSecondary'; + } + + if (variant === 'negative' && inputBackground === 'bg') { + return 'bgNegativeWash'; + } + + return inputBackground; + }, [variant, inputBackground]); + return ( (function SelectStack( { @@ -86,6 +88,7 @@ export const SelectStack = memo( } labelVariant={labelVariant} variant={variant} + borderRadius={defaultSelectBorderRadius} width="100%" /> ); diff --git a/packages/web/src/controls/SelectTrigger.tsx b/packages/web/src/controls/SelectTrigger.tsx index 219aefd3ad..03625d57a0 100644 --- a/packages/web/src/controls/SelectTrigger.tsx +++ b/packages/web/src/controls/SelectTrigger.tsx @@ -18,6 +18,7 @@ import { SelectStack } from './SelectStack'; const selectTriggerMinHeight = 56; const selectTriggerCompactMinHeight = 40; const selectTriggerInsideLabelMinHeight = 62; +const selectTriggerBorderRadius = 400; export type SelectTriggerProps = Omit< SelectBaseProps, @@ -129,7 +130,7 @@ export const SelectTrigger = memo( ) : null} { - return useMemo( - () => (focused && variant !== 'positive' && variant !== 'negative' ? 'primary' : variant), - [focused, variant], - ); -}; - const variantColorMap: Record = { primary: 'fgPrimary', positive: 'fgPositive', @@ -148,6 +141,8 @@ const variantColorMap: Record = { secondary: 'fg', }; +const defaultTextInputBorderRadius: InputStackBaseProps['borderRadius'] = 400; + export const TextInput = memo( forwardRef(function TextInput(_props: TextInputProps, ref: React.ForwardedRef) { const mergedProps = useComponentConfig('TextInput', _props); @@ -168,7 +163,7 @@ export const TextInput = memo( suffix = '', onFocus, onBlur, - borderRadius = 200, + borderRadius = defaultTextInputBorderRadius, height, inputNode, bordered = true, @@ -181,7 +176,7 @@ export const TextInput = memo( ...nativeInputRestProps } = mergedProps; const [focused, setFocused] = useState(false); - const focusedVariant = useInputVariant(focused, variant); + const focusedVariant = variant; const internalRef = useRef(); const refs = useMergeRefs(ref, internalRef); @@ -226,7 +221,7 @@ export const TextInput = memo( // Define a distinct read-only style to differentiate it from the disabled style. const readOnlyInputBackground = useMemo(() => { if (!disabled && nativeInputRestProps.readOnly) { - return 'bgSecondary'; + return 'bgSecondaryWash'; } return undefined; }, [disabled, nativeInputRestProps.readOnly]); diff --git a/packages/web/src/controls/__stories__/TextInput.stories.tsx b/packages/web/src/controls/__stories__/TextInput.stories.tsx index df28ee49c4..de420bcc68 100644 --- a/packages/web/src/controls/__stories__/TextInput.stories.tsx +++ b/packages/web/src/controls/__stories__/TextInput.stories.tsx @@ -62,38 +62,78 @@ export const Basic = function Basic() { }; export const InsideLabel = function InsideLabel() { + const figmaStates = [ + { + title: 'Default', + node: , + }, + { + title: 'Filled', + node: ( + undefined} value="Text" /> + ), + }, + { + title: 'Read only', + node: , + }, + { + title: 'Negative', + node: , + }, + ] as const; + return ( - - - } - variant="secondary" - /> - } - label=" Secondary End" - labelVariant="inside" - placeholder="Placeholder" - variant="secondary" - /> - - - + + + + Figma inside-label states + + + {figmaStates.map((state) => ( + + + {state.title} + + {state.node} + + ))} + + + + Existing inside-label variations + + + } + variant="secondary" + /> + } + label=" Secondary End" + labelVariant="inside" + placeholder="Placeholder" + variant="secondary" + /> + + + + ); }; diff --git a/packages/web/src/controls/__tests__/TextInput.test.tsx b/packages/web/src/controls/__tests__/TextInput.test.tsx index fe37548557..e884f22e5e 100644 --- a/packages/web/src/controls/__tests__/TextInput.test.tsx +++ b/packages/web/src/controls/__tests__/TextInput.test.tsx @@ -263,7 +263,7 @@ describe('TextInput', () => { ); const inputArea = screen.getByTestId('input-interactable-area'); - expect(inputArea).toHaveStyle('--border-color-focused: var(--color-bgPrimary)'); + expect(inputArea).toHaveStyle('--border-color-focused: var(--color-bgInverse)'); expect(inputArea).toHaveStyle('--border-width-focused: var(--borderWidth-200)'); }); From 1957750aef0d24c21c1ef7cd6d5e766c25f855dc Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 08:29:32 -0300 Subject: [PATCH 02/10] fix: enforce alpha select value type constraints Add runtime guards for Alpha Select value shape based on selection mode so invalid single/multi value combinations fail fast with clear errors. Co-authored-by: Cursor --- packages/web/src/alpha/select/Select.tsx | 7 ++++ .../alpha/select/__tests__/Select.test.tsx | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/web/src/alpha/select/Select.tsx b/packages/web/src/alpha/select/Select.tsx index 91b6fda3b6..5ee1191547 100644 --- a/packages/web/src/alpha/select/Select.tsx +++ b/packages/web/src/alpha/select/Select.tsx @@ -120,6 +120,13 @@ const SelectBase = memo( classNames, testID, } = mergedProps; + + if (type === 'multi' && !Array.isArray(value)) + throw Error('Select component value must be an array when "type" is "multi".'); + + if (type === 'single' && Array.isArray(value)) + throw Error('Select component value must not be an array when "type" is "single".'); + const hasMounted = useHasMounted(); const [openInternal, setOpenInternal] = useState(defaultOpen ?? false); const open = openProp ?? openInternal; diff --git a/packages/web/src/alpha/select/__tests__/Select.test.tsx b/packages/web/src/alpha/select/__tests__/Select.test.tsx index 29b82019b6..f4c9c1abd8 100644 --- a/packages/web/src/alpha/select/__tests__/Select.test.tsx +++ b/packages/web/src/alpha/select/__tests__/Select.test.tsx @@ -524,6 +524,46 @@ describe('Select', () => { consoleError.mockRestore(); }); + + it('throws error when multi select receives a non-array value', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => { + render( + + + , + ); + }).toThrow('Select component value must not be an array when "type" is "single".'); + + consoleError.mockRestore(); + }); }); describe('Keyboard Navigation', () => { From 15b5f7405c79ac5cba19e914b5fdd3911674ebf9 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 09:31:06 -0300 Subject: [PATCH 03/10] fix: disable text input focus styles for readonly and disabled states Prevent readonly and disabled TextInput from entering focused visual state so borders do not thicken or change color when interaction is blocked. Co-authored-by: Cursor --- packages/mobile/src/controls/TextInput.tsx | 31 +++++++++------ .../src/controls/__tests__/TextInput.test.tsx | 38 +++++++++++++++++++ packages/web/src/controls/TextInput.tsx | 25 ++++++++---- .../src/controls/__tests__/TextInput.test.tsx | 22 +++++++++++ 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/packages/mobile/src/controls/TextInput.tsx b/packages/mobile/src/controls/TextInput.tsx index 8575012cfd..183ae5107a 100644 --- a/packages/mobile/src/controls/TextInput.tsx +++ b/packages/mobile/src/controls/TextInput.tsx @@ -164,11 +164,20 @@ export const TextInput = memo( } = mergedProps; const theme = useTheme(); const [focused, setFocused] = useState(false); - const focusedVariant = variant; + const isReadOnly = !!editableInputProps.readOnly; + const disableFocusedStyle = disabled || isReadOnly; + const shouldShowFocusedState = focused && !disableFocusedStyle; + const focusedVariant = useMemo(() => { + if (variant === 'foreground' || variant === 'foregroundMuted') { + return 'foreground'; + } + + return variant; + }, [variant]); const internalRef = useRef(null); const refs = useMergeRefs(ref, internalRef); const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle( - focused, + shouldShowFocusedState, variant, focusedVariant, bordered, @@ -180,7 +189,7 @@ export const TextInput = memo( ...editableInputProps, onFocus: (e: NativeSyntheticEvent) => { editableInputProps?.onFocus?.(e); - setFocused(true); + setFocused(!disableFocusedStyle); }, onBlur: (e: NativeSyntheticEvent) => { editableInputProps?.onBlur?.(e); @@ -189,11 +198,11 @@ export const TextInput = memo( }; const handleNodePress = useCallback(() => { - if (!editableInputAddonProps.readOnly) { - setFocused(true); - internalRef.current?.focus(); - } - }, [setFocused, internalRef, editableInputAddonProps.readOnly]); + if (disableFocusedStyle) return; + + setFocused(true); + internalRef.current?.focus(); + }, [disableFocusedStyle]); const hasLabel = useMemo(() => !!label || !!labelNode, [label, labelNode]); @@ -235,11 +244,11 @@ export const TextInput = memo( }, [start]); const readOnlyInputBackground = useMemo(() => { - if (!disabled && editableInputAddonProps.readOnly) { + if (!disabled && isReadOnly) { return 'bgSecondaryWash'; } return undefined; - }, [disabled, editableInputAddonProps.readOnly]); + }, [disabled, isReadOnly]); return ( ) } - focused={focused} + focused={shouldShowFocusedState} focusedBorderWidth={focusedBorderWidth} helperTextNode={ !!helperText && diff --git a/packages/mobile/src/controls/__tests__/TextInput.test.tsx b/packages/mobile/src/controls/__tests__/TextInput.test.tsx index 28d582204a..8d5953c35e 100644 --- a/packages/mobile/src/controls/__tests__/TextInput.test.tsx +++ b/packages/mobile/src/controls/__tests__/TextInput.test.tsx @@ -336,6 +336,44 @@ describe('TextInput', () => { ); }); + it('uses inverse focused border color for foreground-muted variant', () => { + const testID = 'input-testid'; + render( + + + , + ); + + fireEvent(screen.getByTestId(testID), 'focus'); + const focusedBorderOverlayStyle = getFocusedBorderOverlayStyle(); + expect(focusedBorderOverlayStyle).toEqual( + expect.objectContaining({ borderColor: defaultTheme.lightColor.bgInverse }), + ); + }); + + it('does not show focused border styling when readOnly', () => { + const testID = 'input-readonly-testid'; + render( + + + , + ); + + fireEvent(screen.getByTestId(testID), 'focus'); + const focusedBorderOverlayStyle = getFocusedBorderOverlayStyle(); + expect(focusedBorderOverlayStyle).toBeUndefined(); + }); + it('renders label outside by default', () => { const labelTestID = 'label-test'; render( diff --git a/packages/web/src/controls/TextInput.tsx b/packages/web/src/controls/TextInput.tsx index 1c713a2336..1996c30844 100644 --- a/packages/web/src/controls/TextInput.tsx +++ b/packages/web/src/controls/TextInput.tsx @@ -175,7 +175,10 @@ export const TextInput = memo( inputBackground, ...nativeInputRestProps } = mergedProps; + const isReadOnly = !!nativeInputRestProps.readOnly; + const disableFocusedStyle = disabled || isReadOnly; const [focused, setFocused] = useState(false); + const shouldShowFocusedState = focused && !disableFocusedStyle; const focusedVariant = variant; const internalRef = useRef(); const refs = useMergeRefs(ref, internalRef); @@ -197,11 +200,13 @@ export const TextInput = memo( const handleOnFocus = useCallback( (e: React.FocusEvent) => { - setFocused(true); + setFocused(!disableFocusedStyle); onFocus?.(e); - internalRef.current?.addEventListener('wheel', preventWheelScroll); + if (!disableFocusedStyle) { + internalRef.current?.addEventListener('wheel', preventWheelScroll); + } }, - [onFocus, internalRef, preventWheelScroll], + [disableFocusedStyle, onFocus, internalRef, preventWheelScroll], ); const handleOnBlur = useCallback( @@ -214,9 +219,11 @@ export const TextInput = memo( ); const handleNodePress = useCallback(() => { + if (disableFocusedStyle) return; + setFocused(true); internalRef.current?.focus(); - }, [setFocused, internalRef]); + }, [disableFocusedStyle]); // Define a distinct read-only style to differentiate it from the disabled style. const readOnlyInputBackground = useMemo(() => { @@ -290,11 +297,15 @@ export const TextInput = memo( ]); return ( - + ) } - focused={focused} + focused={shouldShowFocusedState} focusedBorderWidth={focusedBorderWidth} height={height} helperTextNode={ diff --git a/packages/web/src/controls/__tests__/TextInput.test.tsx b/packages/web/src/controls/__tests__/TextInput.test.tsx index e884f22e5e..e4b3276736 100644 --- a/packages/web/src/controls/__tests__/TextInput.test.tsx +++ b/packages/web/src/controls/__tests__/TextInput.test.tsx @@ -267,6 +267,28 @@ describe('TextInput', () => { expect(inputArea).toHaveStyle('--border-width-focused: var(--borderWidth-200)'); }); + it('disables focus border styling when readOnly', () => { + render( + + + , + ); + + const inputArea = screen.getByTestId('input-interactable-area'); + expect(inputArea).toHaveStyle('--border-color-focused: transparent'); + }); + + it('disables focus border styling when disabled', () => { + render( + + + , + ); + + const inputArea = screen.getByTestId('input-interactable-area'); + expect(inputArea).toHaveStyle('--border-color-focused: transparent'); + }); + it('focuses input when start node is pressed', () => { const onFocus = jest.fn(); const startNodeText = 'Start'; From d03eae73a6421a67ac52d84318a3d6934fe32cbb Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 11:24:32 -0300 Subject: [PATCH 04/10] chore: fix some issues at android --- .../src/alpha/select/DefaultSelectControl.tsx | 4 +- .../dates/__stories__/DateInput.stories.tsx | 46 ++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index 5f8f4bc4ba..dcfa833366 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -21,6 +21,7 @@ import { isSelectOptionGroup } from './Select'; const LABEL_VARIANT_INSIDE_HEIGHT = 24; const COMPACT_HEIGHT = 40; const DEFAULT_HEIGHT = 56; +const selectControlBorderRadius = 400; const variantColor: Record = { foreground: 'fg', @@ -193,7 +194,7 @@ export const DefaultSelectControlComponent = memo( style={styles?.controlLabelNode} > { - const [date, setDate] = useState(today); +const DateInputExample = ({ + initialDate = today, + ...props +}: Omit< + React.ComponentProps, + 'date' | 'onChangeDate' | 'error' | 'onErrorDate' +> & { + initialDate?: Date | null; +}) => { + const [date, setDate] = useState(initialDate); const [error, setError] = useState(null); - const props = { date, onChangeDate: setDate, error, onErrorDate: setError }; + + return ( + + ); +}; + +export const Examples = () => { return ( - + - + - + - - - - + + + @@ -54,17 +67,16 @@ export const Examples = () => { } /> - } placeholder="Hello world" start={} /> - - - - + + + + From d7494f14c8e133d226cd33791cefb80390eea7cb Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 13:35:49 -0300 Subject: [PATCH 05/10] chore: final fixes --- .../src/alpha/select/DefaultSelectControl.tsx | 14 ++++++++++---- packages/mobile/src/controls/Select.tsx | 9 ++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index dcfa833366..0ce1266471 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -158,12 +158,18 @@ export const DefaultSelectControlComponent = memo( value, ]); - // Prop value doesn't have default value because it affects the color of the - // animated caret - const focusedVariant = useInputVariant(!!open, variant ?? 'foregroundMuted'); + const resolvedVariant = variant ?? 'foregroundMuted'; + const defaultFocusedVariant = useInputVariant(!!open, resolvedVariant); + const focusedVariant = useMemo(() => { + if (resolvedVariant === 'foreground' || resolvedVariant === 'foregroundMuted') { + return 'foreground'; + } + + return defaultFocusedVariant; + }, [defaultFocusedVariant, resolvedVariant]); const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle( !!open, - variant ?? 'foregroundMuted', + resolvedVariant, focusedVariant, bordered, borderWidth, diff --git a/packages/mobile/src/controls/Select.tsx b/packages/mobile/src/controls/Select.tsx index 00a1d3c41b..7cce4db1ab 100644 --- a/packages/mobile/src/controls/Select.tsx +++ b/packages/mobile/src/controls/Select.tsx @@ -98,7 +98,14 @@ export const Select = memo( const [isSelectTrayOpen, toggleSelectTray] = useState(false); const toggleSelectTrayOff = useCallback(() => toggleSelectTray(false), [toggleSelectTray]); const toggleSelectTrayOn = useCallback(() => toggleSelectTray(true), [toggleSelectTray]); - const focusedVariant = useInputVariant(!!isSelectTrayOpen, variant); + const defaultFocusedVariant = useInputVariant(!!isSelectTrayOpen, variant); + const focusedVariant = useMemo(() => { + if (variant === 'foreground' || variant === 'foregroundMuted') { + return 'foreground'; + } + + return defaultFocusedVariant; + }, [defaultFocusedVariant, variant]); const sanitizedValue = defaultValue === '' ? undefined : defaultValue; const internalRef = useRef(null); const refs = useMergeRefs(ref, internalRef); From 3b19331062b6a09dca8a6785d4d2d762bacec2bc Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Tue, 12 May 2026 16:44:39 -0300 Subject: [PATCH 06/10] feat: consolidate input and dropdown updates into one signed commit Unifies the input and dropdown design updates into a single signed commit so the history is easier to audit and verify. Co-authored-by: Cursor --- packages/mobile/src/controls/InputLabel.tsx | 2 +- packages/mobile/src/controls/InputStack.tsx | 20 +++- packages/mobile/src/controls/NativeInput.tsx | 6 +- packages/mobile/src/controls/Select.tsx | 4 +- packages/mobile/src/controls/TextInput.tsx | 9 +- .../__stories__/TextInput.stories.tsx | 70 +++++------- .../src/hooks/useInputBorderAnimation.tsx | 2 +- .../src/alpha/select/DefaultSelectControl.tsx | 6 +- packages/web/src/controls/InputLabel.tsx | 2 +- packages/web/src/controls/InputStack.tsx | 27 ++++- packages/web/src/controls/NativeInput.tsx | 6 +- packages/web/src/controls/NativeTextArea.tsx | 2 +- packages/web/src/controls/SelectStack.tsx | 3 + packages/web/src/controls/SelectTrigger.tsx | 3 +- packages/web/src/controls/TextInput.tsx | 15 +-- .../__stories__/TextInput.stories.tsx | 102 ++++++++++++------ .../src/controls/__tests__/TextInput.test.tsx | 2 +- 17 files changed, 171 insertions(+), 110 deletions(-) diff --git a/packages/mobile/src/controls/InputLabel.tsx b/packages/mobile/src/controls/InputLabel.tsx index e09cf160d4..0502768076 100644 --- a/packages/mobile/src/controls/InputLabel.tsx +++ b/packages/mobile/src/controls/InputLabel.tsx @@ -5,7 +5,7 @@ import { Text } from '../typography/Text'; import type { HelperTextProps } from './HelperText'; export const InputLabel = memo(function InputLabel({ - color = 'fg', + color = 'fgMuted', font = 'label1', ...props }: HelperTextProps) { diff --git a/packages/mobile/src/controls/InputStack.tsx b/packages/mobile/src/controls/InputStack.tsx index a0cd2e0a50..d1dd992280 100644 --- a/packages/mobile/src/controls/InputStack.tsx +++ b/packages/mobile/src/controls/InputStack.tsx @@ -95,7 +95,7 @@ const variantColorMap: Record = { positive: 'fgPositive', negative: 'fgNegative', foreground: 'fg', - foregroundMuted: 'fgMuted', + foregroundMuted: 'bgLine', secondary: 'bgSecondary', }; @@ -127,6 +127,17 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) { } = mergedProps; const theme = useTheme(); const [inputAreaSize, onInputAreaLayout] = useLayout(); + const resolvedInputBackground = useMemo(() => { + if (variant === 'secondary') { + return 'bgSecondary'; + } + + if (variant === 'negative' && inputBackground === 'bg') { + return 'bgNegativeWash'; + } + + return inputBackground; + }, [variant, inputBackground]); const inputAreaStyle: ViewStyle = useMemo(() => { const inputBorderRadius: ViewStyle = { @@ -149,13 +160,12 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) { variant === 'secondary' ? 'transparent' : theme.color[ - variant === 'foregroundMuted' || !variant ? 'bgLineHeavy' : variantColorMap[variant] + variant === 'foregroundMuted' || !variant ? 'bgLine' : variantColorMap[variant] ], borderWidth: theme.borderWidth[borderWidth], flexDirection: 'row', flexGrow: 1, - backgroundColor: - variant === 'secondary' ? theme.color.bgSecondary : theme.color[inputBackground], + backgroundColor: theme.color[resolvedInputBackground], borderRadius: theme.borderRadius[borderRadius], overflow: 'hidden', ...inputBorderRadius, @@ -168,7 +178,7 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) { theme.borderWidth, theme.borderRadius, borderWidth, - inputBackground, + resolvedInputBackground, borderRadius, ]); diff --git a/packages/mobile/src/controls/NativeInput.tsx b/packages/mobile/src/controls/NativeInput.tsx index 16473d7a97..a525bb0f8e 100644 --- a/packages/mobile/src/controls/NativeInput.tsx +++ b/packages/mobile/src/controls/NativeInput.tsx @@ -11,6 +11,8 @@ import type { TextBaseProps } from '../typography/Text'; import type { TextInputBaseProps } from './TextInput'; +const defaultNativeInputBorderRadius: ThemeVars.BorderRadius = 400; + export type NativeInputProps = { /** * Text Align Input @@ -90,13 +92,15 @@ export const NativeInput = memo( ...containerSpacing, ...(!disabled && editableInputAddonProps.readOnly && { - backgroundColor: theme.color.bgSecondary, + backgroundColor: theme.color.bgSecondaryWash, + borderRadius: theme.borderRadius[defaultNativeInputBorderRadius], }), }; }, [ containerSpacing, theme.space, theme.color, + theme.borderRadius, compact, editableInputAddonProps.readOnly, disabled, diff --git a/packages/mobile/src/controls/Select.tsx b/packages/mobile/src/controls/Select.tsx index 5bba5dbcd8..00a1d3c41b 100644 --- a/packages/mobile/src/controls/Select.tsx +++ b/packages/mobile/src/controls/Select.tsx @@ -29,6 +29,7 @@ import { useSelect } from './useSelect'; const selectTriggerMinHeight = 56; const selectTriggerCompactMinHeight = 40; const selectTriggerInsideLabelMinHeight = 24; +const selectTriggerBorderRadius = 400; const variantColorMap: Record = { primary: 'fgPrimary', @@ -180,6 +181,7 @@ export const Select = memo( = { secondary: 'bgSecondary', }; +const defaultTextInputBorderRadius: InputStackBaseProps['borderRadius'] = 400; + export const TextInput = memo( forwardRef((_props: TextInputProps, ref: ForwardedRef) => { const mergedProps = useComponentConfig('TextInput', _props); @@ -153,7 +154,7 @@ export const TextInput = memo( compact, suffix = '', accessibilityLabel, - borderRadius, + borderRadius = defaultTextInputBorderRadius, enableColorSurge = false, helperTextErrorIconAccessibilityLabel = 'error', bordered = true, @@ -165,7 +166,7 @@ export const TextInput = memo( } = mergedProps; const theme = useTheme(); const [focused, setFocused] = useState(false); - const focusedVariant = useInputVariant(focused, variant); + const focusedVariant = variant; const internalRef = useRef(null); const refs = useMergeRefs(ref, internalRef); const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle( @@ -237,7 +238,7 @@ export const TextInput = memo( const readOnlyInputBackground = useMemo(() => { if (!disabled && editableInputAddonProps.readOnly) { - return 'bgSecondary'; + return 'bgSecondaryWash'; } return undefined; }, [disabled, editableInputAddonProps.readOnly]); diff --git a/packages/mobile/src/controls/__stories__/TextInput.stories.tsx b/packages/mobile/src/controls/__stories__/TextInput.stories.tsx index 32fe0b8e0c..f80e656334 100644 --- a/packages/mobile/src/controls/__stories__/TextInput.stories.tsx +++ b/packages/mobile/src/controls/__stories__/TextInput.stories.tsx @@ -436,50 +436,32 @@ const InputScreen = () => { placeholder="Placeholder" /> - - - - - } - /> - - - } - label="Username" - labelVariant="inside" - placeholder="john.doe@coinbase.com" - /> - - - } - label="Username" - labelVariant="inside" - placeholder="john.doe@coinbase.com" - start={} - /> - - - - - - + + + + + Figma inside-label states + + + + Default + + + + Filled + + + + Read only + + + + Negative + + + + + = { positive: 'bgPositive', negative: 'bgNegative', foreground: 'bgInverse', - foregroundMuted: 'bgLineHeavy', + foregroundMuted: 'bgLine', secondary: 'bgSecondary', }; diff --git a/packages/web/src/alpha/select/DefaultSelectControl.tsx b/packages/web/src/alpha/select/DefaultSelectControl.tsx index 2c93074c3f..3f7a09a7ab 100644 --- a/packages/web/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/web/src/alpha/select/DefaultSelectControl.tsx @@ -26,6 +26,7 @@ import { const LABEL_VARIANT_INSIDE_HEIGHT = 32; const COMPACT_HEIGHT = 40; const DEFAULT_HEIGHT = 56; +const selectControlBorderRadius = 400; const noFocusOutlineCss = css` &:focus, @@ -246,7 +247,7 @@ const DefaultSelectControlComponent = memo( setOpen((s) => !s)} tabIndex={-1}> {label} @@ -491,6 +492,7 @@ const DefaultSelectControlComponent = memo( ; export const InputLabel = memo(function InputLabel({ - color = 'fg', + color = 'fgMuted', disabled = false, font = 'label1', ...props diff --git a/packages/web/src/controls/InputStack.tsx b/packages/web/src/controls/InputStack.tsx index 3081d0c944..2dfc81f3f7 100644 --- a/packages/web/src/controls/InputStack.tsx +++ b/packages/web/src/controls/InputStack.tsx @@ -66,7 +66,7 @@ const variantColorMap: Record = { positive: 'bgPositive', negative: 'bgNegative', foreground: 'bgInverse', - foregroundMuted: 'bgLineHeavy', + foregroundMuted: 'bgLine', secondary: 'bgSecondary', }; @@ -204,12 +204,19 @@ export const InputStack = memo( return 'transparent'; } + if (variant === 'secondary') { + return 'transparent'; + } + + if (variant === 'foreground' || variant === 'foregroundMuted') { + return 'var(--color-bgInverse)'; + } + if (variant === 'positive' || variant === 'negative') { return `var(--color-${variantColorMap[variant]})`; } - // all variants except for positive/negative receive the primary focus color - return 'var(--color-bgPrimary)'; + return `var(--color-${variantColorMap[variant]})`; }, [disableFocusedStyle, variant]); const borderColorUnfocused = useMemo(() => { @@ -229,6 +236,18 @@ export const InputStack = memo( }; }, [borderColorUnfocused, borderColorFocused, focusedBorderWidth, inputBorderRadius]); + const resolvedInputBackground = useMemo(() => { + if (variant === 'secondary') { + return 'bgSecondary'; + } + + if (variant === 'negative' && inputBackground === 'bg') { + return 'bgNegativeWash'; + } + + return inputBackground; + }, [variant, inputBackground]); + return ( (function SelectStack( { @@ -86,6 +88,7 @@ export const SelectStack = memo( } labelVariant={labelVariant} variant={variant} + borderRadius={defaultSelectBorderRadius} width="100%" /> ); diff --git a/packages/web/src/controls/SelectTrigger.tsx b/packages/web/src/controls/SelectTrigger.tsx index 219aefd3ad..03625d57a0 100644 --- a/packages/web/src/controls/SelectTrigger.tsx +++ b/packages/web/src/controls/SelectTrigger.tsx @@ -18,6 +18,7 @@ import { SelectStack } from './SelectStack'; const selectTriggerMinHeight = 56; const selectTriggerCompactMinHeight = 40; const selectTriggerInsideLabelMinHeight = 62; +const selectTriggerBorderRadius = 400; export type SelectTriggerProps = Omit< SelectBaseProps, @@ -129,7 +130,7 @@ export const SelectTrigger = memo( ) : null} { - return useMemo( - () => (focused && variant !== 'positive' && variant !== 'negative' ? 'primary' : variant), - [focused, variant], - ); -}; - const variantColorMap: Record = { primary: 'fgPrimary', positive: 'fgPositive', @@ -148,6 +141,8 @@ const variantColorMap: Record = { secondary: 'fg', }; +const defaultTextInputBorderRadius: InputStackBaseProps['borderRadius'] = 400; + export const TextInput = memo( forwardRef(function TextInput(_props: TextInputProps, ref: React.ForwardedRef) { const mergedProps = useComponentConfig('TextInput', _props); @@ -170,7 +165,7 @@ export const TextInput = memo( suffix = '', onFocus, onBlur, - borderRadius = 200, + borderRadius = defaultTextInputBorderRadius, height, inputNode, bordered = true, @@ -183,7 +178,7 @@ export const TextInput = memo( ...nativeInputRestProps } = mergedProps; const [focused, setFocused] = useState(false); - const focusedVariant = useInputVariant(focused, variant); + const focusedVariant = variant; const internalRef = useRef(); const refs = useMergeRefs(ref, internalRef); @@ -228,7 +223,7 @@ export const TextInput = memo( // Define a distinct read-only style to differentiate it from the disabled style. const readOnlyInputBackground = useMemo(() => { if (!disabled && nativeInputRestProps.readOnly) { - return 'bgSecondary'; + return 'bgSecondaryWash'; } return undefined; }, [disabled, nativeInputRestProps.readOnly]); diff --git a/packages/web/src/controls/__stories__/TextInput.stories.tsx b/packages/web/src/controls/__stories__/TextInput.stories.tsx index df28ee49c4..de420bcc68 100644 --- a/packages/web/src/controls/__stories__/TextInput.stories.tsx +++ b/packages/web/src/controls/__stories__/TextInput.stories.tsx @@ -62,38 +62,78 @@ export const Basic = function Basic() { }; export const InsideLabel = function InsideLabel() { + const figmaStates = [ + { + title: 'Default', + node: , + }, + { + title: 'Filled', + node: ( + undefined} value="Text" /> + ), + }, + { + title: 'Read only', + node: , + }, + { + title: 'Negative', + node: , + }, + ] as const; + return ( - - - } - variant="secondary" - /> - } - label=" Secondary End" - labelVariant="inside" - placeholder="Placeholder" - variant="secondary" - /> - - - + + + + Figma inside-label states + + + {figmaStates.map((state) => ( + + + {state.title} + + {state.node} + + ))} + + + + Existing inside-label variations + + + } + variant="secondary" + /> + } + label=" Secondary End" + labelVariant="inside" + placeholder="Placeholder" + variant="secondary" + /> + + + + ); }; diff --git a/packages/web/src/controls/__tests__/TextInput.test.tsx b/packages/web/src/controls/__tests__/TextInput.test.tsx index e4a12169c1..19e64b59c0 100644 --- a/packages/web/src/controls/__tests__/TextInput.test.tsx +++ b/packages/web/src/controls/__tests__/TextInput.test.tsx @@ -280,7 +280,7 @@ describe('TextInput', () => { ); const inputArea = screen.getByTestId('input-interactable-area'); - expect(inputArea).toHaveStyle('--border-color-focused: var(--color-bgPrimary)'); + expect(inputArea).toHaveStyle('--border-color-focused: var(--color-bgInverse)'); expect(inputArea).toHaveStyle('--border-width-focused: var(--borderWidth-200)'); }); From 85392238bb6648e854e01846aff9579c83df6276 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 08:29:32 -0300 Subject: [PATCH 07/10] fix: enforce alpha select value type constraints Add runtime guards for Alpha Select value shape based on selection mode so invalid single/multi value combinations fail fast with clear errors. Co-authored-by: Cursor --- packages/web/src/alpha/select/Select.tsx | 7 ++++ .../alpha/select/__tests__/Select.test.tsx | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/web/src/alpha/select/Select.tsx b/packages/web/src/alpha/select/Select.tsx index 91b6fda3b6..5ee1191547 100644 --- a/packages/web/src/alpha/select/Select.tsx +++ b/packages/web/src/alpha/select/Select.tsx @@ -120,6 +120,13 @@ const SelectBase = memo( classNames, testID, } = mergedProps; + + if (type === 'multi' && !Array.isArray(value)) + throw Error('Select component value must be an array when "type" is "multi".'); + + if (type === 'single' && Array.isArray(value)) + throw Error('Select component value must not be an array when "type" is "single".'); + const hasMounted = useHasMounted(); const [openInternal, setOpenInternal] = useState(defaultOpen ?? false); const open = openProp ?? openInternal; diff --git a/packages/web/src/alpha/select/__tests__/Select.test.tsx b/packages/web/src/alpha/select/__tests__/Select.test.tsx index 29b82019b6..f4c9c1abd8 100644 --- a/packages/web/src/alpha/select/__tests__/Select.test.tsx +++ b/packages/web/src/alpha/select/__tests__/Select.test.tsx @@ -524,6 +524,46 @@ describe('Select', () => { consoleError.mockRestore(); }); + + it('throws error when multi select receives a non-array value', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => { + render( + + + , + ); + }).toThrow('Select component value must not be an array when "type" is "single".'); + + consoleError.mockRestore(); + }); }); describe('Keyboard Navigation', () => { From d41d1591bc1b319ce10763bdcc46ee1b4870af7c Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 09:31:06 -0300 Subject: [PATCH 08/10] fix: disable text input focus styles for readonly and disabled states Prevent readonly and disabled TextInput from entering focused visual state so borders do not thicken or change color when interaction is blocked. Co-authored-by: Cursor --- packages/mobile/src/controls/TextInput.tsx | 31 +++++++++------ .../src/controls/__tests__/TextInput.test.tsx | 38 +++++++++++++++++++ packages/web/src/controls/TextInput.tsx | 25 ++++++++---- .../src/controls/__tests__/TextInput.test.tsx | 22 +++++++++++ 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/packages/mobile/src/controls/TextInput.tsx b/packages/mobile/src/controls/TextInput.tsx index d88f00581a..9c46ae8d08 100644 --- a/packages/mobile/src/controls/TextInput.tsx +++ b/packages/mobile/src/controls/TextInput.tsx @@ -166,11 +166,20 @@ export const TextInput = memo( } = mergedProps; const theme = useTheme(); const [focused, setFocused] = useState(false); - const focusedVariant = variant; + const isReadOnly = !!editableInputProps.readOnly; + const disableFocusedStyle = disabled || isReadOnly; + const shouldShowFocusedState = focused && !disableFocusedStyle; + const focusedVariant = useMemo(() => { + if (variant === 'foreground' || variant === 'foregroundMuted') { + return 'foreground'; + } + + return variant; + }, [variant]); const internalRef = useRef(null); const refs = useMergeRefs(ref, internalRef); const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle( - focused, + shouldShowFocusedState, variant, focusedVariant, bordered, @@ -182,7 +191,7 @@ export const TextInput = memo( ...editableInputProps, onFocus: (e: NativeSyntheticEvent) => { editableInputProps?.onFocus?.(e); - setFocused(true); + setFocused(!disableFocusedStyle); }, onBlur: (e: NativeSyntheticEvent) => { editableInputProps?.onBlur?.(e); @@ -191,11 +200,11 @@ export const TextInput = memo( }; const handleNodePress = useCallback(() => { - if (!editableInputAddonProps.readOnly) { - setFocused(true); - internalRef.current?.focus(); - } - }, [setFocused, internalRef, editableInputAddonProps.readOnly]); + if (disableFocusedStyle) return; + + setFocused(true); + internalRef.current?.focus(); + }, [disableFocusedStyle]); const hasLabel = useMemo(() => !!label || !!labelNode, [label, labelNode]); @@ -237,11 +246,11 @@ export const TextInput = memo( }, [start]); const readOnlyInputBackground = useMemo(() => { - if (!disabled && editableInputAddonProps.readOnly) { + if (!disabled && isReadOnly) { return 'bgSecondaryWash'; } return undefined; - }, [disabled, editableInputAddonProps.readOnly]); + }, [disabled, isReadOnly]); return ( ) } - focused={focused} + focused={shouldShowFocusedState} focusedBorderWidth={focusedBorderWidth} helperTextNode={ !!helperText && diff --git a/packages/mobile/src/controls/__tests__/TextInput.test.tsx b/packages/mobile/src/controls/__tests__/TextInput.test.tsx index 27d67eab28..24a763deec 100644 --- a/packages/mobile/src/controls/__tests__/TextInput.test.tsx +++ b/packages/mobile/src/controls/__tests__/TextInput.test.tsx @@ -356,6 +356,44 @@ describe('TextInput', () => { ); }); + it('uses inverse focused border color for foreground-muted variant', () => { + const testID = 'input-testid'; + render( + + + , + ); + + fireEvent(screen.getByTestId(testID), 'focus'); + const focusedBorderOverlayStyle = getFocusedBorderOverlayStyle(); + expect(focusedBorderOverlayStyle).toEqual( + expect.objectContaining({ borderColor: defaultTheme.lightColor.bgInverse }), + ); + }); + + it('does not show focused border styling when readOnly', () => { + const testID = 'input-readonly-testid'; + render( + + + , + ); + + fireEvent(screen.getByTestId(testID), 'focus'); + const focusedBorderOverlayStyle = getFocusedBorderOverlayStyle(); + expect(focusedBorderOverlayStyle).toBeUndefined(); + }); + it('renders label outside by default', () => { const labelTestID = 'label-test'; render( diff --git a/packages/web/src/controls/TextInput.tsx b/packages/web/src/controls/TextInput.tsx index e463598147..3af965fd05 100644 --- a/packages/web/src/controls/TextInput.tsx +++ b/packages/web/src/controls/TextInput.tsx @@ -177,7 +177,10 @@ export const TextInput = memo( inputBackground, ...nativeInputRestProps } = mergedProps; + const isReadOnly = !!nativeInputRestProps.readOnly; + const disableFocusedStyle = disabled || isReadOnly; const [focused, setFocused] = useState(false); + const shouldShowFocusedState = focused && !disableFocusedStyle; const focusedVariant = variant; const internalRef = useRef(); const refs = useMergeRefs(ref, internalRef); @@ -199,11 +202,13 @@ export const TextInput = memo( const handleOnFocus = useCallback( (e: React.FocusEvent) => { - setFocused(true); + setFocused(!disableFocusedStyle); onFocus?.(e); - internalRef.current?.addEventListener('wheel', preventWheelScroll); + if (!disableFocusedStyle) { + internalRef.current?.addEventListener('wheel', preventWheelScroll); + } }, - [onFocus, internalRef, preventWheelScroll], + [disableFocusedStyle, onFocus, internalRef, preventWheelScroll], ); const handleOnBlur = useCallback( @@ -216,9 +221,11 @@ export const TextInput = memo( ); const handleNodePress = useCallback(() => { + if (disableFocusedStyle) return; + setFocused(true); internalRef.current?.focus(); - }, [setFocused, internalRef]); + }, [disableFocusedStyle]); // Define a distinct read-only style to differentiate it from the disabled style. const readOnlyInputBackground = useMemo(() => { @@ -292,11 +299,15 @@ export const TextInput = memo( ]); return ( - + ) } - focused={focused} + focused={shouldShowFocusedState} focusedBorderWidth={focusedBorderWidth} height={height} helperTextNode={ diff --git a/packages/web/src/controls/__tests__/TextInput.test.tsx b/packages/web/src/controls/__tests__/TextInput.test.tsx index 19e64b59c0..752d4d5699 100644 --- a/packages/web/src/controls/__tests__/TextInput.test.tsx +++ b/packages/web/src/controls/__tests__/TextInput.test.tsx @@ -284,6 +284,28 @@ describe('TextInput', () => { expect(inputArea).toHaveStyle('--border-width-focused: var(--borderWidth-200)'); }); + it('disables focus border styling when readOnly', () => { + render( + + + , + ); + + const inputArea = screen.getByTestId('input-interactable-area'); + expect(inputArea).toHaveStyle('--border-color-focused: transparent'); + }); + + it('disables focus border styling when disabled', () => { + render( + + + , + ); + + const inputArea = screen.getByTestId('input-interactable-area'); + expect(inputArea).toHaveStyle('--border-color-focused: transparent'); + }); + it('focuses input when start node is pressed', () => { const onFocus = jest.fn(); const startNodeText = 'Start'; From 89d79be09e97d768a4746060ae6cf6fe2c57abf9 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 11:24:32 -0300 Subject: [PATCH 09/10] chore: fix some issues at android --- .../src/alpha/select/DefaultSelectControl.tsx | 4 +- .../dates/__stories__/DateInput.stories.tsx | 46 ++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index 5f8f4bc4ba..dcfa833366 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -21,6 +21,7 @@ import { isSelectOptionGroup } from './Select'; const LABEL_VARIANT_INSIDE_HEIGHT = 24; const COMPACT_HEIGHT = 40; const DEFAULT_HEIGHT = 56; +const selectControlBorderRadius = 400; const variantColor: Record = { foreground: 'fg', @@ -193,7 +194,7 @@ export const DefaultSelectControlComponent = memo( style={styles?.controlLabelNode} > { - const [date, setDate] = useState(today); +const DateInputExample = ({ + initialDate = today, + ...props +}: Omit< + React.ComponentProps, + 'date' | 'onChangeDate' | 'error' | 'onErrorDate' +> & { + initialDate?: Date | null; +}) => { + const [date, setDate] = useState(initialDate); const [error, setError] = useState(null); - const props = { date, onChangeDate: setDate, error, onErrorDate: setError }; + + return ( + + ); +}; + +export const Examples = () => { return ( - + - + - + - - - - + + + @@ -54,17 +67,16 @@ export const Examples = () => { } /> - } placeholder="Hello world" start={} /> - - - - + + + + From 4030cf2329e5fbbcb9002dd744aab3ae3e7c9859 Mon Sep 17 00:00:00 2001 From: Luiz Fernando Date: Wed, 13 May 2026 13:35:49 -0300 Subject: [PATCH 10/10] chore: final fixes --- .../src/alpha/select/DefaultSelectControl.tsx | 14 ++++++++++---- packages/mobile/src/controls/Select.tsx | 9 ++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index dcfa833366..0ce1266471 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -158,12 +158,18 @@ export const DefaultSelectControlComponent = memo( value, ]); - // Prop value doesn't have default value because it affects the color of the - // animated caret - const focusedVariant = useInputVariant(!!open, variant ?? 'foregroundMuted'); + const resolvedVariant = variant ?? 'foregroundMuted'; + const defaultFocusedVariant = useInputVariant(!!open, resolvedVariant); + const focusedVariant = useMemo(() => { + if (resolvedVariant === 'foreground' || resolvedVariant === 'foregroundMuted') { + return 'foreground'; + } + + return defaultFocusedVariant; + }, [defaultFocusedVariant, resolvedVariant]); const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle( !!open, - variant ?? 'foregroundMuted', + resolvedVariant, focusedVariant, bordered, borderWidth, diff --git a/packages/mobile/src/controls/Select.tsx b/packages/mobile/src/controls/Select.tsx index 00a1d3c41b..7cce4db1ab 100644 --- a/packages/mobile/src/controls/Select.tsx +++ b/packages/mobile/src/controls/Select.tsx @@ -98,7 +98,14 @@ export const Select = memo( const [isSelectTrayOpen, toggleSelectTray] = useState(false); const toggleSelectTrayOff = useCallback(() => toggleSelectTray(false), [toggleSelectTray]); const toggleSelectTrayOn = useCallback(() => toggleSelectTray(true), [toggleSelectTray]); - const focusedVariant = useInputVariant(!!isSelectTrayOpen, variant); + const defaultFocusedVariant = useInputVariant(!!isSelectTrayOpen, variant); + const focusedVariant = useMemo(() => { + if (variant === 'foreground' || variant === 'foregroundMuted') { + return 'foreground'; + } + + return defaultFocusedVariant; + }, [defaultFocusedVariant, variant]); const sanitizedValue = defaultValue === '' ? undefined : defaultValue; const internalRef = useRef(null); const refs = useMergeRefs(ref, internalRef);