From 90f8556f8c6d143d3e577be40050d78f79c539b5 Mon Sep 17 00:00:00 2001 From: Yangshun Tay Date: Sun, 9 Oct 2022 17:52:39 +0800 Subject: [PATCH] [ui][typeahead] implementation --- .../components/global/ProductNavigation.tsx | 10 +- apps/storybook/stories/typeahead.stories.tsx | 76 ++++++++++++ .../HorizontalDivider/HorizontalDivider.tsx | 2 +- packages/ui/src/Typeahead/Typeahead.tsx | 109 ++++++++++++++++++ packages/ui/src/index.tsx | 3 + 5 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 apps/storybook/stories/typeahead.stories.tsx create mode 100644 packages/ui/src/Typeahead/Typeahead.tsx diff --git a/apps/portal/src/components/global/ProductNavigation.tsx b/apps/portal/src/components/global/ProductNavigation.tsx index e34cadd7..caae6c08 100644 --- a/apps/portal/src/components/global/ProductNavigation.tsx +++ b/apps/portal/src/components/global/ProductNavigation.tsx @@ -25,11 +25,11 @@ export default function ProductNavigation({ items, title }: Props) { {items.map((item) => item.children != null && item.children.length > 0 ? ( - + {item.name} ( {child.name} @@ -63,7 +63,7 @@ export default function ProductNavigation({ items, title }: Props) { ) : ( {item.name} diff --git a/apps/storybook/stories/typeahead.stories.tsx b/apps/storybook/stories/typeahead.stories.tsx new file mode 100644 index 00000000..0d846045 --- /dev/null +++ b/apps/storybook/stories/typeahead.stories.tsx @@ -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; + +export function Basic({ + disabled, + isLabelHidden, + label, +}: Pick< + React.ComponentProps, + '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( + 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 ( + + ); +} + +Basic.args = { + disabled: false, + isLabelHidden: false, + label: 'Author', +}; diff --git a/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx b/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx index c9f3d2b1..6e2396dc 100644 --- a/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx +++ b/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx @@ -8,7 +8,7 @@ export default function HorizontalDivider({ className }: Props) { return (
); } diff --git a/packages/ui/src/Typeahead/Typeahead.tsx b/packages/ui/src/Typeahead/Typeahead.tsx new file mode 100644 index 00000000..d80d9200 --- /dev/null +++ b/packages/ui/src/Typeahead/Typeahead.tsx @@ -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, + ) => void; + onSelectOption: (option: TypeaheadOption) => void; + options: ReadonlyArray; + selectedOption: TypeaheadOption; +}>; + +export default function Typeahead({ + disabled = false, + isLabelHidden, + label, + options, + onQueryChange, + selectedOption, + onSelectOption, +}: Props) { + const [query, setQuery] = useState(''); + return ( + + + {label} + +
+
+ + (option as unknown as TypeaheadOption).label + } + onChange={(event) => { + !disabled && onQueryChange(event.target.value, event); + }} + /> + + +
+ setQuery('')} + as={Fragment} + leave="transition ease-in duration-100" + leaveFrom="opacity-100" + leaveTo="opacity-0"> + + {options.length === 0 && query !== '' ? ( +
+ Nothing found. +
+ ) : ( + options.map((option) => ( + + clsx( + 'relative cursor-default select-none py-2 px-4 text-slate-500', + active && 'bg-slate-100', + ) + } + value={option}> + {({ selected }) => ( + + {option.label} + + )} + + )) + )} +
+
+
+
+ ); +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 1e4efb66..586920c7 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -49,3 +49,6 @@ export { default as TextArea } from './TextArea/TextArea'; // TextInput export * from './TextInput/TextInput'; export { default as TextInput } from './TextInput/TextInput'; +// Typeahead +export * from './Typeahead/Typeahead'; +export { default as Typeahead } from './Typeahead/Typeahead';