-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
PopupMenu.tsx
96 lines (83 loc) · 3.43 KB
/
PopupMenu.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { ComponentChild } from 'preact';
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
import useResizeObserver from 'use-resize-observer';
import { useDocumentEventListener } from '../hooks/useDocumentEventListener';
import { calcButtonContainerStyle, calcPopupMenuStyle, calcTriangleStyle } from '../lib/calc-style';
import { CursorPosition } from '../types';
import { PopupMenuButton } from './PopupMenu/Button';
const editor = document.querySelector<HTMLElement>('.editor')!;
export type Item = ComponentChild;
type PopupMenuProps = {
open: boolean;
emptyMessage?: string;
cursorPosition: CursorPosition;
items: Item[];
onSelect?: (item: Item, index: number) => void;
onSelectNonexistent?: () => void;
onClose?: () => void;
};
export function PopupMenu({
open,
emptyMessage,
cursorPosition,
items,
onSelect,
onSelectNonexistent,
onClose,
}: PopupMenuProps) {
const { ref, width: buttonContainerWidth = 0 } = useResizeObserver<HTMLDivElement>();
const isEmpty = useMemo(() => items.length === 0, [items.length]);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const { width: editorWidth = 0 } = useResizeObserver({ ref: editor });
// items が変わったら選択位置を 0 番目に戻す。ただし空なら null にセットする。
useEffect(() => {
setSelectedIndex(isEmpty ? null : 0);
}, [isEmpty, items]);
const handleKeydown = useCallback(
(e: KeyboardEvent) => {
// 閉じている時は何もしない
if (!open) return;
// IMEによる変換中は何もしない
if (e.isComposing) return;
const isTab = e.key === 'Tab' && !e.ctrlKey && !e.shiftKey && !e.altKey;
const isShiftTab = e.key === 'Tab' && !e.ctrlKey && e.shiftKey && !e.altKey;
const isEnter = e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.altKey;
const isEscape = e.key === 'Escape' && !e.ctrlKey && !e.shiftKey && !e.altKey;
if (isTab || isShiftTab || isEnter || isEscape) {
e.preventDefault();
e.stopPropagation();
}
if (isEmpty || selectedIndex === null) {
if (isEnter) onSelectNonexistent?.();
if (isEscape) onClose?.();
} else {
if (isTab) setSelectedIndex((selectedIndex + 1) % items.length);
if (isShiftTab) setSelectedIndex((selectedIndex - 1 + items.length) % items.length);
if (isEnter) onSelect?.(items[selectedIndex], selectedIndex);
if (isEscape) onClose?.();
}
},
[isEmpty, items, onClose, onSelect, onSelectNonexistent, open, selectedIndex],
);
useDocumentEventListener('keydown', handleKeydown, { capture: true });
const popupMenuStyle = calcPopupMenuStyle(cursorPosition);
const triangleStyle = calcTriangleStyle(cursorPosition, isEmpty);
const buttonContainerStyle = calcButtonContainerStyle(editorWidth, buttonContainerWidth, cursorPosition, isEmpty);
const itemListElement = items.map((item, i) => (
<PopupMenuButton key={i} selected={selectedIndex === i}>
{item}
</PopupMenuButton>
));
return (
<>
{open && (
<div className="popup-menu" style={popupMenuStyle} data-testid="popup-menu">
<div ref={ref} className="button-container" style={buttonContainerStyle}>
{items.length === 0 ? emptyMessage ?? 'アイテムは空です' : itemListElement}
</div>
<div className="triangle" style={triangleStyle} />
</div>
)}
</>
);
}