[ui][typeahead] implementation

pull/340/head
Yangshun Tay 2 years ago
parent a26bd49a96
commit 90f8556f8c

@ -25,11 +25,11 @@ export default function ProductNavigation({ items, title }: Props) {
{items.map((item) => {items.map((item) =>
item.children != null && item.children.length > 0 ? ( item.children != null && item.children.length > 0 ? (
<Menu key={item.name} as="div" className="relative text-left"> <Menu key={item.name} as="div" className="relative text-left">
<Menu.Button className="focus:ring-primary-600 flex items-center rounded-md text-sm font-medium text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2"> <Menu.Button className="focus:ring-primary-600 flex items-center rounded-md text-sm font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-offset-2">
<span>{item.name}</span> <span>{item.name}</span>
<ChevronDownIcon <ChevronDownIcon
aria-hidden="true" aria-hidden="true"
className="ml-1 h-5 w-5 text-gray-500" className="ml-1 h-5 w-5 text-slate-500"
/> />
</Menu.Button> </Menu.Button>
<Transition <Transition
@ -47,8 +47,8 @@ export default function ProductNavigation({ items, title }: Props) {
{({ active }) => ( {({ active }) => (
<Link <Link
className={clsx( className={clsx(
active ? 'bg-gray-100' : '', active ? 'bg-slate-100' : '',
'block px-4 py-2 text-sm text-gray-700', 'block px-4 py-2 text-sm text-slate-700',
)} )}
href={child.href}> href={child.href}>
{child.name} {child.name}
@ -63,7 +63,7 @@ export default function ProductNavigation({ items, title }: Props) {
) : ( ) : (
<Link <Link
key={item.name} key={item.name}
className="hover:text-primary-600 text-sm font-medium text-gray-900" className="hover:text-primary-600 text-sm font-medium text-slate-900"
href={item.href}> href={item.href}>
{item.name} {item.name}
</Link> </Link>

@ -0,0 +1,76 @@
import React, { useState } from 'react';
import type { ComponentMeta } from '@storybook/react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
export default {
argTypes: {
disabled: {
control: 'boolean',
},
isLabelHidden: {
control: 'boolean',
},
label: {
control: 'text',
},
},
component: Typeahead,
parameters: {
docs: {
iframeHeight: 400,
inlineStories: false,
},
},
title: 'Typeahead',
} as ComponentMeta<typeof Typeahead>;
export function Basic({
disabled,
isLabelHidden,
label,
}: Pick<
React.ComponentProps<typeof Typeahead>,
'disabled' | 'isLabelHidden' | 'label'
>) {
const people = [
{ id: '1', label: 'Wade Cooper', value: '1' },
{ id: '2', label: 'Arlene Mccoy', value: '2' },
{ id: '3', label: 'Devon Webb', value: '3' },
{ id: '4', label: 'Tom Cook', value: '4' },
{ id: '5', label: 'Tanya Fox', value: '5' },
{ id: '6', label: 'Hellen Schmidt', value: '6' },
];
const [selectedEntry, setSelectedEntry] = useState<TypeaheadOption>(
people[0],
);
const [query, setQuery] = useState('');
const filteredPeople =
query === ''
? people
: people.filter((person) =>
person.label
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.toLowerCase().replace(/\s+/g, '')),
);
return (
<Typeahead
disabled={disabled}
isLabelHidden={isLabelHidden}
label={label}
options={filteredPeople}
selectedOption={selectedEntry}
onQueryChange={setQuery}
onSelectOption={setSelectedEntry}
/>
);
}
Basic.args = {
disabled: false,
isLabelHidden: false,
label: 'Author',
};

@ -8,7 +8,7 @@ export default function HorizontalDivider({ className }: Props) {
return ( return (
<hr <hr
aria-hidden={true} aria-hidden={true}
className={clsx('my-2 h-0 border-t border-slate-200', className)} className={clsx('my-2 h-0 border-t border-slate-100', className)}
/> />
); );
} }

@ -0,0 +1,109 @@
import clsx from 'clsx';
import { Fragment, useState } from 'react';
import { Combobox, Transition } from '@headlessui/react';
import { ChevronUpDownIcon } from '@heroicons/react/20/solid';
export type TypeaheadOption = Readonly<{
// String value to uniquely identify the option.
id: string;
label: string;
value: string;
}>;
type Props = Readonly<{
disabled?: boolean;
isLabelHidden?: boolean;
label: string;
onQueryChange: (
value: string,
event: React.ChangeEvent<HTMLInputElement>,
) => void;
onSelectOption: (option: TypeaheadOption) => void;
options: ReadonlyArray<TypeaheadOption>;
selectedOption: TypeaheadOption;
}>;
export default function Typeahead({
disabled = false,
isLabelHidden,
label,
options,
onQueryChange,
selectedOption,
onSelectOption,
}: Props) {
const [query, setQuery] = useState('');
return (
<Combobox
disabled={disabled}
value={selectedOption}
onChange={onSelectOption}>
<Combobox.Label
className={clsx(
isLabelHidden
? 'sr-only'
: 'mb-1 block text-sm font-medium text-slate-700',
)}>
{label}
</Combobox.Label>
<div className="relative">
<div className="focus-visible:ring-offset-primary-300 relative w-full cursor-default overflow-hidden rounded-lg border border-slate-300 bg-white text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm">
<Combobox.Input
className={clsx(
'w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-slate-900 focus:ring-0',
disabled && 'pointer-events-none select-none bg-slate-100',
)}
displayValue={(option) =>
(option as unknown as TypeaheadOption).label
}
onChange={(event) => {
!disabled && onQueryChange(event.target.value, event);
}}
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
aria-hidden="true"
className="h-5 w-5 text-slate-400"
/>
</Combobox.Button>
</div>
<Transition
afterLeave={() => setQuery('')}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{options.length === 0 && query !== '' ? (
<div className="relative cursor-default select-none py-2 px-4 text-slate-700">
Nothing found.
</div>
) : (
options.map((option) => (
<Combobox.Option
key={option.id}
className={({ active }) =>
clsx(
'relative cursor-default select-none py-2 px-4 text-slate-500',
active && 'bg-slate-100',
)
}
value={option}>
{({ selected }) => (
<span
className={clsx(
'block truncate',
selected ? 'font-medium' : 'font-normal',
)}>
{option.label}
</span>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
</div>
</Combobox>
);
}

@ -49,3 +49,6 @@ export { default as TextArea } from './TextArea/TextArea';
// TextInput // TextInput
export * from './TextInput/TextInput'; export * from './TextInput/TextInput';
export { default as TextInput } from './TextInput/TextInput'; export { default as TextInput } from './TextInput/TextInput';
// Typeahead
export * from './Typeahead/Typeahead';
export { default as Typeahead } from './Typeahead/Typeahead';

Loading…
Cancel
Save