Refactor Checkbox and add Tristate

This commit is contained in:
IRBorisov 2023-09-07 16:30:43 +03:00
parent c58743bdb0
commit 75fd9a855c
17 changed files with 180 additions and 88 deletions

View File

@ -1,5 +1,6 @@
import { useRef } from 'react';
import { useMemo } from 'react';
import { CheckboxChecked } from '../Icons';
import Label from './Label';
export interface CheckboxProps {
@ -9,51 +10,52 @@ export interface CheckboxProps {
disabled?: boolean
widthClass?: string
tooltip?: string
value?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
value: boolean
setValue?: (newValue: boolean) => void
}
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, onChange }: CheckboxProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer';
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, setValue }: CheckboxProps) {
const cursor = useMemo(
() => {
if (disabled) {
return 'cursor-not-allowed';
} else if (setValue) {
return 'cursor-pointer';
} else {
return ''
}
}, [disabled, setValue]);
const bgColor = useMemo(
() => {
return value !== false ? 'clr-primary' : 'clr-app'
}, [value]);
function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
event.preventDefault();
if (!disabled) {
inputRef.current?.click();
if (disabled || !setValue) {
return;
}
setValue(!value);
}
return (
<button
className={'flex [&:not(:first-child)]:mt-3 clr-outline focus:outline-dotted focus:outline-1 ' + widthClass}
id={id}
className={`flex items-center [&:not(:first-child)]:mt-3 clr-outline focus:outline-dotted focus:outline-1 ${widthClass}`}
title={tooltip}
disabled={disabled}
onClick={handleClick}
>
<input id={id} type='checkbox' ref={inputRef}
className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox ${cursor}`}
required={required}
disabled={disabled}
checked={value}
onChange={onChange}
tabIndex={-1}
/>
<div className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none ${bgColor} ${cursor}`} />
{ label &&
<Label
className={`${cursor} px-2`}
className={`${cursor} px-2 text-start`}
text={label}
required={required}
htmlFor={id}
/>}
<svg
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
viewBox='0 0 512 512'
fill='currentColor'
>
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
</svg>
{value && <CheckboxChecked />}
</button>
);
}

View File

@ -1,15 +1,15 @@
import Checkbox from './Checkbox';
interface DropdownCheckboxProps {
value: boolean
label?: string
tooltip?: string
disabled?: boolean
value?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
setValue?: (newValue: boolean) => void
}
function DropdownCheckbox({ tooltip, onChange, disabled, ...props }: DropdownCheckboxProps) {
const behavior = (onChange && !disabled ? 'clr-hover' : '');
function DropdownCheckbox({ tooltip, setValue, disabled, ...props }: DropdownCheckboxProps) {
const behavior = (setValue && !disabled ? 'clr-hover' : '');
return (
<div
title={tooltip}
@ -18,7 +18,7 @@ function DropdownCheckbox({ tooltip, onChange, disabled, ...props }: DropdownChe
<Checkbox
widthClass='w-full'
disabled={disabled}
onChange={onChange}
setValue={setValue}
{...props}
/>
</div>

View File

@ -4,18 +4,22 @@ import { UploadIcon } from '../Icons';
import Button from './Button';
import Label from './Label';
interface FileInputProps {
id?: string
required?: boolean
interface FileInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title' | 'style' | 'accept' | 'type'> {
label: string
tooltip?: string
acceptType?: string
widthClass?: string
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onChange }: FileInputProps) {
function FileInput({
label, acceptType, tooltip,
widthClass = 'w-fit', onChange,
...props
}: FileInputProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [labelText, setLabelText] = useState('');
const [fileName, setFileName] = useState('');
const handleUploadClick = () => {
inputRef.current?.click();
@ -23,9 +27,9 @@ function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onC
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setLabelText(event.target.files[0].name)
setFileName(event.target.files[0].name)
} else {
setLabelText('')
setFileName('')
}
if (onChange) {
onChange(event);
@ -33,21 +37,22 @@ function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onC
};
return (
<div className={'flex flex-col gap-2 py-2 [&:not(:first-child)]:mt-3 items-start ' + widthClass}>
<input id={id} type='file'
<div className={`flex flex-col gap-2 py-2 mt-3 items-start ${widthClass}`}>
<input type='file'
ref={inputRef}
required={required}
style={{ display: 'none' }}
accept={acceptType}
onChange={handleFileChange}
{...props}
/>
<Button
text={label}
icon={<UploadIcon/>}
onClick={handleUploadClick}
tooltip={tooltip}
/>
<Label
text={labelText}
text={fileName}
/>
</div>
);

View File

@ -14,7 +14,7 @@ function SubmitButton({
return (
<button type='submit'
title={tooltip}
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${widthClass} ${loading ? ' cursor-progress' : ''}`}
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-semibold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${widthClass} ${loading ? ' cursor-progress' : ''}`}
disabled={disabled ?? loading}
>
{icon && <span>{icon}</span>}

View File

@ -1,9 +1,7 @@
import { type InputHTMLAttributes } from 'react';
import Label from './Label';
interface TextInputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> {
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> {
id: string
label: string
tooltip?: string

View File

@ -0,0 +1,64 @@
import { useMemo } from 'react';
import { CheckboxChecked, CheckboxNull } from '../Icons';
import { CheckboxProps } from './Checkbox';
import Label from './Label';
export interface TristateProps
extends Omit<CheckboxProps, 'value' | 'setValue'> {
value: boolean | null
setValue?: (newValue: boolean | null) => void
}
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, setValue }: TristateProps) {
const cursor = useMemo(
() => {
if (disabled) {
return 'cursor-not-allowed';
} else if (setValue) {
return 'cursor-pointer';
} else {
return ''
}
}, [disabled, setValue]);
const bgColor = useMemo(
() => {
return value !== false ? 'clr-primary' : 'clr-app'
}, [value]);
function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
event.preventDefault();
if (disabled || !setValue) {
return;
}
if (value === false) {
setValue(null);
} else {
setValue(!value);
}
}
return (
<button
id={id}
className={`flex items-center [&:not(:first-child)]:mt-3 clr-outline focus:outline-dotted focus:outline-1 ${widthClass}`}
title={tooltip}
disabled={disabled}
onClick={handleClick}
>
<div className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none ${bgColor} ${cursor}`} />
{ label &&
<Label
className={`${cursor} px-2 text-start`}
text={label}
required={required}
htmlFor={id}
/>}
{value && <CheckboxChecked />}
{value === null && <CheckboxNull />}
</button>
);
}
export default Checkbox;

View File

@ -317,3 +317,27 @@ export function InDoor(props: IconProps) {
</IconSVG>
);
}
export function CheckboxChecked() {
return (
<svg
className='absolute w-3 h-3 mt-1 ml-0.5'
viewBox='0 0 512 512'
fill='#ffffff'
>
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
</svg>
);
}
export function CheckboxNull() {
return (
<svg
className='absolute w-3 h-3 mt-1 ml-0.5'
viewBox='0 0 512 512'
fill='#ffffff'
>
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
</svg>
);
}

View File

@ -140,7 +140,6 @@
.clr-footer,
.clr-modal-backdrop,
.clr-btn-nav,
.clr-checkbox,
.clr-input:disabled
) {
background-color: var(--cl-bg-100);
@ -168,8 +167,7 @@
:is(.clr-primary,
.clr-btn-primary:hover,
.clr-btn-primary:focus,
.clr-checkbox:checked
.clr-btn-primary:focus
) {
color: var(--cl-prim-fg-100);
background-color: var(--cl-prim-bg-100);

View File

@ -93,7 +93,7 @@ function CreateRSFormPage() {
/>
<Checkbox id='common' label='Общедоступная схема'
value={common}
onChange={event => setCommon(event.target.checked)}
setValue={value => setCommon(value ?? false)}
/>
<FileInput id='trs' label='Загрузить из Экстеор'
acceptType='.trs'

View File

@ -36,38 +36,38 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
{ pickerMenu.isActive &&
<Dropdown>
<DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.MANUAL)}
setValue={() => handleChange(LibraryFilterStrategy.MANUAL)}
value={value === LibraryFilterStrategy.MANUAL}
label='Отображать все'
/>
<DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.COMMON)}
setValue={() => handleChange(LibraryFilterStrategy.COMMON)}
value={value === LibraryFilterStrategy.COMMON}
label='Общедоступные'
tooltip='Отображать только общедоступные схемы'
/>
<DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.CANONICAL)}
setValue={() => handleChange(LibraryFilterStrategy.CANONICAL)}
value={value === LibraryFilterStrategy.CANONICAL}
label='Неизменные'
tooltip='Отображать только стандартные схемы'
/>
<DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.PERSONAL)}
setValue={() => handleChange(LibraryFilterStrategy.PERSONAL)}
value={value === LibraryFilterStrategy.PERSONAL}
label='Личные'
disabled={!user}
tooltip='Отображать только подписки и владеемые схемы'
/>
<DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}
setValue={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}
value={value === LibraryFilterStrategy.SUBSCRIBE}
label='Подписки'
disabled={!user}
tooltip='Отображать только подписки'
/>
<DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.OWNED)}
setValue={() => handleChange(LibraryFilterStrategy.OWNED)}
value={value === LibraryFilterStrategy.OWNED}
disabled={!user}
label='Я - Владелец!'

View File

@ -79,7 +79,7 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
/>
<Checkbox id='common' label='Общедоступная схема'
value={common}
onChange={event => setCommon(event.target.checked)}
setValue={value => setCommon(value)}
/>
</Modal>
);

View File

@ -52,7 +52,7 @@ function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
<Checkbox
label='Удалить зависимые конституенты'
value={expandOut}
onChange={data => setExpandOut(data.target.checked)}
setValue={value => setExpandOut(value)}
/>
</div>
</Modal>

View File

@ -81,25 +81,25 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm }:DlgGraphOptionsProps
label='Скрыть текст'
tooltip='Не отображать термины'
value={noTerms}
onChange={ event => setNoTerms(event.target.checked) }
setValue={ value => setNoTerms(value) }
/>
<Checkbox
label='Скрыть несвязанные'
tooltip='Неиспользуемые конституенты'
value={noHermits}
onChange={ event => setNoHermits(event.target.checked) }
setValue={ value => setNoHermits(value) }
/>
<Checkbox
label='Скрыть шаблоны'
tooltip='Терм-функции и предикат-функции с параметризованными аргументами'
value={noTemplates}
onChange={ event => setNoTemplates(event.target.checked) }
setValue={ value => setNoTemplates(value) }
/>
<Checkbox
label='Транзитивная редукция'
tooltip='Удалить связи, образующие транзитивные пути в графе'
value={noTransitive}
onChange={ event => setNoTransitive(event.target.checked) }
setValue={ value => setNoTransitive(value) }
/>
</div>
<div className='flex flex-col'>
@ -107,42 +107,42 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm }:DlgGraphOptionsProps
<Checkbox
label={getCstTypeLabel(CstType.BASE)}
value={allowBase}
onChange={ event => setAllowBase(event.target.checked) }
setValue={ value => setAllowBase(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.STRUCTURED)}
value={allowStruct}
onChange={ event => setAllowStruct(event.target.checked) }
setValue={ value => setAllowStruct(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.TERM)}
value={allowTerm}
onChange={ event => setAllowTerm(event.target.checked) }
setValue={ value => setAllowTerm(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.AXIOM)}
value={allowAxiom}
onChange={ event => setAllowAxiom(event.target.checked) }
setValue={ value => setAllowAxiom(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.FUNCTION)}
value={allowFunction}
onChange={ event => setAllowFunction(event.target.checked) }
setValue={ value => setAllowFunction(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.PREDICATE)}
value={allowPredicate}
onChange={ event => setAllowPredicate(event.target.checked) }
setValue={ value => setAllowPredicate(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.CONSTANT)}
value={allowConstant}
onChange={ event => setAllowConstant(event.target.checked) }
setValue={ value => setAllowConstant(value) }
/>
<Checkbox
label={getCstTypeLabel(CstType.THEOREM)}
value={allowTheorem}
onChange={ event => setAllowTheorem(event.target.checked) }
setValue ={ value => setAllowTheorem(value) }
/>
</div>
</div>

View File

@ -44,7 +44,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
onSubmit={handleSubmit}
submitText='Загрузить'
>
<div className='max-w-[20rem]'>
<div className='flex flex-col items-center'>
<FileInput
label='Выбрать файл'
acceptType='.trs'
@ -53,7 +53,8 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
<Checkbox
label='Загружать название и комментарий'
value={loadMetadata}
onChange={event => setLoadMetadata(event.target.checked)}
setValue={value => setLoadMetadata(value)}
widthClass='w-fit pb-2'
/>
</div>
</Modal>

View File

@ -136,14 +136,14 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
value={common}
widthClass='w-fit mt-3'
disabled={!isEditable}
onChange={event => setCommon(event.target.checked)}
setValue={value => setCommon(value)}
/>
<Checkbox id='canonical' label='Неизменная схема'
widthClass='w-fit'
value={canonical}
tooltip='Только администраторы могут присваивать схемам неизменный статус'
disabled={!isEditable || !isForceAdmin}
onChange={event => setCanonical(event.target.checked)}
setValue={value => setCanonical(value)}
/>
</div>

View File

@ -415,18 +415,18 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
<Checkbox
label='Скрыть текст'
value={noTerms}
onChange={ event => setNoTerms(event.target.checked) }
setValue={ value => setNoTerms(value) }
/>
<Checkbox
label='Транзитивная редукция'
value={noTransitive}
onChange={ event => setNoTransitive(event.target.checked) }
setValue={ value => setNoTransitive(value) }
/>
<Checkbox
disabled={!is3D}
label='Анимация вращения'
value={orbit}
onChange={ event => setOrbit(event.target.checked) }
setValue={ value => setOrbit(value) }
/>
<Divider margins='mt-3 mb-2' />

View File

@ -146,14 +146,14 @@ function RSTabsMenu({
{(isOwned || user?.is_staff) &&
<DropdownCheckbox
value={isReadonly}
onChange={toggleReadonly}
setValue={toggleReadonly}
label='Я — читатель!'
tooltip='Режим чтения'
/>}
{user?.is_staff &&
<DropdownCheckbox
value={isForceAdmin}
onChange={toggleForceAdmin}
setValue={toggleForceAdmin}
label='Я — администратор!'
tooltip='Режим редактирования для администраторов'
/>}