Skip to content

Commit

Permalink
feat: added search field component
Browse files Browse the repository at this point in the history
  • Loading branch information
coderwelsch committed Nov 6, 2023
1 parent 35446b6 commit 4134a1e
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/components/form-field/form-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Listbox } from "./listbox/listbox";
import { MultiCombobox } from "./multi-combobox/multi-combobox";
import { SingleCombobox } from "./single-combobox/single-combobox";
import { FormFieldGroup } from "./form-field-group";
import { SearchInput } from "./search-field/search-field";

interface FormFieldProps {
children: React.ReactNode;
Expand All @@ -32,5 +33,6 @@ FormField.Listbox = Listbox;
FormField.MultiCombobox = MultiCombobox;
FormField.SingleCombobox = SingleCombobox;
FormField.Group = FormFieldGroup;
FormField.SearchInput = SearchInput;

export { FormField };
81 changes: 81 additions & 0 deletions src/components/form-field/search-field/search-field.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable react/jsx-props-no-spreading */
import type { Meta, StoryObj } from "@storybook/react";
import React, { useState } from "react";
import { FormField } from "../form-field";

const meta: Meta<typeof FormField.SearchInput> = {
title: "Input/SearchInput",
component: FormField.SearchInput,
};

export default meta;

type Story = StoryObj<typeof FormField.SearchInput>;

const SearchInputWithHooks = ({
error = false,
disabled = false,
readOnly = false,
value,
}: {
error?: boolean;
disabled?: boolean;
readOnly?: boolean;
value?: string;
}) => {
const [inputValue, setInputValue] = useState(value);

return (
<FormField>
<FormField.LabelGroup>
<FormField.Label htmlFor="value">Label</FormField.Label>
<FormField.Description id="value-description">Description</FormField.Description>
</FormField.LabelGroup>
<FormField.SearchInput
id="value"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
ariaDescribedBy="value-description"
error={error}
disabled={disabled}
readOnly={readOnly}
onClear={() => {
setInputValue("");
}}
/>
{error ? <FormField.ErrorMessage>Error message.</FormField.ErrorMessage> : null}
</FormField>
);
};

export const Default: Story = {
render: () => (
<div className="w-72">
<SearchInputWithHooks />
</div>
),
};

export const WithError: Story = {
render: () => (
<div className="w-72">
<SearchInputWithHooks error />
</div>
),
};

export const ReadOnly: Story = {
render: () => (
<div className="w-72">
<SearchInputWithHooks readOnly value="Readonly text" />
</div>
),
};

export const Disabled: Story = {
render: () => (
<div className="w-72">
<SearchInputWithHooks disabled />
</div>
),
};
89 changes: 89 additions & 0 deletions src/components/form-field/search-field/search-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useMemo, useRef } from "react";
import { classNames } from "../../../util/class-names";
import CrossIcon from "../../../icons/cross-icon";
import SearchIcon from "../../../icons/search-icon";

export interface SearchInputProps extends React.ComponentPropsWithoutRef<"input"> {
autoSelect?: boolean;
ariaDescribedBy?: string;
error?: boolean;
onClear: () => void;
}

export const SearchInput = ({
ariaDescribedBy,
readOnly,
autoSelect,
onClear,
error,
value,
disabled,
className,
...props
}: SearchInputProps) => {
const inputRef = useRef<HTMLInputElement>(null);

const isClearIconShown = useMemo(() => {
return !readOnly && !disabled && value?.toString().length;
}, [disabled, readOnly, value]);

const handleAutoSelection = () => {
if (autoSelect && inputRef.current) {
inputRef.current.select();
}
};

return (
<div className={classNames("relative w-full")}>
<div
className="pointer-events-none absolute inset-y-0 left-0 z-10 flex items-center pl-3"
aria-hidden="true"
>
<SearchIcon className="text-gray-400 h-3.5 w-3.5 fill-neutral-600" />
</div>

<input
ref={inputRef}
aria-describedby={ariaDescribedBy}
onMouseOver={handleAutoSelection}
onFocus={handleAutoSelection}
onClick={handleAutoSelection}
type="search"
className={classNames(
"paragraph-100 relative block h-8 w-full rounded border border-neutral-400 py-2 px-9 text-neutral-800 placeholder:text-neutral-600 focus:outline-none",

Check failure on line 53 in src/components/form-field/search-field/search-field.tsx

View workflow job for this annotation

GitHub Actions / format-check

Replace `y-2·px-9` with `x-9·py-2`
readOnly && "bg-neutral-100",
disabled && "cursor-not-allowed bg-neutral-100 text-neutral-600",
!error &&
!disabled &&
"hover:border-neutral-600 focus:border-primary-400 focus:ring-2 focus:ring-primary-200",
error && !disabled && "border-danger-500",
className
)}
readOnly={readOnly}
disabled={disabled}
value={value}
{...props}
/>

{isClearIconShown ? (
<div
tabIndex={0}
onClick={onClear}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();

onClear();
}
}}
className="absolute top-1/2 transform -translate-y-1/2 right-0 z-10 flex items-center justify-center bg-neutral-100 border border-transparent hover:border-neutral-300 mr-1.5 w-5 h-5 rounded cursor-pointer"

Check failure on line 80 in src/components/form-field/search-field/search-field.tsx

View workflow job for this annotation

GitHub Actions / format-check

Replace `top-1/2·transform·-translate-y-1/2·right-0·z-10·flex·items-center·justify-center·bg-neutral-100·border·border-transparent·hover:border-neutral-300·mr-1.5·w-5·h-5·rounded·cursor-pointer` with `right-0·top-1/2·z-10·mr-1.5·flex·h-5·w-5·-translate-y-1/2·transform·cursor-pointer·items-center·justify-center·rounded·border·border-transparent·bg-neutral-100·hover:border-neutral-300`
aria-label="Clear Search Input"
role="button"
>
<CrossIcon className="h-3.5 w-3.5 fill-neutral-600" />
</div>
) : null}
</div>
);
};

0 comments on commit 4134a1e

Please sign in to comment.