diff --git a/src/elements/Input/Input.tsx b/src/elements/Input/Input.tsx index f61a152..aae63b3 100644 --- a/src/elements/Input/Input.tsx +++ b/src/elements/Input/Input.tsx @@ -7,6 +7,7 @@ import isString from "lodash/isString" import { RefObject, forwardRef, + memo, useCallback, useEffect, useImperativeHandle, @@ -26,7 +27,13 @@ import { } from "react-native" import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated" import styled from "styled-components" -import { INPUT_VARIANTS, InputState, InputVariant, getInputState, getInputVariant } from "./helpers" +import { + getInputVariants, + InputState, + InputVariant, + getInputState, + getInputVariant, +} from "./helpers" import { maskValue, unmaskText } from "./maskValue" import { EyeClosedIcon, EyeOpenedIcon, TriangleDown, XCircleIcon } from "../../svgs" import { useTheme } from "../../utils/hooks" @@ -126,610 +133,621 @@ export interface InputRef { clear: () => void } -export const Input = forwardRef( - ( - { - addClearListener = false, - defaultValue, - disabled = false, - enableClearButton = false, - fixedRightPlaceholder, - hintText = "What's this?", - icon, - leftComponentWidth = LEFT_COMPONENT_WIDTH, - mask, - selectComponentWidth = SELECT_COMPONENT_WIDTH, - loading = false, - onBlur, - onChangeText, - onClear, - onFocus, - onSelectTap, - placeholder, - secureTextEntry = false, - style: styleProp = {}, - unit, - value: propValue, - selectDisplayLabel, - ...props - }, - ref - ) => { - const { color, theme, space } = useTheme() - - const [focused, setIsFocused] = useState(false) - const [delayedFocused, setDelayedFocused] = useState(false) - - const [value, setValue] = useState( - maskValue({ - currentValue: propValue ?? defaultValue, - mask: mask, - }) - ) +export const Input = memo( + forwardRef( + ( + { + addClearListener = false, + defaultValue, + disabled = false, + enableClearButton = false, + fixedRightPlaceholder, + hintText = "What's this?", + icon, + leftComponentWidth = LEFT_COMPONENT_WIDTH, + mask, + selectComponentWidth = SELECT_COMPONENT_WIDTH, + loading = false, + onBlur, + onChangeText, + onClear, + onFocus, + onSelectTap, + placeholder, + secureTextEntry = false, + style: styleProp = {}, + unit, + value: propValue, + selectDisplayLabel, + ...props + }, + ref + ) => { + const { color, theme, space } = useTheme() + + const [focused, setIsFocused] = useState(false) + const [delayedFocused, setDelayedFocused] = useState(false) + + const [value, setValue] = useState( + maskValue({ + currentValue: propValue ?? defaultValue, + mask: mask, + }) + ) - const [showPassword, setShowPassword] = useState(!secureTextEntry) + const [showPassword, setShowPassword] = useState(!secureTextEntry) - const [inputWidth, setInputWidth] = useState(0) - const placeholderWidths = useRef([]) + const [inputWidth, setInputWidth] = useState(0) + const placeholderWidths = useRef([]) - const rightComponentRef = useRef(null) - const inputRef = useRef() + const rightComponentRef = useRef(null) + const inputRef = useRef() - const variant: InputVariant = getInputVariant({ - hasError: !!props.error, - disabled: disabled, - }) + const variant: InputVariant = getInputVariant({ + hasError: !!props.error, + disabled: disabled, + }) - const hasLeftComponent = useMemo( - () => !!unit || !!icon || !!onSelectTap, - [unit, icon, onSelectTap] - ) + const hasLeftComponent = useMemo( + () => !!unit || !!icon || !!onSelectTap, + [unit, icon, onSelectTap] + ) - const animatedState = useSharedValue( - getInputState({ - isFocused: !!focused, - value: value, - }) - ) + const animatedState = useSharedValue( + getInputState({ + isFocused: !!focused, + value: value, + }) + ) - useImperativeHandle(ref, () => inputRef.current as InputRef) + useImperativeHandle(ref, () => inputRef.current as InputRef) - useEffect(() => { - // If the prop value changes, update the local state - // This optimisation is not needed if no propValue has been specified - if (propValue !== undefined && propValue !== value) { - setValue(maskValue({ currentValue: propValue || "", mask })) - } - }, [propValue, value, mask]) + useEffect(() => { + // If the prop value changes, update the local state + // This optimisation is not needed if no propValue has been specified + if (propValue !== undefined && propValue !== value) { + setValue(maskValue({ currentValue: propValue || "", mask })) + } + }, [propValue, value, mask]) - useEffect(() => { - // If the mask value changes, update the value state to be formatted again - setValue(maskValue({ currentValue: value, mask })) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mask]) + useEffect(() => { + // If the mask value changes, update the value state to be formatted again + setValue(maskValue({ currentValue: value, mask })) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mask]) - const fontFamily = theme.fonts.sans.regular + const fontFamily = theme.fonts.sans.regular - useEffect(() => { - /* to make the font work for secure text inputs, + useEffect(() => { + /* to make the font work for secure text inputs, see https://github.com/facebook/react-native/issues/30123#issuecomment-711076098 */ - inputRef.current?.setNativeProps({ - style: { fontFamily }, - }) - }, [fontFamily]) + inputRef.current?.setNativeProps({ + style: { fontFamily }, + }) + }, [fontFamily]) + + useEffect(() => { + // We don't need to delay hiding the placeholder + if (!focused && delayedFocused) { + setDelayedFocused(false) + } - useEffect(() => { - // We don't need to delay hiding the placeholder - if (!focused && delayedFocused) { - setDelayedFocused(false) - } + let delayFocusedTimeout: NodeJS.Timeout - let delayFocusedTimeout: NodeJS.Timeout + // We only want to show the placeholder after we're sure the title animation has finished + if (!delayedFocused && focused) { + delayFocusedTimeout = setTimeout(() => { + setDelayedFocused(focused) + }, 200) + } - // We only want to show the placeholder after we're sure the title animation has finished - if (!delayedFocused && focused) { - delayFocusedTimeout = setTimeout(() => { - setDelayedFocused(focused) - }, 200) - } + return () => { + if (delayFocusedTimeout) { + clearTimeout(delayFocusedTimeout) + } + } + }, [focused, delayedFocused]) + + const handleChangeText = useCallback( + (text: string) => { + if (mask) { + const newText = + maskValue({ currentValue: text, mask: mask, previousValue: value }) || "" + setValue(newText) + onChangeText?.(newText, unmaskText(text)) + } else { + setValue(text) + onChangeText?.(text) + } + }, + [onChangeText, value, mask] + ) - return () => { - if (delayFocusedTimeout) { - clearTimeout(delayFocusedTimeout) + const handleClear = useCallback(() => { + LayoutAnimation.configureNext({ ...LayoutAnimation.Presets.easeInEaseOut, duration: 200 }) + inputRef.current?.clear() + handleChangeText("") + onClear?.() + }, [onClear, handleChangeText]) + + useEffect(() => { + if (!addClearListener) { + return } - } - }, [focused, delayedFocused]) - - const handleChangeText = useCallback( - (text: string) => { - if (mask) { - const newText = maskValue({ currentValue: text, mask: mask, previousValue: value }) || "" - setValue(newText) - onChangeText?.(newText, unmaskText(text)) - } else { - setValue(text) - onChangeText?.(text) + + inputEvents.addListener("clear", handleClear) + + return () => { + inputEvents.removeListener("clear", handleClear) } - }, - [onChangeText, value, mask] - ) - - const handleClear = useCallback(() => { - LayoutAnimation.configureNext({ ...LayoutAnimation.Presets.easeInEaseOut, duration: 200 }) - inputRef.current?.clear() - handleChangeText("") - onClear?.() - }, [onClear, handleChangeText]) - - useEffect(() => { - if (!addClearListener) { - return - } + }, [addClearListener, handleClear]) - inputEvents.addListener("clear", handleClear) + const { width: rightComponentWidth = 0 } = useMeasure({ ref: rightComponentRef }) - return () => { - inputEvents.removeListener("clear", handleClear) - } - }, [addClearListener, handleClear]) + const textInputPaddingLeft = useMemo(() => { + if (!hasLeftComponent) { + return HORIZONTAL_PADDING + } - const { width: rightComponentWidth = 0 } = useMeasure({ ref: rightComponentRef }) + if (onSelectTap) { + return selectComponentWidth + HORIZONTAL_PADDING + } - const textInputPaddingLeft = useMemo(() => { - if (!hasLeftComponent) { - return HORIZONTAL_PADDING - } + return leftComponentWidth + }, [hasLeftComponent, leftComponentWidth, onSelectTap, selectComponentWidth]) + + const styles = useMemo(() => { + return { + fontFamily: fontFamily, + fontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), + minHeight: props.multiline ? MULTILINE_INPUT_MIN_HEIGHT : INPUT_MIN_HEIGHT, + maxHeight: props.multiline ? MULTILINE_INPUT_MAX_HEIGHT : undefined, + height: props.multiline ? MULTILINE_INPUT_MIN_HEIGHT : undefined, + borderWidth: 1, + paddingRight: rightComponentWidth + HORIZONTAL_PADDING, + paddingLeft: textInputPaddingLeft, + ...(styleProp as {}), + } + }, [fontFamily, styleProp, props.multiline, rightComponentWidth, textInputPaddingLeft]) + + const labelStyles = useMemo(() => { + return { + // this is neeeded too make sure the label is on top of the input + backgroundColor: color("background"), + marginRight: space(0.5), + zIndex: 100, + fontFamily: fontFamily, + } + }, [fontFamily, space, color]) + + const inputVariants = getInputVariants(theme) + + useEffect(() => { + const inputState = getInputState({ + isFocused: !!focused, + value: value, + }) + animatedState.set(() => inputState) + }, [value, focused, animatedState]) + + const textInputAnimatedStyles = useAnimatedStyle(() => { + return { + borderColor: withTiming(inputVariants[variant][animatedState.get()].inputBorderColor), + color: withTiming(inputVariants[variant][animatedState.get()].inputTextColor), + } + }) - if (onSelectTap) { - return selectComponentWidth + HORIZONTAL_PADDING - } + const labelAnimatedStyles = useAnimatedStyle(() => { + const hasValue = !!value - return leftComponentWidth - }, [hasLeftComponent, leftComponentWidth, onSelectTap, selectComponentWidth]) - - const styles = useMemo(() => { - return { - fontFamily: fontFamily, - fontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), - minHeight: props.multiline ? MULTILINE_INPUT_MIN_HEIGHT : INPUT_MIN_HEIGHT, - maxHeight: props.multiline ? MULTILINE_INPUT_MAX_HEIGHT : undefined, - height: props.multiline ? MULTILINE_INPUT_MIN_HEIGHT : undefined, - borderWidth: 1, - paddingRight: rightComponentWidth + HORIZONTAL_PADDING, - paddingLeft: textInputPaddingLeft, - ...(styleProp as {}), - } - }, [fontFamily, styleProp, props.multiline, rightComponentWidth, textInputPaddingLeft]) - - const labelStyles = useMemo(() => { - return { - // this is neeeded too make sure the label is on top of the input - backgroundColor: "white", - marginRight: space(0.5), - zIndex: 100, - fontFamily: fontFamily, - } - }, [fontFamily, space]) + // Only add a margin if the input has a left component and it is not focused and has no value + const marginLeft = + textInputPaddingLeft && !focused && !hasValue + ? textInputPaddingLeft - 3 + : HORIZONTAL_PADDING - useEffect(() => { - const inputState = getInputState({ - isFocused: !!focused, - value: value, + return { + color: withTiming(inputVariants[variant][animatedState.get()].labelColor), + top: withTiming(inputVariants[variant][animatedState.get()].labelTop), + fontSize: withTiming(inputVariants[variant][animatedState.get()].labelFontSize), + marginLeft: withTiming(marginLeft), + } }) - animatedState.set(() => inputState) - }, [value, focused, animatedState]) - const textInputAnimatedStyles = useAnimatedStyle(() => { - return { - borderColor: withTiming(INPUT_VARIANTS[variant][animatedState.get()].inputBorderColor), - color: withTiming(INPUT_VARIANTS[variant][animatedState.get()].inputTextColor), - } - }) - - const labelAnimatedStyles = useAnimatedStyle(() => { - const hasValue = !!value - - // Only add a margin if the input has a left component and it is not focused and has no value - const marginLeft = - textInputPaddingLeft && !focused && !hasValue - ? textInputPaddingLeft - 3 - : HORIZONTAL_PADDING - - return { - color: withTiming(INPUT_VARIANTS[variant][animatedState.get()].labelColor), - top: withTiming(INPUT_VARIANTS[variant][animatedState.get()].labelTop), - fontSize: withTiming(INPUT_VARIANTS[variant][animatedState.get()].labelFontSize), - marginLeft: withTiming(marginLeft), - } - }) + const selectComponentStyles = useAnimatedStyle(() => { + return { + borderColor: withTiming(inputVariants[variant][animatedState.get()].inputBorderColor), + } + }) - const selectComponentStyles = useAnimatedStyle(() => { - return { - borderColor: withTiming(INPUT_VARIANTS[variant][animatedState.get()].inputBorderColor), + const handleFocus = (e: NativeSyntheticEvent) => { + setIsFocused(true) + onFocus?.(e) } - }) - const handleFocus = (e: NativeSyntheticEvent) => { - setIsFocused(true) - onFocus?.(e) - } - - const handleBlur = (e: NativeSyntheticEvent) => { - setIsFocused(false) - onBlur?.(e) - } + const handleBlur = (e: NativeSyntheticEvent) => { + setIsFocused(false) + onBlur?.(e) + } - const hasTitle = !!props.title + const hasTitle = !!props.title - const renderLeftComponent = useCallback(() => { - const leftComponentSharedStyles: ViewProps["style"] = { - position: "absolute", - paddingHorizontal: HORIZONTAL_PADDING, - height: INPUT_MIN_HEIGHT, - top: hasTitle ? LABEL_HEIGHT : 0, - alignItems: "center", - zIndex: 100, - } + const renderLeftComponent = useCallback(() => { + const leftComponentSharedStyles: ViewProps["style"] = { + position: "absolute", + paddingHorizontal: HORIZONTAL_PADDING, + height: INPUT_MIN_HEIGHT, + top: hasTitle ? LABEL_HEIGHT : 0, + alignItems: "center", + zIndex: 100, + } - if (unit || icon) { - return ( - - {unit && ( - - {unit} - - )} - {icon} - - ) - } + if (unit || icon) { + return ( + + {unit && ( + + {unit} + + )} + {icon} + + ) + } - if (onSelectTap) { - return ( - - - {selectDisplayLabel} - - - - ) - } + + {selectDisplayLabel} + + + + ) + } - return null - }, [ - hasTitle, - unit, - icon, - onSelectTap, - leftComponentWidth, - disabled, - selectComponentWidth, - selectComponentStyles, - selectDisplayLabel, - ]) - - const renderRightComponent = useCallback(() => { - if (fixedRightPlaceholder) { - return ( - - {fixedRightPlaceholder} - - ) - } + return null + }, [ + hasTitle, + unit, + icon, + onSelectTap, + leftComponentWidth, + disabled, + selectComponentWidth, + selectComponentStyles, + selectDisplayLabel, + ]) + + const renderRightComponent = useCallback(() => { + if (fixedRightPlaceholder) { + return ( + + {fixedRightPlaceholder} + + ) + } - if (loading) { - return ( - - - - ) - } + if (loading) { + return ( + + + + ) + } - if (enableClearButton && value) { - return ( - - - - + + + + + ) + } + + if (secureTextEntry) { + return ( + + { + LayoutAnimation.configureNext({ + ...LayoutAnimation.Presets.easeInEaseOut, + duration: 200, + }) + setShowPassword(!showPassword) + }} + accessibilityLabel={showPassword ? "hide password button" : "show password button"} + hitSlop={{ bottom: 40, right: 40, left: 0, top: 40 }} + > + {!showPassword ? ( + + ) : ( + + )} + + + ) + } + + return null + }, [ + fixedRightPlaceholder, + loading, + enableClearButton, + value, + secureTextEntry, + hasTitle, + disabled, + color, + handleClear, + showPassword, + ]) + + const renderBottomComponent = useCallback(() => { + if (!!props.error) { + return ( + + {props.error} + + ) + } + + return ( + + {!!props.required || !!props.optional ? ( + + {!!props.required && "* Required"} + {!!props.optional && "* Optional"} + + ) : ( + // Adding this empty flex to make sure that the maxLength text is always on the right + + )} + {!!props.maxLength && !!props.showLimit && ( + + {(value || "").length} / {props.maxLength} + + )} ) - } + }, [props.error, props.maxLength, props.optional, props.required, props.showLimit, value]) + + const renderHint = useCallback(() => { + if (!props.onHintPress) { + return null + } - if (secureTextEntry) { return ( { - LayoutAnimation.configureNext({ - ...LayoutAnimation.Presets.easeInEaseOut, - duration: 200, - }) - setShowPassword(!showPassword) + onPress={props.onHintPress} + haptic="impactLight" + hitSlop={{ + top: 5, + right: 10, + bottom: 5, + left: 10, }} - accessibilityLabel={showPassword ? "hide password button" : "show password button"} - hitSlop={{ bottom: 40, right: 40, left: 0, top: 40 }} > - {!showPassword ? : } + + {hintText} + ) - } - - return null - }, [ - fixedRightPlaceholder, - loading, - enableClearButton, - value, - secureTextEntry, - hasTitle, - disabled, - color, - handleClear, - showPassword, - ]) - - const renderBottomComponent = useCallback(() => { - if (!!props.error) { - return ( - - {props.error} - - ) - } - - return ( - - {!!props.required || !!props.optional ? ( - - {!!props.required && "* Required"} - {!!props.optional && "* Optional"} - - ) : ( - // Adding this empty flex to make sure that the maxLength text is always on the right - - )} - {!!props.maxLength && !!props.showLimit && ( - - {(value || "").length} / {props.maxLength} - - )} - - ) - }, [props.error, props.maxLength, props.optional, props.required, props.showLimit, value]) - - const renderHint = useCallback(() => { - if (!props.onHintPress) { - return null - } + }, [hintText, props.onHintPress, space]) - return ( - - - - {hintText} - - - - ) - }, [hintText, props.onHintPress, space]) + const getPlatformSpecificPlaceholder = useCallback(() => { + if (!placeholder) { + return "" + } - const getPlatformSpecificPlaceholder = useCallback(() => { - if (!placeholder) { - return "" - } + if (Platform.OS === "ios") { + return isArray(placeholder) ? placeholder[0] : placeholder + } - if (Platform.OS === "ios") { - return isArray(placeholder) ? placeholder[0] : placeholder - } + // if it's android and we only have one string, return that string + if (isString(placeholder)) { + return placeholder + } + // otherwise, find a placeholder that has longest width that fits in the inputtext + const longestFittingStringIndex = placeholderWidths.current.findIndex( + (placeholderWidth) => { + return placeholderWidth <= inputWidth + } + ) + if (longestFittingStringIndex > -1) { + return placeholder[longestFittingStringIndex] + } - // if it's android and we only have one string, return that string - if (isString(placeholder)) { - return placeholder - } - // otherwise, find a placeholder that has longest width that fits in the inputtext - const longestFittingStringIndex = placeholderWidths.current.findIndex((placeholderWidth) => { - return placeholderWidth <= inputWidth - }) - if (longestFittingStringIndex > -1) { - return placeholder[longestFittingStringIndex] - } + // otherwise just return the shortest placeholder + return placeholder[placeholder.length - 1] + }, [inputWidth, placeholder]) - // otherwise just return the shortest placeholder - return placeholder[placeholder.length - 1] - }, [inputWidth, placeholder]) + const getPlaceholder = useCallback(() => { + // Show placeholder always if there is no title + // This is because we won't have a title animation + if (!props.title) { + return getPlatformSpecificPlaceholder() + } - const getPlaceholder = useCallback(() => { - // Show placeholder always if there is no title - // This is because we won't have a title animation - if (!props.title) { - return getPlatformSpecificPlaceholder() - } + // On blur, we want to show the placeholder immediately + if (delayedFocused) { + return getPlatformSpecificPlaceholder() + } - // On blur, we want to show the placeholder immediately - if (delayedFocused) { - return getPlatformSpecificPlaceholder() - } + // On focus, we want to show the placeholder after the title animation has finished + return "" + }, [delayedFocused, getPlatformSpecificPlaceholder, props.title]) - // On focus, we want to show the placeholder after the title animation has finished - return "" - }, [delayedFocused, getPlatformSpecificPlaceholder, props.title]) + const renderAnimatedTitle = useCallback(() => { + if (!props.title) { + return null + } - const renderAnimatedTitle = useCallback(() => { - if (!props.title) { - return null - } + return ( + + + {" "} + {props.title}{" "} + + + ) + }, [labelStyles, labelAnimatedStyles, props.title]) - return ( - - - {" "} - {props.title}{" "} - - - ) - }, [labelStyles, labelAnimatedStyles, props.title]) + const renderAndroidPlaceholderMeasuringHack = useCallback(() => { + if (Platform.OS === "ios" || !isArray(placeholder)) { + return null + } - const renderAndroidPlaceholderMeasuringHack = useCallback(() => { - if (Platform.OS === "ios" || !isArray(placeholder)) { - return null - } + // Do not render the hack if we have already measured the placeholder + if (placeholderWidths.current.length > 0) { + return null + } - // Do not render the hack if we have already measured the placeholder - if (placeholderWidths.current.length > 0) { - return null - } + return ( + + {placeholder.map((placeholderString, index) => ( + { + placeholderWidths.current[index] = event.nativeEvent.layout.width + }} + numberOfLines={1} + style={{ + ...styles, + }} + > + {placeholderString} + + ))} + + ) + }, [placeholder, styles]) return ( - - {placeholder.map((placeholderString, index) => ( - { - placeholderWidths.current[index] = event.nativeEvent.layout.width - }} - numberOfLines={1} - style={{ - ...styles, - }} - > - {placeholderString} - - ))} + + {renderAndroidPlaceholderMeasuringHack()} + + {renderHint()} + + {renderAnimatedTitle()} + + { + setInputWidth(event.nativeEvent.layout.width) + }} + scrollEnabled={props.multiline} + editable={!disabled} + textAlignVertical={props.multiline ? "top" : "center"} + ref={inputRef as RefObject} + placeholderTextColor={color("black60")} + placeholder={getPlaceholder()} + defaultValue={defaultValue} + secureTextEntry={!showPassword} + {...props} + /> + + {renderRightComponent()} + + {renderLeftComponent()} + + {/* Contains error and other data we display below the textinput */} + {renderBottomComponent()} ) - }, [placeholder, styles]) - - return ( - - {renderAndroidPlaceholderMeasuringHack()} - - {renderHint()} - - {renderAnimatedTitle()} - - { - setInputWidth(event.nativeEvent.layout.width) - }} - scrollEnabled={props.multiline} - editable={!disabled} - textAlignVertical={props.multiline ? "top" : "center"} - ref={inputRef as RefObject} - placeholderTextColor={color("black60")} - placeholder={getPlaceholder()} - defaultValue={defaultValue} - secureTextEntry={!showPassword} - {...props} - /> - - {renderRightComponent()} - - {renderLeftComponent()} - - {/* Contains error and other data we display below the textinput */} - {renderBottomComponent()} - - ) - } + } + ) ) const StyledInput = styled(TextInput)` diff --git a/src/elements/Input/helpers.ts b/src/elements/Input/helpers.ts index 86d5273..6a0c7c6 100644 --- a/src/elements/Input/helpers.ts +++ b/src/elements/Input/helpers.ts @@ -1,4 +1,5 @@ import { THEME } from "@artsy/palette-tokens" +import { ThemeType, ThemeWithDarkModeType } from "../../tokens" export const SHRINKED_LABEL_TOP = 13 export const EXPANDED_LABEL_TOP = 40 @@ -27,96 +28,104 @@ export type VariantState = { } } -const DEFAULT_VARIANT_STATES: VariantState = { - // Unfocused input with no value - untouched: { - inputBorderColor: THEME.colors.black30, - labelFontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), - labelColor: THEME.colors.black60, - labelTop: EXPANDED_LABEL_TOP, - inputTextColor: THEME.colors.black100, - }, - // Unfocused input with value - touched: { - inputBorderColor: THEME.colors.black60, - labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), - labelColor: THEME.colors.black60, - labelTop: SHRINKED_LABEL_TOP, - inputTextColor: THEME.colors.black100, - }, - // Focused input with or without value - focused: { - inputBorderColor: THEME.colors.blue100, - labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), - labelColor: THEME.colors.blue100, - labelTop: SHRINKED_LABEL_TOP, - inputTextColor: THEME.colors.black100, - }, +const getDefaultVariantStates = (theme: ThemeType | ThemeWithDarkModeType): VariantState => { + return { + // Unfocused input with no value + untouched: { + inputBorderColor: theme.colors.black30, + labelFontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), + labelColor: theme.colors.black60, + labelTop: EXPANDED_LABEL_TOP, + inputTextColor: theme.colors.black100, + }, + // Unfocused input with value + touched: { + inputBorderColor: theme.colors.black60, + labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), + labelColor: theme.colors.black60, + labelTop: SHRINKED_LABEL_TOP, + inputTextColor: theme.colors.black100, + }, + // Focused input with or without value + focused: { + inputBorderColor: theme.colors.blue100, + labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), + labelColor: theme.colors.blue100, + labelTop: SHRINKED_LABEL_TOP, + inputTextColor: theme.colors.black100, + }, + } } -const ERROR_VARIANT_STATES: VariantState = { - // Unfocused error input with no value - untouched: { - inputBorderColor: THEME.colors.red100, - labelFontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), - labelColor: THEME.colors.red100, - labelTop: EXPANDED_LABEL_TOP, - inputTextColor: THEME.colors.black100, - }, - // Unfocused error input with value - touched: { - inputBorderColor: THEME.colors.red100, - labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), - labelColor: THEME.colors.red100, - labelTop: SHRINKED_LABEL_TOP, - inputTextColor: THEME.colors.black100, - }, - // Focused error input with or without value - focused: { - inputBorderColor: THEME.colors.red100, - labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), - labelColor: THEME.colors.red100, - labelTop: SHRINKED_LABEL_TOP, - inputTextColor: THEME.colors.black100, - }, +const getErrorVariantStates = (theme: ThemeType | ThemeWithDarkModeType): VariantState => { + return { + // Unfocused error input with no value + untouched: { + inputBorderColor: theme.colors.red100, + labelFontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), + labelColor: theme.colors.red100, + labelTop: EXPANDED_LABEL_TOP, + inputTextColor: theme.colors.black100, + }, + // Unfocused error input with value + touched: { + inputBorderColor: theme.colors.red100, + labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), + labelColor: theme.colors.red100, + labelTop: SHRINKED_LABEL_TOP, + inputTextColor: theme.colors.black100, + }, + // Focused error input with or without value + focused: { + inputBorderColor: theme.colors.red100, + labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), + labelColor: theme.colors.red100, + labelTop: SHRINKED_LABEL_TOP, + inputTextColor: theme.colors.black100, + }, + } } -const DISABLED_VARIANT_STATES: VariantState = { - // Unfocused disabled input with no value - untouched: { - inputBorderColor: THEME.colors.black30, - labelFontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), - labelColor: THEME.colors.black30, - labelTop: EXPANDED_LABEL_TOP, - inputTextColor: THEME.colors.black30, - }, - // Unfocused disabled input with value - touched: { - inputBorderColor: THEME.colors.black30, - labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), - labelColor: THEME.colors.black30, - labelTop: SHRINKED_LABEL_TOP, - inputTextColor: THEME.colors.black30, - }, - // Focused disabled input with or without value - // Adding this just to satisfy typescript because a disabled input can't be focused - focused: { - inputBorderColor: THEME.colors.black30, - labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), - labelColor: THEME.colors.black30, - labelTop: SHRINKED_LABEL_TOP, - inputTextColor: THEME.colors.black30, - }, +const getDisabledVariantStates = (theme: ThemeType | ThemeWithDarkModeType): VariantState => { + return { + // Unfocused disabled input with no value + untouched: { + inputBorderColor: theme.colors.black30, + labelFontSize: parseInt(THEME.textVariants["sm-display"].fontSize, 10), + labelColor: theme.colors.black30, + labelTop: EXPANDED_LABEL_TOP, + inputTextColor: theme.colors.black30, + }, + // Unfocused disabled input with value + touched: { + inputBorderColor: theme.colors.black30, + labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), + labelColor: theme.colors.black30, + labelTop: SHRINKED_LABEL_TOP, + inputTextColor: theme.colors.black30, + }, + // Focused disabled input with or without value + // Adding this just to satisfy typescript because a disabled input can't be focused + focused: { + inputBorderColor: theme.colors.black30, + labelFontSize: parseInt(THEME.textVariants.xs.fontSize, 10), + labelColor: theme.colors.black30, + labelTop: SHRINKED_LABEL_TOP, + inputTextColor: theme.colors.black30, + }, + } } -export const INPUT_VARIANTS = { - default: DEFAULT_VARIANT_STATES, - error: ERROR_VARIANT_STATES, - disabled: DISABLED_VARIANT_STATES, +export const getInputVariants = (theme: ThemeType | ThemeWithDarkModeType) => { + return { + default: getDefaultVariantStates(theme), + error: getErrorVariantStates(theme), + disabled: getDisabledVariantStates(theme), + } } -export type InputState = keyof typeof DEFAULT_VARIANT_STATES -export type InputVariant = keyof typeof INPUT_VARIANTS +export type InputState = keyof ReturnType +export type InputVariant = keyof ReturnType export const getInputState = ({ isFocused, diff --git a/src/elements/Message/Message.tsx b/src/elements/Message/Message.tsx index 303678f..ec89544 100644 --- a/src/elements/Message/Message.tsx +++ b/src/elements/Message/Message.tsx @@ -80,13 +80,13 @@ export const Message: React.FC = ({ )} {!!title && ( - + {title} )} {!!text && ( - + {text} )} @@ -139,7 +139,9 @@ const colors: Record< info: { background: "blue10", title: "blue100", - text: "black100", + // The text should be black regardless of the theme + // @ts-expect-error + text: "black", icon: "black100", }, success: { diff --git a/src/elements/Screen/ScreenBase.tsx b/src/elements/Screen/ScreenBase.tsx index f39125c..e1a5961 100644 --- a/src/elements/Screen/ScreenBase.tsx +++ b/src/elements/Screen/ScreenBase.tsx @@ -16,7 +16,12 @@ export const ScreenBase: React.FC = ({ return ( - + {children} @@ -35,6 +40,7 @@ const SafeAreaCover: React.FC<{ safeArea: boolean }> = ({ safeArea }) => { right={0} top={safeArea ? -insets.top : 0} height={insets.top} + backgroundColor="background" /> ) } diff --git a/src/tokens.ts b/src/tokens.ts index 7ebf184..c52d811 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -114,9 +114,10 @@ const v3dark: ThemeWithDarkModeType = { black10: v3.colors.black60, black30: v3.colors.black60, black60: v3.colors.black30, - blue100: "#5E6EFF", + blue100: "#0F77FF", + blue10: "#E6E7F5", red100: "#E44738", - green100: "#16C193", + green100: "#019F73", devpurple: "rgb(136, 82, 237)", background: "#121212", onBackground: THEME_DARK.colors.white100,