Skip to content

Commit

Permalink
refactor(InputGroup): re-architect from scratch
Browse files Browse the repository at this point in the history
  • Loading branch information
kripod committed Jul 21, 2024
1 parent b4705c5 commit 9c4f4fb
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 152 deletions.
93 changes: 38 additions & 55 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { clsx } from "clsx/lite";
import { forwardRef, useContext, useImperativeHandle, useRef } from "react";

import { controlClassName } from "../utils/controlClassName";
import { textBoxClassName } from "../utils/controlClassName";
import {
InputGroupAddon,
InputGroupAddonEndContext,
InputGroupAddonStartContext,
InputGroupDisabledContext,
} from "./InputGroup";

export interface InputProps
Expand All @@ -21,63 +21,46 @@ export const Input = forwardRef(function Input(
const localRef = useRef<HTMLInputElement>(null as never);
useImperativeHandle(ref, () => localRef.current, []);

const groupDisabled = useContext(InputGroupDisabledContext);
const addonStart = useContext(InputGroupAddonStartContext);
const addonEnd = useContext(InputGroupAddonEndContext);
const shape =
shapeRaw ?? (addonStart != null || addonEnd != null ? "pill" : "rectangle");

return (
<>
{addonStart != null ? (
<InputGroupAddon
className={clsx(
"justify-self-start",
size === "sm" && "px-2.5 pe-1.5",
size === "md" && "px-3 pe-1.5",
size === "lg" && "px-4 pe-2",
)}
onWidthChange={(value) => {
localRef.current.style.setProperty(
"padding-inline-start",
`${value}px`,
);
}}
>
{addonStart}
</InputGroupAddon>
) : null}
const grouped = addonStart != null || addonEnd != null;
const shape = shapeRaw ?? (grouped ? "pill" : "rectangle");

<input
ref={localRef}
className={clsx(
className,
controlClassName({ size, shape }),
"text-ui-neutral-950 placeholder:text-ui-neutral-950/65 aria-invalid:ring-2 aria-invalid:ring-inset aria-invalid:ring-ui-danger-600",
size === "sm" && "px-2.5",
size === "md" && "px-3",
size === "lg" && "px-4",
)}
{...props}
/>
const input = (
<input
ref={localRef}
className={clsx(
grouped
? "self-stretch bg-transparent focus:[outline:none]"
: clsx(className, textBoxClassName({ size, shape })),
"text-ui-neutral-950 placeholder:text-ui-neutral-950/65",
size === "sm" && (grouped ? "px-1" : "px-2.5"),
size === "md" && (grouped ? "px-1" : "px-3"),
size === "lg" && (grouped ? "px-1.5" : "px-4"),
)}
{...props}
/>
);

{addonEnd != null ? (
<InputGroupAddon
className={clsx(
"justify-self-end",
size === "sm" && "px-2.5 ps-1.5",
size === "md" && "px-3 ps-1.5",
size === "lg" && "px-4 ps-2",
)}
onWidthChange={(value) => {
localRef.current.style.setProperty(
"padding-inline-end",
`${value}px`,
);
}}
>
{addonEnd}
</InputGroupAddon>
) : null}
</>
return grouped ? (
<fieldset
disabled={groupDisabled}
className={clsx(
className,
textBoxClassName({ size, shape }),
"inline-flex items-center text-ui-neutral-950/65 focus-within:outline focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-ui-accent-600 disabled:*:opacity-100 has-[:not(input):focus]:[outline:none]",
size === "sm" && "px-1.5",
size === "md" && "px-2",
size === "lg" && "px-2.5",
)}
>
{addonStart}
{input}
{addonEnd}
</fieldset>
) : (
input
);
});
50 changes: 6 additions & 44 deletions src/components/InputGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { clsx } from "clsx/lite";
import { createContext, useRef } from "react";

import { useResizeObserver } from "../hooks/useResizeObserver";
import { createContext } from "react";

export interface InputGroupProps {
addonStart?: React.ReactNode;
Expand All @@ -16,6 +13,9 @@ export const InputGroupAddonStartContext =
export const InputGroupAddonEndContext =
createContext<InputGroupProps["addonEnd"]>(null);

export const InputGroupDisabledContext =
createContext<InputGroupProps["disabled"]>(false);

export function InputGroup({
addonStart,
addonEnd,
Expand All @@ -25,48 +25,10 @@ export function InputGroup({
return (
<InputGroupAddonStartContext.Provider value={addonStart}>
<InputGroupAddonEndContext.Provider value={addonEnd}>
<fieldset
disabled={disabled}
className={clsx(
"group/input inline-grid items-center *:col-start-1 *:row-start-1",
)}
>
<InputGroupDisabledContext.Provider value={disabled}>
{children}
</fieldset>
</InputGroupDisabledContext.Provider>
</InputGroupAddonEndContext.Provider>
</InputGroupAddonStartContext.Provider>
);
}

export interface InputGroupAddonProps {
className?: string;
children?: React.ReactNode;
onWidthChange: (value: number) => void;
}

export function InputGroupAddon({
className,
children,
onWidthChange,
}: InputGroupAddonProps) {
const ref = useRef<HTMLSpanElement>(null);
useResizeObserver(ref, (entry) => {
onWidthChange(
// TODO: Remove fallback once most browsers support `borderBoxSize`
entry.borderBoxSize?.[0]?.inlineSize ??
entry.target.getBoundingClientRect().width,
);
});

return (
<span
ref={ref}
className={clsx(
className,
"pointer-events-none z-10 text-ui-neutral-950/65 transition disabled:*:opacity-100 group-disabled/input:opacity-35",
)}
>
{children}
</span>
);
}
15 changes: 0 additions & 15 deletions src/hooks/useEffectEvent.ts

This file was deleted.

24 changes: 0 additions & 24 deletions src/hooks/useResizeObserver.ts

This file was deleted.

23 changes: 9 additions & 14 deletions src/utils/controlClassName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,17 @@ interface ControlProps {
shape: "rectangle" | "pill" | "square" | "circle";
}

export function controlClassName(props: ControlProps) {
return clsx(
"transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ui-accent-600 disabled:pointer-events-none disabled:opacity-35",
controlSizeClassName(props),
controlShapeClassName(props),
);
}

function isEquilateral(shape: ControlProps["shape"]) {
return shape === "square" || shape === "circle";
}

export function controlSizeClassName({ size, shape }: ControlProps) {
export function controlClassName({ size, shape }: ControlProps) {
return clsx(
"transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ui-accent-600 disabled:pointer-events-none disabled:opacity-35",
size === "sm" && clsx("h-8 text-base/none", isEquilateral(shape) && "w-8"),
size === "md" &&
clsx("h-10 text-base/none", isEquilateral(shape) && "w-10"),
size === "lg" && clsx("h-14 text-xl/none", isEquilateral(shape) && "w-14"),
);
}

export function controlShapeClassName({ size, shape }: ControlProps) {
return clsx(
(shape === "rectangle" || shape === "square") &&
clsx(
size === "sm" && "rounded-md",
Expand All @@ -37,3 +25,10 @@ export function controlShapeClassName({ size, shape }: ControlProps) {
(shape === "pill" || shape === "circle") && "rounded-full",
);
}

export function textBoxClassName(props: ControlProps) {
return clsx(
controlClassName(props),
"aria-invalid:ring-2 aria-invalid:ring-inset aria-invalid:ring-ui-danger-600",
);
}

0 comments on commit 9c4f4fb

Please sign in to comment.