154 lines
3.9 KiB
TypeScript
154 lines
3.9 KiB
TypeScript
|
import type { PropsWithChildren } from 'react';
|
||
|
import { useCallback, useState, useRef } from 'react';
|
||
|
|
||
|
import classNames from 'classnames';
|
||
|
|
||
|
import type { Placement, State as PopperState } from '@popperjs/core';
|
||
|
import Overlay from 'react-overlays/Overlay';
|
||
|
|
||
|
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||
|
import type { SelectItem } from 'mastodon/components/dropdown_selector';
|
||
|
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
|
||
|
import { Icon } from 'mastodon/components/icon';
|
||
|
|
||
|
interface DropdownProps {
|
||
|
value: string;
|
||
|
options: SelectItem[];
|
||
|
disabled?: boolean;
|
||
|
onChange: (value: string) => void;
|
||
|
placement?: Placement;
|
||
|
}
|
||
|
|
||
|
const Dropdown: React.FC<DropdownProps> = ({
|
||
|
value,
|
||
|
options,
|
||
|
disabled,
|
||
|
onChange,
|
||
|
placement: initialPlacement = 'bottom-end',
|
||
|
}) => {
|
||
|
const activeElementRef = useRef<Element | null>(null);
|
||
|
const containerRef = useRef(null);
|
||
|
const [isOpen, setOpen] = useState<boolean>(false);
|
||
|
const [placement, setPlacement] = useState<Placement>(initialPlacement);
|
||
|
|
||
|
const handleToggle = useCallback(() => {
|
||
|
if (
|
||
|
isOpen &&
|
||
|
activeElementRef.current &&
|
||
|
activeElementRef.current instanceof HTMLElement
|
||
|
) {
|
||
|
activeElementRef.current.focus({ preventScroll: true });
|
||
|
}
|
||
|
|
||
|
setOpen(!isOpen);
|
||
|
}, [isOpen, setOpen]);
|
||
|
|
||
|
const handleMouseDown = useCallback(() => {
|
||
|
if (!isOpen) activeElementRef.current = document.activeElement;
|
||
|
}, [isOpen]);
|
||
|
|
||
|
const handleKeyDown = useCallback(
|
||
|
(e: React.KeyboardEvent) => {
|
||
|
switch (e.key) {
|
||
|
case ' ':
|
||
|
case 'Enter':
|
||
|
if (!isOpen) activeElementRef.current = document.activeElement;
|
||
|
break;
|
||
|
}
|
||
|
},
|
||
|
[isOpen],
|
||
|
);
|
||
|
|
||
|
const handleClose = useCallback(() => {
|
||
|
if (
|
||
|
isOpen &&
|
||
|
activeElementRef.current &&
|
||
|
activeElementRef.current instanceof HTMLElement
|
||
|
)
|
||
|
activeElementRef.current.focus({ preventScroll: true });
|
||
|
setOpen(false);
|
||
|
}, [isOpen]);
|
||
|
|
||
|
const handleOverlayEnter = useCallback(
|
||
|
(state: Partial<PopperState>) => {
|
||
|
if (state.placement) setPlacement(state.placement);
|
||
|
},
|
||
|
[setPlacement],
|
||
|
);
|
||
|
|
||
|
const valueOption = options.find((item) => item.value === value);
|
||
|
|
||
|
return (
|
||
|
<div ref={containerRef}>
|
||
|
<button
|
||
|
type='button'
|
||
|
onClick={handleToggle}
|
||
|
onMouseDown={handleMouseDown}
|
||
|
onKeyDown={handleKeyDown}
|
||
|
disabled={disabled}
|
||
|
className={classNames('dropdown-button', { active: isOpen })}
|
||
|
>
|
||
|
<span className='dropdown-button__label'>{valueOption?.text}</span>
|
||
|
<Icon id='down' icon={ArrowDropDownIcon} />
|
||
|
</button>
|
||
|
|
||
|
<Overlay
|
||
|
show={isOpen}
|
||
|
offset={[5, 5]}
|
||
|
placement={placement}
|
||
|
flip
|
||
|
target={containerRef}
|
||
|
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
|
||
|
>
|
||
|
{({ props, placement }) => (
|
||
|
<div {...props}>
|
||
|
<div
|
||
|
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
|
||
|
>
|
||
|
<DropdownSelector
|
||
|
items={options}
|
||
|
value={value}
|
||
|
onClose={handleClose}
|
||
|
onChange={onChange}
|
||
|
classNamePrefix='privacy-dropdown'
|
||
|
/>
|
||
|
</div>
|
||
|
</div>
|
||
|
)}
|
||
|
</Overlay>
|
||
|
</div>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
interface Props {
|
||
|
value: string;
|
||
|
options: SelectItem[];
|
||
|
disabled?: boolean;
|
||
|
onChange: (value: string) => void;
|
||
|
}
|
||
|
|
||
|
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
|
||
|
value,
|
||
|
options,
|
||
|
disabled,
|
||
|
children,
|
||
|
onChange,
|
||
|
}) => {
|
||
|
return (
|
||
|
<label className='app-form__toggle'>
|
||
|
<div className='app-form__toggle__label'>{children}</div>
|
||
|
|
||
|
<div className='app-form__toggle__toggle'>
|
||
|
<div>
|
||
|
<Dropdown
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
disabled={disabled}
|
||
|
options={options}
|
||
|
/>
|
||
|
</div>
|
||
|
</div>
|
||
|
</label>
|
||
|
);
|
||
|
};
|