fix: repair Android startup and improve mobile web layouts
This commit is contained in:
1 parent
4c2286e70d
commit
b0cbd0f149
30 files changed
+609
-140
No files matched your search
@@ -196,6 +196,7 @@
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
padding: 16px;
|
||||
background: var(--overlay);
|
||||
animation: pipuIn 0.2s ease-out;
|
||||
@@ -317,3 +318,22 @@
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.button { white-space: normal; height: auto; min-height: 44px; padding: 10px 14px; }
|
||||
.iconButton { width: 44px; height: 44px; flex: none; }
|
||||
.input, .select { font-size: 16px; min-width: 0; }
|
||||
.segmented { flex-wrap: wrap; max-width: 100%; }
|
||||
.segmented button { min-height: 44px; height: auto; padding: 8px 12px; }
|
||||
.overlay { padding: 12px; z-index: 60; }
|
||||
.dialog { min-width: 0; overflow-wrap: anywhere; max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)); }
|
||||
.dialogHead { padding: 16px 16px 0; align-items: flex-start; }
|
||||
.dialogHead h2 { overflow-wrap: anywhere; font-size: 18px; }
|
||||
.dialogBody { padding: 16px; min-width: 0; }
|
||||
.field, .inputWrap { min-width: 0; }
|
||||
.dialogFoot { padding: 0 16px 16px; flex-wrap: wrap; }
|
||||
.dialogFoot > * { flex: 1 1 auto; }
|
||||
.menu { position: fixed; inset: auto 12px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
width: auto; min-width: 0; max-width: none; max-height: 75dvh; overflow-y: auto; z-index: 70; }
|
||||
.menuItem { min-height: 44px; height: auto; padding: 12px; white-space: normal; overflow-wrap: anywhere; }
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { closeIcon, visibilityIcon, visibilityOffIcon } from './icons';
|
||||
@@ -162,6 +163,18 @@ export function Dialog({
|
||||
(first ?? ref.current)?.focus();
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if (e.key === 'Tab') {
|
||||
const controls = [...(ref.current?.querySelectorAll<HTMLElement>(
|
||||
'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]',
|
||||
) ?? [])].filter((el) => el.getClientRects().length > 0);
|
||||
const first = controls[0];
|
||||
const last = controls[controls.length - 1];
|
||||
if (e.shiftKey && (document.activeElement === first || document.activeElement === ref.current)) {
|
||||
e.preventDefault(); last?.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault(); first?.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
@@ -169,7 +182,7 @@ export function Dialog({
|
||||
prev?.focus();
|
||||
};
|
||||
}, [onClose]);
|
||||
return (
|
||||
return createPortal(
|
||||
<div className={s.overlay} onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div ref={ref} className={s.dialog} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}>
|
||||
<div className={s.dialogHead}>
|
||||
@@ -181,7 +194,8 @@ export function Dialog({
|
||||
<div className={s.dialogBody}>{children}</div>
|
||||
{footer && <div className={s.dialogFoot}>{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,17 +219,18 @@ export function Menu({
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
if (!ref.current?.contains(e.target as Node)) setOpen(false);
|
||||
if (!ref.current?.contains(e.target as Node) && !menuRef.current?.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', onDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
ref.current?.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
||||
menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
@@ -224,11 +239,31 @@ export function Menu({
|
||||
const onMenuKey = (e: ReactKeyboardEvent) => {
|
||||
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
|
||||
e.preventDefault();
|
||||
const els = [...(ref.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
|
||||
const els = [...(menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? [])];
|
||||
const i = els.indexOf(document.activeElement as HTMLElement);
|
||||
const next = e.key === 'ArrowDown' ? (i + 1) % els.length : (i - 1 + els.length) % els.length;
|
||||
els[next]?.focus();
|
||||
};
|
||||
const menu = (
|
||||
<div ref={menuRef} className={cx(s.menu, align === 'start' && s.menuStart)} role="menu" aria-label={label} onKeyDown={onMenuKey}>
|
||||
{header}
|
||||
{items.map((it) => (
|
||||
<button
|
||||
key={it.label}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={cx(s.menuItem, it.danger && s.menuDanger)}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
it.onSelect();
|
||||
}}
|
||||
>
|
||||
{it.icon && <Icon src={it.icon} size={18} />}
|
||||
{it.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={s.menuWrap} ref={ref}>
|
||||
<button
|
||||
@@ -241,26 +276,9 @@ export function Menu({
|
||||
>
|
||||
{trigger}
|
||||
</button>
|
||||
{open && (
|
||||
<div className={cx(s.menu, align === 'start' && s.menuStart)} role="menu" aria-label={label} onKeyDown={onMenuKey}>
|
||||
{header}
|
||||
{items.map((it) => (
|
||||
<button
|
||||
key={it.label}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={cx(s.menuItem, it.danger && s.menuDanger)}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
it.onSelect();
|
||||
}}
|
||||
>
|
||||
{it.icon && <Icon src={it.icon} size={18} />}
|
||||
{it.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{open && (window.matchMedia?.('(max-width: 767px)').matches
|
||||
? createPortal(menu, document.body)
|
||||
: menu)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user