Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions packages/mobile/src/alpha/select/DefaultSelectControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ThemeVars.Color> = {
foreground: 'fg',
Expand Down Expand Up @@ -157,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,
Expand Down Expand Up @@ -193,7 +200,7 @@ export const DefaultSelectControlComponent = memo(
style={styles?.controlLabelNode}
>
<InputLabel
color="fg"
color="fgMuted"
paddingEnd={0}
paddingStart={labelVariant === 'inside' ? 2 : 0}
paddingY={labelVariant === 'inside' || compact ? 0 : 0.5}
Expand Down Expand Up @@ -396,6 +403,7 @@ export const DefaultSelectControlComponent = memo(
return (
<InputStack
borderFocusedStyle={borderFocusedStyle}
borderRadius={selectControlBorderRadius}
borderStyle={borderUnfocusedStyle}
borderWidth={borderWidth}
disabled={disabled}
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/src/controls/InputLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 15 additions & 5 deletions packages/mobile/src/controls/InputStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const variantColorMap: Record<InputVariant, ThemeVars.Color> = {
positive: 'fgPositive',
negative: 'fgNegative',
foreground: 'fg',
foregroundMuted: 'fgMuted',
foregroundMuted: 'bgLine',
secondary: 'bgSecondary',
};

Expand Down Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -168,7 +178,7 @@ export const InputStack = memo(function InputStack(_props: InputStackProps) {
theme.borderWidth,
theme.borderRadius,
borderWidth,
inputBackground,
resolvedInputBackground,
borderRadius,
]);

Expand Down
6 changes: 5 additions & 1 deletion packages/mobile/src/controls/NativeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 11 additions & 2 deletions packages/mobile/src/controls/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useSelect } from './useSelect';
const selectTriggerMinHeight = 56;
const selectTriggerCompactMinHeight = 40;
const selectTriggerInsideLabelMinHeight = 24;
const selectTriggerBorderRadius = 400;

const variantColorMap: Record<InputVariant, ThemeVars.Color> = {
primary: 'fgPrimary',
Expand Down Expand Up @@ -97,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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is deprecated. Is it possible to migrate to use the alpha Select instead.

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);
Expand Down Expand Up @@ -180,6 +188,7 @@ export const Select = memo(
<InputStack
animated
borderFocusedStyle={borderFocusedStyle}
borderRadius={selectTriggerBorderRadius}
borderStyle={borderUnfocusedStyle}
disabled={disabled}
endNode={
Expand Down Expand Up @@ -209,7 +218,7 @@ export const Select = memo(
inputNode={
<HStack
alignItems="center"
borderRadius={200}
borderRadius={selectTriggerBorderRadius}
flexBasis={1}
flexGrow={1}
flexShrink={1}
Expand Down
38 changes: 24 additions & 14 deletions packages/mobile/src/controls/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import type {
ViewStyle,
} from 'react-native';
import type { ThemeVars } from '@coinbase/cds-common/core/theme';
import { useInputVariant } from '@coinbase/cds-common/hooks/useInputVariant';
import { useMergeRefs } from '@coinbase/cds-common/hooks/useMergeRefs';
import type {
SharedAccessibilityProps,
Expand Down Expand Up @@ -133,6 +132,8 @@ const variantColorMap: Record<InputVariant, ThemeVars.Color> = {
secondary: 'bgSecondary',
};

const defaultTextInputBorderRadius: InputStackBaseProps['borderRadius'] = 400;

export const TextInput = memo(
forwardRef((_props: TextInputProps, ref: ForwardedRef<RNTextInput>) => {
const mergedProps = useComponentConfig('TextInput', _props);
Expand All @@ -153,7 +154,7 @@ export const TextInput = memo(
compact,
suffix = '',
accessibilityLabel,
borderRadius,
borderRadius = defaultTextInputBorderRadius,
enableColorSurge = false,
helperTextErrorIconAccessibilityLabel = 'error',
bordered = true,
Expand All @@ -165,11 +166,20 @@ export const TextInput = memo(
} = mergedProps;
const theme = useTheme();
const [focused, setFocused] = useState(false);
const focusedVariant = useInputVariant(focused, 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<RNTextInput>(null);
const refs = useMergeRefs(ref, internalRef);
const { borderFocusedStyle, borderUnfocusedStyle } = useInputBorderStyle(
focused,
shouldShowFocusedState,
variant,
focusedVariant,
bordered,
Expand All @@ -181,7 +191,7 @@ export const TextInput = memo(
...editableInputProps,
onFocus: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
editableInputProps?.onFocus?.(e);
setFocused(true);
setFocused(!disableFocusedStyle);
},
onBlur: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
editableInputProps?.onBlur?.(e);
Expand All @@ -190,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]);

Expand Down Expand Up @@ -236,11 +246,11 @@ export const TextInput = memo(
}, [start]);

const readOnlyInputBackground = useMemo(() => {
if (!disabled && editableInputAddonProps.readOnly) {
return 'bgSecondary';
if (!disabled && isReadOnly) {
return 'bgSecondaryWash';
}
return undefined;
}, [disabled, editableInputAddonProps.readOnly]);
}, [disabled, isReadOnly]);

return (
<InputStack
Expand Down Expand Up @@ -276,7 +286,7 @@ export const TextInput = memo(
</HStack>
)
}
focused={focused}
focused={shouldShowFocusedState}
focusedBorderWidth={focusedBorderWidth}
helperTextNode={
!!helperText &&
Expand Down
70 changes: 26 additions & 44 deletions packages/mobile/src/controls/__stories__/TextInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,50 +436,32 @@ const InputScreen = () => {
placeholder="Placeholder"
/>
</Example>
<Example inline title="TextInput with inside label">
<MockTextInput label="Username" labelVariant="inside" placeholder="john.doe@coinbase.com" />
</Example>
<Example inline title="TextInput with inside label and start node">
<MockTextInput
label="Username"
labelVariant="inside"
placeholder="john.doe@coinbase.com"
start={<InputIconButton transparent name="search" />}
/>
</Example>
<Example inline title="TextInput with inside label and end node">
<MockTextInput
end={<InputIconButton transparent name="lightningBolt" />}
label="Username"
labelVariant="inside"
placeholder="john.doe@coinbase.com"
/>
</Example>
<Example inline title="TextInput with inside label and both nodes">
<MockTextInput
end={<InputIconButton transparent name="close" />}
label="Username"
labelVariant="inside"
placeholder="john.doe@coinbase.com"
start={<InputIconButton transparent name="search" />}
/>
</Example>
<Example inline title="TextInput with inside label and compact">
<MockTextInput
compact
label="Username"
labelVariant="inside"
placeholder="john.doe@coinbase.com"
/>
</Example>
<Example inline title="TextInput with inside label and error state">
<MockTextInput
helperText="Error: Your favorite color is not orange"
label="Error state"
labelVariant="inside"
placeholder="Enter your favorite color"
variant="negative"
/>
<Example inline title="TextInput inside label (Figma + existing variations)">
<Box width="100%">
<VStack gap={2} width="100%">
<Text color="fgMuted" font="caption">
Figma inside-label states
</Text>
<VStack gap={1} width="100%">
<Text color="fgMuted" font="caption">
Default
</Text>
<MockTextInput label="Label" labelVariant="inside" placeholder="Placeholder" />
<Text color="fgMuted" font="caption">
Filled
</Text>
<MockTextInput label="Label" labelVariant="inside" value="Text" />
<Text color="fgMuted" font="caption">
Read only
</Text>
<MockTextInput label="Label" labelVariant="inside" readOnly value="Text" />
<Text color="fgMuted" font="caption">
Negative
</Text>
<MockTextInput label="Label" labelVariant="inside" value="Text" variant="negative" />
</VStack>
</VStack>
</Box>
</Example>
<Example inline title="TextInput with custom label">
<MockTextInput
Expand Down
38 changes: 38 additions & 0 deletions packages/mobile/src/controls/__tests__/TextInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,44 @@ describe('TextInput', () => {
);
});

it('uses inverse focused border color for foreground-muted variant', () => {
const testID = 'input-testid';
render(
<DefaultThemeProvider>
<TextInput
accessibilityHint="Text input field"
accessibilityLabel="Text input field"
testID={testID}
variant="foregroundMuted"
/>
</DefaultThemeProvider>,
);

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(
<DefaultThemeProvider>
<TextInput
accessibilityHint="Text input field"
accessibilityLabel="Text input field"
readOnly
testID={testID}
/>
</DefaultThemeProvider>,
);

fireEvent(screen.getByTestId(testID), 'focus');
const focusedBorderOverlayStyle = getFocusedBorderOverlayStyle();
expect(focusedBorderOverlayStyle).toBeUndefined();
});

it('renders label outside by default', () => {
const labelTestID = 'label-test';
render(
Expand Down
Loading
Loading