Skip to content

Commit

Permalink
Extract InternalText; add TextInput
Browse files Browse the repository at this point in the history
  • Loading branch information
federico-ercoles committed Nov 29, 2023
1 parent 2a4d336 commit 2eb242a
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 90 deletions.
117 changes: 117 additions & 0 deletions packages/bento-design-system/src/TextField/InternalTextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useBentoConfig } from "../BentoConfigContext";
import { Box } from "../Box/Box";
import { Children } from "../util/Children";
import { LocalizedString } from "../util/LocalizedString";
import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig";
import { inputRecipe } from "../Field/Field.css";
import { bodyRecipe } from "../Typography/Body/Body.css";
import { getReadOnlyBackgroundStyle } from "../Field/utils";
import useDimensions from "react-cool-dimensions";
import { match } from "ts-pattern";
import { Columns } from "../Layout/Columns";
import { IconButton } from "../IconButton/IconButton";
import { useDefaultMessages } from "../util/useDefaultMessages";
import { useState } from "react";

type Props = {
inputProps: React.InputHTMLAttributes<HTMLInputElement>;
inputRef: React.Ref<HTMLInputElement>;
placeholder?: LocalizedString;
validationState?: "valid" | "invalid";
type?: "text" | "email" | "url" | "password";
disabled?: boolean;
isReadOnly?: boolean;
rightAccessory?: Children;
showPasswordLabel?: never;
hidePasswordLabel?: never;
};

export function InternalTextInput(props: Props) {
const config = useBentoConfig().input;
const { defaultMessages } = useDefaultMessages();

const { observe: rightAccessoryRef, width: rightAccessoryWidth } = useDimensions({
// This is needed to include the padding in the width
useBorderBoxSize: true,
});

const [showPassword, setShowPassword] = useState(false);
const passwordIcon = showPassword ? config.passwordHideIcon : config.passwordShowIcon;
const passwordIconLabel = showPassword
? props.hidePasswordLabel ?? defaultMessages.TextField.hidePasswordLabel
: props.showPasswordLabel ?? defaultMessages.TextField.showPasswordLabel;

const type = match(props.type ?? "text")
.with("password", () => (showPassword ? "text" : "password"))
.with("text", "email", "url", () => props.type)
.exhaustive();

const rightAccessory = match(props.type ?? "text")
.with("password", () => (
// if we have both a rightAccessory and type='password', display the accessory on the left of the password toggle field
<Columns space={config.paddingX} alignY="center">
<IconButton
size={config.passwordIconSize}
icon={passwordIcon}
onPress={() => setShowPassword((prevValue) => !prevValue)}
kind="transparent"
hierarchy="secondary"
label={passwordIconLabel}
/>
{props.rightAccessory}
</Columns>
))
.with("email", "text", "url", () => props.rightAccessory)
.exhaustive();

return (
<Box position="relative" display="flex">
<Box
as="input"
{...props.inputProps}
ref={props.inputRef}
placeholder={props.placeholder}
type={type}
// NOTE(gabro): this is to please TS, since the inputProps type is very broad
color={undefined}
width={undefined}
height={undefined}
className={[
inputRecipe({
validation: props.isReadOnly ? "notSet" : props.validationState ?? "notSet",
}),
bodyRecipe({
color: props.disabled ? "disabled" : "primary",
weight: "default",
size: config.fontSize,
ellipsis: false,
}),
]}
{...getRadiusPropsFromConfig(config.radius)}
paddingX={config.paddingX}
paddingY={config.paddingY}
background={config.background.default}
style={{
paddingRight: rightAccessory ? rightAccessoryWidth : undefined,
flexGrow: 1,
...getReadOnlyBackgroundStyle(config),
}}
/>
{rightAccessory && (
<Box
ref={rightAccessoryRef}
position="absolute"
display="flex"
justifyContent="center"
alignItems="center"
paddingX={config.paddingX}
top={0}
bottom={0}
right={0}
>
{rightAccessory}
</Box>
)}
</Box>
);
}
99 changes: 9 additions & 90 deletions packages/bento-design-system/src/TextField/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { useTextField } from "@react-aria/textfield";
import { useRef, useState } from "react";
import { Box, IconButton, Field, Children, Columns, useDefaultMessages } from "..";
import { useRef } from "react";
import { Field, Children } from "..";
import { LocalizedString } from "../util/LocalizedString";
import { inputRecipe } from "../Field/Field.css";
import { FieldProps } from "../Field/FieldProps";
import { bodyRecipe } from "../Typography/Body/Body.css";
import useDimensions from "react-cool-dimensions";
import { useBentoConfig } from "../BentoConfigContext";
import { match } from "ts-pattern";
import { getReadOnlyBackgroundStyle } from "../Field/utils";
import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig";
import { InternalTextInput } from "./InternalTextInput";

type Props = FieldProps<string> & {
placeholder?: LocalizedString;
Expand All @@ -21,14 +15,7 @@ type Props = FieldProps<string> & {
} & Pick<React.HTMLProps<HTMLInputElement>, "onKeyDown" | "onKeyUp">;

export function TextField(props: Props) {
const config = useBentoConfig().input;
const inputRef = useRef<HTMLInputElement>(null);
const { defaultMessages } = useDefaultMessages();

const { observe: rightAccessoryRef, width: rightAccessoryWidth } = useDimensions({
// This is needed to include the padding in the width
useBorderBoxSize: true,
});

const validationState = props.isReadOnly ? undefined : props.issues ? "invalid" : "valid";

Expand All @@ -43,87 +30,19 @@ export function TextField(props: Props) {
inputRef
);

const [showPassword, setShowPassword] = useState(false);
const passwordIcon = showPassword ? config.passwordHideIcon : config.passwordShowIcon;
const passwordIconLabel = showPassword
? props.hidePasswordLabel ?? defaultMessages.TextField.hidePasswordLabel
: props.showPasswordLabel ?? defaultMessages.TextField.showPasswordLabel;

const type = match(props.type ?? "text")
.with("password", () => (showPassword ? "text" : "password"))
.with("text", "email", "url", () => props.type)
.exhaustive();

const rightAccessory = match(props.type ?? "text")
.with("password", () => (
// if we have both a rightAccessory and type='password', display the accessory on the left of the password toggle field
<Columns space={config.paddingX} alignY="center">
<IconButton
size={config.passwordIconSize}
icon={passwordIcon}
onPress={() => setShowPassword((prevValue) => !prevValue)}
kind="transparent"
hierarchy="secondary"
label={passwordIconLabel}
/>
{props.rightAccessory}
</Columns>
))
.with("email", "text", "url", () => props.rightAccessory)
.exhaustive();

return (
<Field
{...props}
labelProps={labelProps}
assistiveTextProps={descriptionProps}
errorMessageProps={errorMessageProps}
>
<Box position="relative" display="flex">
<Box
as="input"
ref={inputRef}
{...inputProps}
type={type}
// NOTE(gabro): this is to please TS, since the inputProps type is very broad
color={undefined}
width={undefined}
height={undefined}
{...getRadiusPropsFromConfig(config.radius)}
paddingX={config.paddingX}
paddingY={config.paddingY}
background={config.background.default}
className={[
inputRecipe({ validation: validationState || "notSet" }),
bodyRecipe({
color: props.disabled ? "disabled" : "primary",
weight: "default",
size: config.fontSize,
ellipsis: false,
}),
]}
style={{
paddingRight: rightAccessory ? rightAccessoryWidth : undefined,
flexGrow: 1,
...getReadOnlyBackgroundStyle(config),
}}
/>
{rightAccessory && (
<Box
ref={rightAccessoryRef}
position="absolute"
display="flex"
justifyContent="center"
alignItems="center"
paddingX={config.paddingX}
top={0}
bottom={0}
right={0}
>
{rightAccessory}
</Box>
)}
</Box>
<InternalTextInput
inputProps={inputProps}
inputRef={inputRef}
validationState={validationState}
{...props}
/>
</Field>
);
}
Expand Down
40 changes: 40 additions & 0 deletions packages/bento-design-system/src/TextField/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { HTMLAttributes, useRef } from "react";
import { AtLeast } from "../util/AtLeast";
import { FieldProps } from "../Field/FieldProps";
import { InternalTextInput } from "./InternalTextInput";
import { useTextField } from "@react-aria/textfield";
import { LocalizedString } from "../util/LocalizedString";
import { Children } from "../util/Children";

type Props = AtLeast<Pick<HTMLAttributes<HTMLInputElement>, "aria-label" | "aria-labelledby">> &
Pick<FieldProps<string>, "autoFocus" | "disabled" | "onBlur" | "onChange" | "value"> & {
validationState: "valid" | "invalid";
placeholder?: LocalizedString;
isReadOnly?: boolean;
type?: "text" | "email" | "url" | "password";
rightAccessory?: Children;
showPasswordLabel?: never;
hidePasswordLabel?: never;
};

/**
* Standalone text input component.
*
* Since it has no label, users must pass either `aria-label` or `aria-labelledby` in order to
* preserve accessibility.
*/
export function TextInput(props: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const { inputProps } = useTextField(
{
...props,
validationState: props.validationState,
isDisabled: props.disabled,
},
inputRef
);

return <InternalTextInput inputProps={inputProps} inputRef={inputRef} {...props} />;
}

export type { Props as TextInputProps };

0 comments on commit 2eb242a

Please sign in to comment.