Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,35 +43,67 @@ describe('Input', () => {
const input = getByTestId(TEST_ID);

expect(input).toBeDisabled();
expect(input).toHaveStyle({ opacity: tw`opacity-50`.opacity });
const expectedOpacity = (tw.style('opacity-50') as TextStyle).opacity;
expect(input).toHaveStyle({ opacity: expectedOpacity });
});

it('applies iOS placeholder lineHeight workaround when placeholder is visible', () => {
if (Platform.OS !== 'ios') {
return;
}
describe('iOS placeholder lineHeight workaround', () => {
const originalOS = Platform.OS;

afterEach(() => {
Platform.OS = originalOS;
});

it('applies iOS placeholder lineHeight workaround when placeholder is visible and multiline is false', () => {
Platform.OS = 'ios';

const { getByTestId } = render(
<Input
testID={TEST_ID}
value=""
placeholder="Disabled"
multiline={false}
/>,
);

const input = getByTestId(TEST_ID);

expect(input).toHaveStyle({ lineHeight: 0 });
});

it('does not apply placeholder lineHeight workaround outside iOS', () => {
Platform.OS = 'android';

const { getByTestId } = render(
<Input testID={TEST_ID} value="" placeholder="Disabled" />,
);

const input = getByTestId(TEST_ID);

expect(input).not.toHaveStyle({ lineHeight: 0 });
});
});

it('when multiline is true, does not apply lineHeight zero for visible placeholder', () => {
const { getByTestId } = render(
<Input testID={TEST_ID} value="" placeholder="Disabled" />,
<Input testID={TEST_ID} value="" multiline placeholder="Placeholder" />,
);

const input = getByTestId(TEST_ID);

expect(input).toHaveStyle({ lineHeight: 0 });
expect(input).not.toHaveStyle({ lineHeight: 0 });
});

it('does not apply placeholder lineHeight workaround outside iOS', () => {
if (Platform.OS === 'ios') {
return;
}

it('when multiline is true, applies BodyMd paragraph lineHeight', () => {
const { getByTestId } = render(
<Input testID={TEST_ID} value="" placeholder="Disabled" />,
<Input testID={TEST_ID} value="" multiline placeholder="p" />,
);

const input = getByTestId(TEST_ID);
const expectedLineHeight = (tw.style('text-body-md') as TextStyle)
.lineHeight;

expect(input).not.toHaveStyle({ lineHeight: 0 });
expect(input).toHaveStyle({ lineHeight: expectedLineHeight });
});

it('removes placeholder lineHeight workaround after value changes from empty to non-empty', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ export const Input = forwardRef<TextInput, InputProps>(
onBlur,
onFocus,
autoFocus = false,
multiline,
...props
},
ref,
) => {
const [isFocused, setIsFocused] = useState(autoFocus);
const tw = useTailwind();
const theme = useTheme();
const isMultiline = multiline === true;

const placeholderTextColor = useMemo(
() =>
Expand All @@ -56,7 +58,30 @@ export const Input = forwardRef<TextInput, InputProps>(
// scoped to the placeholder-visible state without affecting typed text.
const isPlaceholderVisible = hasPlaceholder && value === '';

const inputStyle = useMemo(
// Multiline field styles
const multilineChromeStyle = useMemo(
() =>
tw.style(
'text-default',
'bg-default',
'border',
'border-transparent',
!isStateStylesDisabled && isDisabled && 'opacity-50',
!isStateStylesDisabled &&
!isDisabled &&
isFocused &&
'border-primary-default',
twClassName,
),
[isStateStylesDisabled, isDisabled, isFocused, twClassName, tw],
);
const multilineTypographyStyle = useMemo(
() => tw.style(`text-${textVariant}`, fontClass),
[textVariant, fontClass, tw],
);

// Single-line field styles
const singleLineChromeStyle = useMemo(
() =>
tw.style(
fontClass,
Expand All @@ -80,11 +105,26 @@ export const Input = forwardRef<TextInput, InputProps>(
tw,
],
);
Comment thread
brianacnguyen marked this conversation as resolved.

const variantTextStyle = useMemo(
const singleLineTypographyStyle = useMemo(
() => MAP_TEXT_VARIANT_INPUT_METRICS[textVariant],
[textVariant],
);
// iOS-only single-line placeholder fix: native TextInput can render
// placeholder text vertically offset. Do not use on Android (lineHeight 0
// collapses text) or on multiline (breaks paragraph layout).
const iosSingleLinePlaceholderLineHeightFix = Platform.OS === 'ios' &&
isPlaceholderVisible && { lineHeight: 0 as const };

const resolvedStyle = (
isMultiline
? [multilineChromeStyle, multilineTypographyStyle, style]
: [
singleLineChromeStyle,
singleLineTypographyStyle,
iosSingleLinePlaceholderLineHeightFix,
style,
]
).filter(Boolean);

useEffect(() => {
if (isDisabled || isReadOnly) {
Expand All @@ -107,21 +147,12 @@ export const Input = forwardRef<TextInput, InputProps>(
},
[onFocus],
);
const resolvedStyle = [
inputStyle,
variantTextStyle,
// iOS-only workaround: when a placeholder is visible, native iOS
// TextInput can render placeholder text vertically offset.
// Keep this iOS-only because lineHeight: 0 can collapse text on Android.
Platform.OS === 'ios' &&
isPlaceholderVisible && { lineHeight: 0 as const },
style,
].filter(Boolean);

return (
<TextInput
ref={ref}
{...props}
multiline={multiline}
placeholder={placeholder}
placeholderTextColor={placeholderTextColor}
value={value}
Expand Down
Loading
Loading