fix: repair Android startup and improve mobile web layouts
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m17s
ci / ts / ts (packages + web) (push) Successful in 2m22s
release / android (APK в релиз) (push) Successful in 4m15s
release / release (версия и заметки) (push) Successful in 6s

This commit is contained in:
mrleo1nid committed 2026-10-21 17:14:05 +03:00
1 parent 4c2286e70d
commit b0cbd0f149
30 files changed
+609 -140

No files matched your search

+43 -25
View File
@@ -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>
);
}