feat(a11y): cross-platform hardening + modal focus-trap from impeccable audit

Responsive / viewport:
- add .min-h-viewport / .h-viewport utilities (100dvh + Telegram --tg-viewport-stable-height)
- migrate min-h-screen / h-screen / 100vh across 18 files
- reveal hover-only controls on focus-within + touch (desktop nav, admin settings)
- grid breakpoints (TopUpAmount, Contests), larger touch targets, nav aria-labels

Platform routing (Telegram WebView correctness):
- clipboard -> copyToClipboard util with execCommand fallback (10 files)
- window.open -> openTelegramLink / openLink (Profile, Referral, ChannelSubscriptionScreen)
- window.confirm -> useNativeDialog (admin pages); PromoOffersSection -> useDestructiveConfirm
- window.prompt -> usePrompt / PromptDialogHost (TipTap link editors)
- ESLint no-restricted-properties guard against regressions (adapters/clipboard util exempt)

Modal accessibility:
- new useFocusTrap hook (focus trap, Esc, scroll lock, focus restore)
- role=dialog / aria-modal / focus-trap: Polls, SuccessNotificationModal, AdminPolicies,
  AdminPromoGroups, AdminCampaigns, BroadcastPreview (Telegram + Email)
- new global usePrompt store + PromptDialogHost
This commit is contained in:
c0mrade
2026-05-25 23:16:07 +03:00
parent 2bcba3b60f
commit 8e0b63bac8
48 changed files with 584 additions and 173 deletions

View File

@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { promocodesApi, PromoGroup } from '../api/promocodes';
import { usePlatform } from '../platform/hooks/usePlatform';
import { useFocusTrap } from '../hooks/useFocusTrap';
// Icons
const BackIcon = () => (
@@ -61,6 +62,9 @@ export default function AdminPromoGroups() {
const { capabilities } = usePlatform();
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
const deleteDialogRef = useFocusTrap<HTMLDivElement>(!!deleteConfirm, {
onEscape: () => setDeleteConfirm(null),
});
// Query
const { data: groupsData, isLoading } = useQuery({
@@ -216,8 +220,20 @@ export default function AdminPromoGroups() {
{/* Delete Confirmation */}
{deleteConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="w-full max-w-sm rounded-xl bg-dark-800 p-6">
<h3 className="mb-2 text-lg font-semibold text-dark-100">
<div
className="absolute inset-0 bg-black/60"
onClick={() => setDeleteConfirm(null)}
aria-hidden="true"
/>
<div
ref={deleteDialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="promo-group-delete-title"
tabIndex={-1}
className="relative w-full max-w-sm rounded-xl bg-dark-800 p-6"
>
<h3 id="promo-group-delete-title" className="mb-2 text-lg font-semibold text-dark-100">
{t('admin.promoGroups.confirm.title')}
</h3>
<p className="mb-6 text-dark-400">{t('admin.promoGroups.confirm.text')}</p>