135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
/**
|
|
* 시간대 선택 — 하단 바 클릭·설정 화면 공용
|
|
*/
|
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { TIMEZONE_OPTIONS, formatUnixClock, getTimezoneAbbr, normalizeTimezone } from '../utils/timezone';
|
|
|
|
interface Props {
|
|
value: string;
|
|
onChange: (tz: string) => void;
|
|
/** compact: 하단 바용 버튼 스타일 */
|
|
variant?: 'bar' | 'select';
|
|
/** 하단 바: 마지막 봉 시각 표시 (unix 초) */
|
|
displayUnix?: number;
|
|
className?: string;
|
|
}
|
|
|
|
const TimezonePicker: React.FC<Props> = ({ value, onChange, variant = 'bar', displayUnix, className = '' }) => {
|
|
const [open, setOpen] = useState(false);
|
|
const [menuPos, setMenuPos] = useState<{ bottom: number; right: number } | null>(null);
|
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
const menuRef = useRef<HTMLUListElement>(null);
|
|
const tz = normalizeTimezone(value);
|
|
|
|
const updateMenuPos = useCallback(() => {
|
|
const btn = triggerRef.current;
|
|
if (!btn) return;
|
|
const r = btn.getBoundingClientRect();
|
|
setMenuPos({
|
|
bottom: window.innerHeight - r.top + 4,
|
|
right: Math.max(8, window.innerWidth - r.right),
|
|
});
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
if (!open || variant !== 'bar') return;
|
|
updateMenuPos();
|
|
const onResize = () => updateMenuPos();
|
|
window.addEventListener('resize', onResize);
|
|
window.addEventListener('scroll', onResize, true);
|
|
return () => {
|
|
window.removeEventListener('resize', onResize);
|
|
window.removeEventListener('scroll', onResize, true);
|
|
};
|
|
}, [open, variant, updateMenuPos]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onDoc = (e: MouseEvent) => {
|
|
const t = e.target as Node;
|
|
if (rootRef.current?.contains(t) || menuRef.current?.contains(t)) return;
|
|
setOpen(false);
|
|
};
|
|
const id = window.setTimeout(() => {
|
|
document.addEventListener('mousedown', onDoc);
|
|
}, 0);
|
|
return () => {
|
|
window.clearTimeout(id);
|
|
document.removeEventListener('mousedown', onDoc);
|
|
};
|
|
}, [open]);
|
|
|
|
if (variant === 'select') {
|
|
return (
|
|
<select
|
|
className={`stg-select ${className}`}
|
|
value={tz}
|
|
onChange={e => onChange(e.target.value)}
|
|
>
|
|
{TIMEZONE_OPTIONS.map(o => (
|
|
<option key={o.id} value={o.id}>{o.label}</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
const menu = open && variant === 'bar' && menuPos && (
|
|
<ul
|
|
ref={menuRef}
|
|
className="tz-picker-menu tz-picker-menu--portal"
|
|
role="listbox"
|
|
style={{ bottom: menuPos.bottom, right: menuPos.right }}
|
|
>
|
|
{TIMEZONE_OPTIONS.map(o => (
|
|
<li key={o.id}>
|
|
<button
|
|
type="button"
|
|
role="option"
|
|
aria-selected={o.id === tz}
|
|
className={`tz-picker-item${o.id === tz ? ' active' : ''}`}
|
|
onClick={() => {
|
|
onChange(o.id);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<span className="tz-picker-item-label">{o.label}</span>
|
|
<span className="tz-picker-item-abbr">{o.abbr}</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
|
|
return (
|
|
<div className={`tz-picker ${className}`} ref={rootRef}>
|
|
<button
|
|
ref={triggerRef}
|
|
type="button"
|
|
className="tv-time-display tz-picker-trigger"
|
|
onClick={() => {
|
|
if (open) {
|
|
setOpen(false);
|
|
return;
|
|
}
|
|
updateMenuPos();
|
|
setOpen(true);
|
|
}}
|
|
title="시간대 변경"
|
|
aria-expanded={open}
|
|
aria-haspopup="listbox"
|
|
>
|
|
{displayUnix != null && displayUnix > 0 && (
|
|
<span className="tz-picker-clock">{formatUnixClock(displayUnix, tz, true)}</span>
|
|
)}
|
|
<span className="tz-picker-abbr">{getTimezoneAbbr(tz)}</span>
|
|
<span className="tz-picker-caret">▾</span>
|
|
</button>
|
|
{menu && createPortal(menu, document.body)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TimezonePicker;
|