123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import './styles.scss';
|
|
import { memo, useEffect, useState, type KeyboardEvent } from 'react';
|
|
import { useCache } from './cache';
|
|
import { SectionRow } from './SectionRow';
|
|
import { AutocompleteSections } from './Autocomplete';
|
|
import type { WidgetProps } from './index';
|
|
import { WidgetPropsProvider } from './utils';
|
|
|
|
function handleKeyboardNav(ev: KeyboardEvent<HTMLDivElement>) {
|
|
const { currentTarget } = ev;
|
|
|
|
switch (ev.key) {
|
|
case 'Escape':
|
|
(currentTarget.querySelector('.form-control') as HTMLInputElement).focus();
|
|
break;
|
|
|
|
case 'ArrowDown':
|
|
if (!document.activeElement?.classList.contains('section-row')) {
|
|
const firstFocusTarget = currentTarget.querySelector('.section-row') as HTMLDivElement | undefined;
|
|
if (!firstFocusTarget) {
|
|
return;
|
|
}
|
|
|
|
firstFocusTarget.focus();
|
|
firstFocusTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
return;
|
|
}
|
|
|
|
const nextSibling = document.activeElement.nextElementSibling as HTMLDivElement | undefined;
|
|
if (nextSibling && nextSibling.classList.contains('section-row')) {
|
|
nextSibling.focus();
|
|
nextSibling.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case 'ArrowUp':
|
|
const prevSibling = document.activeElement?.previousElementSibling as HTMLDivElement | undefined;
|
|
if (prevSibling && prevSibling.classList.contains('section-row')) {
|
|
prevSibling.focus();
|
|
prevSibling.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
export function SectionsWidget(props: WidgetProps) {
|
|
const [shouldShow, setShouldShow] = useState(false);
|
|
const [value, setValue] = useState('');
|
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
|
const [cache] = useCache();
|
|
|
|
useEffect(() => {
|
|
function handleClick() {
|
|
setShouldShow(false);
|
|
}
|
|
|
|
document.addEventListener('click', handleClick);
|
|
|
|
return () => {
|
|
document.removeEventListener('click', handleClick);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const timeout = setTimeout(() => setDebouncedSearchTerm(value), 300);
|
|
|
|
return () => clearTimeout(timeout);
|
|
}, [value]);
|
|
|
|
return (
|
|
<div
|
|
onKeyUp={handleKeyboardNav}
|
|
onKeyDown={(ev) => {
|
|
if (ev.key === 'ArrowUp' || ev.key === 'ArrowDown') {
|
|
ev.preventDefault();
|
|
}
|
|
}}
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
setShouldShow(true);
|
|
}}
|
|
>
|
|
<input
|
|
onClick={() => {
|
|
cache._fetchTopLevelSections();
|
|
setShouldShow(true);
|
|
}}
|
|
type="text"
|
|
className="form-control"
|
|
onChange={(e) => setValue(e.target.value)}
|
|
value={value}
|
|
placeholder="Vyhledejte sekci"
|
|
onFocus={() => {
|
|
setShouldShow(true);
|
|
setValue('');
|
|
}}
|
|
/>
|
|
<WidgetPropsProvider value={{...props, hide: () => setShouldShow(false), show: () => setShouldShow(true) }}>
|
|
{shouldShow && <WidgetPopover searchTerm={debouncedSearchTerm} />}
|
|
</WidgetPropsProvider>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const WidgetPopover = memo(({ searchTerm }: { searchTerm: string }) => {
|
|
const topLevel = useCache('topLevelSections');
|
|
|
|
return (
|
|
<div className="autocomplete-window">
|
|
{!searchTerm && topLevel.map((section, i) => (
|
|
<SectionRow
|
|
section={section}
|
|
key={`t${section.id}-${i}`}
|
|
hasSubsections={!!section.has_subsections}
|
|
/>
|
|
))}
|
|
{!!searchTerm && <AutocompleteSections searchTerm={searchTerm} />}
|
|
</div>
|
|
);
|
|
});
|