mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 18:05:55 +08:00
[ui][typeahead] add required field
This commit is contained in:
@ -20,6 +20,9 @@ export default {
|
|||||||
placeholder: {
|
placeholder: {
|
||||||
control: 'text',
|
control: 'text',
|
||||||
},
|
},
|
||||||
|
required: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
component: Typeahead,
|
component: Typeahead,
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -80,3 +83,39 @@ Basic.args = {
|
|||||||
isLabelHidden: false,
|
isLabelHidden: false,
|
||||||
label: 'Author',
|
label: 'Author',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function Required() {
|
||||||
|
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
|
||||||
|
label="Author"
|
||||||
|
options={filteredPeople}
|
||||||
|
required={true}
|
||||||
|
value={selectedEntry}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={setSelectedEntry}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import type { InputHTMLAttributes } from 'react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { Combobox, Transition } from '@headlessui/react';
|
import { Combobox, Transition } from '@headlessui/react';
|
||||||
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
||||||
@ -10,8 +11,18 @@ export type TypeaheadOption = Readonly<{
|
|||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type Attributes = Pick<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
| 'disabled'
|
||||||
|
| 'name'
|
||||||
|
| 'onBlur'
|
||||||
|
| 'onFocus'
|
||||||
|
| 'pattern'
|
||||||
|
| 'placeholder'
|
||||||
|
| 'required'
|
||||||
|
>;
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
disabled?: boolean;
|
|
||||||
isLabelHidden?: boolean;
|
isLabelHidden?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
noResultsMessage?: string;
|
noResultsMessage?: string;
|
||||||
@ -22,9 +33,9 @@ type Props = Readonly<{
|
|||||||
) => void;
|
) => void;
|
||||||
onSelect: (option: TypeaheadOption) => void;
|
onSelect: (option: TypeaheadOption) => void;
|
||||||
options: ReadonlyArray<TypeaheadOption>;
|
options: ReadonlyArray<TypeaheadOption>;
|
||||||
placeholder?: string;
|
|
||||||
value?: TypeaheadOption;
|
value?: TypeaheadOption;
|
||||||
}>;
|
}> &
|
||||||
|
Readonly<Attributes>;
|
||||||
|
|
||||||
export default function Typeahead({
|
export default function Typeahead({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@ -34,9 +45,10 @@ export default function Typeahead({
|
|||||||
nullable = false,
|
nullable = false,
|
||||||
options,
|
options,
|
||||||
onQueryChange,
|
onQueryChange,
|
||||||
|
required,
|
||||||
value,
|
value,
|
||||||
onSelect,
|
onSelect,
|
||||||
placeholder,
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
return (
|
return (
|
||||||
@ -68,6 +80,12 @@ export default function Typeahead({
|
|||||||
: 'mb-1 block text-sm font-medium text-slate-700',
|
: 'mb-1 block text-sm font-medium text-slate-700',
|
||||||
)}>
|
)}>
|
||||||
{label}
|
{label}
|
||||||
|
{required && (
|
||||||
|
<span aria-hidden="true" className="text-danger-500">
|
||||||
|
{' '}
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Combobox.Label>
|
</Combobox.Label>
|
||||||
<div className="relative">
|
<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">
|
<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">
|
||||||
@ -79,11 +97,12 @@ export default function Typeahead({
|
|||||||
displayValue={(option) =>
|
displayValue={(option) =>
|
||||||
(option as unknown as TypeaheadOption)?.label
|
(option as unknown as TypeaheadOption)?.label
|
||||||
}
|
}
|
||||||
placeholder={placeholder}
|
required={required}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setQuery(event.target.value);
|
setQuery(event.target.value);
|
||||||
onQueryChange(event.target.value, event);
|
onQueryChange(event.target.value, event);
|
||||||
}}
|
}}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
|
Reference in New Issue
Block a user