Shared/KeywordSearchInput
Default
Empty
English Placeholder
Interactive
With Value
keyword-search-input.tsx
/**
* Shared keyword search input — text field with a leading search icon and an
* inline × clear button. Promoted from `components/products/product-keyword-search.tsx`
* (issue #1384, part of epic #1365) so `/notes/` can reuse the same look
* instead of forking it — the #1333/#1334 shared-component pattern already
* used for `FilterToggle` / `ClearFilterButton` / `FilterSectionHeading`.
*
* Purely presentational — holds NO internal state beyond the input ref used
* to refocus after the inline clear. Callers own the debounce timing and
* value/URL-param wiring (see `/products/` and `/notes/` filter islands).
*/
import { useRef } from 'preact/hooks';
export interface KeywordSearchInputProps {
value: string;
onChange: (value: string) => void;
/** Input placeholder — locale-driven by the caller. Defaults to the JA copy. */
placeholder?: string;
// data-* passthrough (e.g. `data-notes-keyword-input`) so callers can hang a
// DOM-contract hook off the rendered <input> without the component needing
// to know about it.
[key: `data-${string}`]: unknown;
}
const SearchIcon = () => (
<svg
class="w-[1.2em] h-[1.2em]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
);
export function KeywordSearchInput({
value,
onChange,
placeholder = 'キーワードでフィルタ',
...rest
}: KeywordSearchInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div class="flex items-center pt-vgap-sm">
<div class="flex items-center pr-hgap-xs">
<SearchIcon />
</div>
<div class="relative">
<input
{...rest}
ref={inputRef}
type="text"
value={value}
onChange={(e) => onChange((e.currentTarget as HTMLInputElement).value)}
placeholder={placeholder}
aria-label={placeholder}
class="rounded-none w-[250px] sm:w-[350px] lg:w-[450px] xl:w-[550px] px-hgap-sm py-vgap-xs border-[3px] border-zd-white bg-white text-zd-literal-black"
/>
{value && (
<button
onClick={() => {
onChange('');
inputRef.current?.focus();
}}
class="absolute right-[8px] top-[50%] -translate-y-1/2 text-zd-gray hover:text-zd-literal-black text-lg leading-none"
aria-label="Clear search"
>
×
</button>
)}
</div>
</div>
);
}
export default KeywordSearchInput;
keyword-search-input.stories.tsx
import { useState } from 'preact/hooks';
import { KeywordSearchInput } from './keyword-search-input';
import type { ControlsMap } from '@/sub-packages/styleguide-v2/src/data/control-types';
/**
* KeywordSearchInput Component
*
* Shared keyword search input — a lens icon + text field with an inline ×
* clear button. Promoted from the `/products/` filter island's
* `product-keyword-search.tsx` (issue #1384) so `/notes/` reuses the same
* look instead of forking it.
*
* Purely presentational — holds NO debounce/URL-param state; callers own
* that (see `products-page-content.tsx` / `notes-filter-island.tsx`).
*/
export const meta = {
title: 'Shared/KeywordSearchInput',
};
export const controls: ControlsMap = {
placeholder: { type: 'text', default: 'キーワードでフィルタ' },
};
/**
* Default - controllable via props panel
*/
export const Default = {
args: { placeholder: 'キーワードでフィルタ' },
render: ({ placeholder }: { placeholder: string }) => {
const [value, setValue] = useState('');
return <KeywordSearchInput value={value} onChange={setValue} placeholder={placeholder} />;
},
};
/**
* Empty — no clear button (nothing typed yet)
*/
export const Empty = () => (
<KeywordSearchInput value="" onChange={() => {}} placeholder="キーワードでフィルタ" />
);
/**
* With a value — the inline × clear button appears
*/
export const WithValue = () => (
<KeywordSearchInput value="OXI ONE" onChange={() => {}} placeholder="キーワードでフィルタ" />
);
/**
* EN placeholder (`/en/notes/`, `/en/products/`)
*/
export const EnglishPlaceholder = () => (
<KeywordSearchInput value="" onChange={() => {}} placeholder="Filter by keyword" />
);
/**
* Interactive — type to see the value update and the clear button appear;
* click × to clear and refocus.
*/
const InteractiveInput = () => {
const [value, setValue] = useState('');
return (
<KeywordSearchInput value={value} onChange={setValue} placeholder="キーワードでフィルタ" />
);
};
export const Interactive = () => <InteractiveInput />;