mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-14 18:05:55 +08:00
[ui] make disabled color more consistent
This commit is contained in:
@ -6,6 +6,7 @@ import { trpc } from '~/utils/trpc';
|
|||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
isLabelHidden?: boolean;
|
isLabelHidden?: boolean;
|
||||||
onSelect: (option: TypeaheadOption) => void;
|
onSelect: (option: TypeaheadOption) => void;
|
||||||
placeHolder?: string;
|
placeHolder?: string;
|
||||||
|
@ -74,11 +74,18 @@ export function HiddenLabel() {
|
|||||||
|
|
||||||
export function Disabled() {
|
export function Disabled() {
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
<TextArea
|
<TextArea
|
||||||
disabled={true}
|
disabled={true}
|
||||||
label="Comment"
|
label="Comment"
|
||||||
placeholder="You can't type here, it's disabled."
|
placeholder="You can't type here, it's disabled. (Placeholder)"
|
||||||
/>
|
/>
|
||||||
|
<TextArea
|
||||||
|
disabled={true}
|
||||||
|
label="Comment"
|
||||||
|
value="You can't type here, it's disabled. (Value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,9 +111,15 @@ export function Disabled() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
disabled={true}
|
disabled={true}
|
||||||
label="Disabled input"
|
label="Disabled input"
|
||||||
placeholder="John Doe"
|
placeholder="John Doe (Placeholder)"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
disabled={true}
|
||||||
|
label="Disabled input"
|
||||||
|
type="text"
|
||||||
|
value="John Doe (Value)"
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled={true}
|
disabled={true}
|
||||||
endAddOn={
|
endAddOn={
|
||||||
|
@ -128,3 +128,57 @@ export function Required() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Disabled() {
|
||||||
|
return (
|
||||||
|
<Typeahead
|
||||||
|
disabled={true}
|
||||||
|
label="Author"
|
||||||
|
options={[]}
|
||||||
|
placeholder="John Doe"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onQueryChange={() => {}}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Error() {
|
||||||
|
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
|
||||||
|
errorMessage={
|
||||||
|
selectedEntry.id === '1' ? 'Cannot select Wade Cooper' : undefined
|
||||||
|
}
|
||||||
|
label="Author"
|
||||||
|
options={filteredPeople}
|
||||||
|
required={true}
|
||||||
|
value={selectedEntry}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={setSelectedEntry}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -50,7 +50,7 @@ function CheckboxInput(
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'h-4 w-4 rounded border-slate-300',
|
'h-4 w-4 rounded border-slate-300',
|
||||||
disabled
|
disabled
|
||||||
? 'bg-slate-100 text-slate-400'
|
? 'bg-slate-50 text-slate-400'
|
||||||
: 'text-primary-600 focus:ring-primary-500',
|
: 'text-primary-600 focus:ring-primary-500',
|
||||||
)}
|
)}
|
||||||
defaultChecked={defaultValue}
|
defaultChecked={defaultValue}
|
||||||
|
@ -88,10 +88,9 @@ function Select<T>(
|
|||||||
aria-label={isLabelHidden ? label : undefined}
|
aria-label={isLabelHidden ? label : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
display === 'block' && 'block w-full',
|
display === 'block' && 'block w-full',
|
||||||
'rounded-md py-2 pl-3 pr-8 text-sm focus:outline-none',
|
'rounded-md py-2 pl-3 pr-8 text-sm focus:outline-none disabled:bg-slate-50 disabled:text-slate-500',
|
||||||
stateClasses[state],
|
stateClasses[state],
|
||||||
borderClasses[borderStyle],
|
borderClasses[borderStyle],
|
||||||
disabled && 'bg-slate-100',
|
|
||||||
)}
|
)}
|
||||||
defaultValue={defaultValue != null ? String(defaultValue) : undefined}
|
defaultValue={defaultValue != null ? String(defaultValue) : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -108,9 +108,8 @@ function TextArea(
|
|||||||
aria-describedby={hasError ? errorId : undefined}
|
aria-describedby={hasError ? errorId : undefined}
|
||||||
aria-invalid={hasError ? true : undefined}
|
aria-invalid={hasError ? true : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'block w-full rounded-md text-sm',
|
'block w-full rounded-md text-sm disabled:bg-slate-50 disabled:text-slate-500',
|
||||||
stateClasses[state].textArea,
|
stateClasses[state].textArea,
|
||||||
disabled && 'bg-slate-100',
|
|
||||||
resizeClasses[resize],
|
resizeClasses[resize],
|
||||||
)}
|
)}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
@ -143,7 +143,7 @@ function TextInput(
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex w-full overflow-hidden rounded-md border text-sm focus-within:ring-1',
|
'flex w-full overflow-hidden rounded-md border text-sm focus-within:ring-1',
|
||||||
disabled && 'pointer-events-none select-none bg-slate-100',
|
disabled && 'pointer-events-none select-none bg-slate-50',
|
||||||
containerClass,
|
containerClass,
|
||||||
)}>
|
)}>
|
||||||
{(() => {
|
{(() => {
|
||||||
@ -178,9 +178,8 @@ function TextInput(
|
|||||||
aria-describedby={hasError ? errorId : undefined}
|
aria-describedby={hasError ? errorId : undefined}
|
||||||
aria-invalid={hasError ? true : undefined}
|
aria-invalid={hasError ? true : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-0 flex-1 border-none text-sm focus:outline-none focus:ring-0',
|
'w-0 flex-1 border-none text-sm focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:bg-transparent disabled:text-slate-500',
|
||||||
inputClass,
|
inputClass,
|
||||||
disabled && 'bg-transparent',
|
|
||||||
)}
|
)}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { InputHTMLAttributes } from 'react';
|
import type { InputHTMLAttributes } from 'react';
|
||||||
|
import { useId } 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';
|
||||||
@ -24,6 +25,7 @@ type Attributes = Pick<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
|
errorMessage?: React.ReactNode;
|
||||||
isLabelHidden?: boolean;
|
isLabelHidden?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
noResultsMessage?: string;
|
noResultsMessage?: string;
|
||||||
@ -39,6 +41,27 @@ type Props = Readonly<{
|
|||||||
}> &
|
}> &
|
||||||
Readonly<Attributes>;
|
Readonly<Attributes>;
|
||||||
|
|
||||||
|
type State = 'error' | 'normal';
|
||||||
|
|
||||||
|
const stateClasses: Record<
|
||||||
|
State,
|
||||||
|
Readonly<{
|
||||||
|
container: string;
|
||||||
|
input: string;
|
||||||
|
}>
|
||||||
|
> = {
|
||||||
|
error: {
|
||||||
|
container:
|
||||||
|
'border-danger-300 focus-within:outline-none focus-within:ring-danger-500 focus-within:border-danger-500',
|
||||||
|
input: 'text-danger-900 placeholder-danger-300',
|
||||||
|
},
|
||||||
|
normal: {
|
||||||
|
container:
|
||||||
|
'focus-within:ring-primary-500 focus-within:border-primary-500 border-slate-300',
|
||||||
|
input: 'placeholder:text-slate-400',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const textSizes: Record<TypeaheadTextSize, string> = {
|
const textSizes: Record<TypeaheadTextSize, string> = {
|
||||||
default: 'text-sm',
|
default: 'text-sm',
|
||||||
inherit: '',
|
inherit: '',
|
||||||
@ -46,6 +69,7 @@ const textSizes: Record<TypeaheadTextSize, string> = {
|
|||||||
|
|
||||||
export default function Typeahead({
|
export default function Typeahead({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
errorMessage,
|
||||||
isLabelHidden,
|
isLabelHidden,
|
||||||
label,
|
label,
|
||||||
noResultsMessage = 'No results',
|
noResultsMessage = 'No results',
|
||||||
@ -58,7 +82,11 @@ export default function Typeahead({
|
|||||||
onSelect,
|
onSelect,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const hasError = errorMessage != null;
|
||||||
|
const errorId = useId();
|
||||||
|
const state: State = hasError ? 'error' : 'normal';
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Combobox
|
<Combobox
|
||||||
@ -102,14 +130,18 @@ export default function Typeahead({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'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',
|
'relative w-full cursor-default overflow-hidden rounded-md border text-left focus-within:ring-1',
|
||||||
|
disabled && 'pointer-events-none select-none bg-slate-50',
|
||||||
|
stateClasses[state].container,
|
||||||
textSizes[textSize],
|
textSizes[textSize],
|
||||||
)}>
|
)}>
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
|
aria-describedby={hasError ? errorId : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full border-none py-2 pl-3 pr-10 leading-5 text-slate-900 focus:ring-0',
|
'w-full border-none py-2 pl-3 pr-10 leading-5 focus:ring-0',
|
||||||
|
stateClasses[state].input,
|
||||||
textSizes[textSize],
|
textSizes[textSize],
|
||||||
disabled && 'pointer-events-none select-none bg-slate-100',
|
'disabled:cursor-not-allowed disabled:bg-transparent disabled:text-slate-500',
|
||||||
)}
|
)}
|
||||||
displayValue={(option) =>
|
displayValue={(option) =>
|
||||||
(option as unknown as TypeaheadOption)?.label
|
(option as unknown as TypeaheadOption)?.label
|
||||||
@ -170,6 +202,11 @@ export default function Typeahead({
|
|||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-danger-600 mt-2 text-sm" id={errorId}>
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user