From 6bc8187c8af8ab3eb74eee8c43f455c6838f65c6 Mon Sep 17 00:00:00 2001 From: yt314 Date: Fri, 18 Sep 2026 07:52:34 +0000 Subject: [PATCH] =?UTF-8?q?=D7=A0=D7=92=D7=99=D7=A9=D7=95=D7=AA:=20=D7=A9?= =?UTF-8?q?=D7=9D=20=D7=9C=D7=9B=D7=9C=20=D7=9B=D7=A4=D7=AA=D7=95=D7=A8,?= =?UTF-8?q?=20=D7=94=D7=97=D7=96=D7=A8=D7=AA=20=D7=9E=D7=99=D7=A7=D7=95?= =?UTF-8?q?=D7=93=20=D7=90=D7=97=D7=A8=D7=99=20=D7=93=D7=99=D7=90=D7=9C?= =?UTF-8?q?=D7=95=D7=92,=20=D7=95=D7=9B=D7=95=D7=AA=D7=A8=D7=AA=20=D7=9C?= =?UTF-8?q?=D7=9B=D7=9C=20=D7=9E=D7=A1=D7=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit שלושה דברים שמי שעובדת בעכבר לא מרגישה בהם, ומי שעובדת במקלדת או בקורא מסך נתקעת בהם. 1. כפתורי אייקון בלי שם. קורא מסך הקריא "לחצן" בלבד — בכרטיס תלמידה אחת עשרה פעמים ברצף, כולל מחיקת הערה וניתוק הורה. בפרופיל המורה גם עשרת שדות השעה של חלונות הזמינות היו בלי שם, כך שאי אפשר היה לדעת איזה יום ואיזה קצה. התוספת היא ariaLabel שמזהה את השורה: "מחיקת התשלום של דוד כהן מ־08/09/2026". הדפוס כבר קיים במערכת (פעמון ההתראות, מסך ההודעות) — רק לא הופעל כאן. 2. המיקוד אבד אחרי כל סגירת דיאלוג. p-dialog מעביר מיקוד פנימה בפתיחה ואין לו בכלל הגדרה להחזיר אותו בסגירה, כך שאחרי Escape המיקוד חזר ל-body וצריך היה לטאב מראש הדף. הנחיה על הבורר p-dialog זוכרת מה היה ממוקד ומחזירה — רק אם הכפתור עדיין קיים ואם אף אחד לא לקח את המיקוד בינתיים. 3. כותרת הדף הייתה "תלמידון" בכל מסך. באפליקציית עמוד יחיד אין טעינה שתכריז על מעבר מסך, והכותרת היא ההודעה היחידה שאומרת "עברת". נוספה TitleStrategy וכותרת לכל מסלול, זהה לכותרת שעל המסך. --- frontend/src/app/app.config.ts | 4 +- frontend/src/app/app.routes.ts | 35 +++++++- .../src/app/core/title/app-title.strategy.ts | 25 ++++++ .../contact-requests.component.html | 2 +- .../lessons-list/lessons-list.component.ts | 3 +- .../features/messages/messages.component.ts | 3 +- .../messages/portal-messages.component.ts | 3 +- .../parent-lessons.component.ts | 3 +- .../payments-list.component.html | 2 +- .../student-lessons.component.ts | 3 +- .../student-detail.component.html | 10 +-- .../student-detail.component.ts | 3 +- .../students-list.component.html | 2 +- .../students-list/students-list.component.ts | 3 +- .../teacher/profile/profile.component.html | 21 ++++- .../shared/a11y/restore-focus.directive.ts | 86 +++++++++++++++++++ .../app/shared/ui/beta-notice.component.ts | 3 +- .../app/shared/ui/idle-warning.component.ts | 3 +- .../app/shared/ui/quick-search.component.ts | 3 +- .../app/shared/ui/share-profile.component.ts | 3 +- 20 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 frontend/src/app/core/title/app-title.strategy.ts create mode 100644 frontend/src/app/shared/a11y/restore-focus.directive.ts diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 69347f7..e468c86 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; -import { provideRouter, withViewTransitions } from '@angular/router'; +import { TitleStrategy, provideRouter, withViewTransitions } from '@angular/router'; import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import { providePrimeNG } from 'primeng/config'; import { ConfirmationService, MessageService } from 'primeng/api'; @@ -8,6 +8,7 @@ import { routes } from './app.routes'; import { authInterceptor } from './core/auth/auth.interceptor'; import { HEBREW_TRANSLATION } from './core/i18n/primeng-hebrew'; import { TalmidonPreset } from './core/theme/talmidon-preset'; +import { AppTitleStrategy } from './core/title/app-title.strategy'; export const appConfig: ApplicationConfig = { providers: [ @@ -15,6 +16,7 @@ export const appConfig: ApplicationConfig = { // מעבר חלק בין מסכים במקום החלפה חדה; דפדפנים בלי התמיכה מתעלמים בשקט provideRouter(routes, withViewTransitions()), provideHttpClient(withFetch(), withInterceptors([authInterceptor])), + { provide: TitleStrategy, useClass: AppTitleStrategy }, MessageService, ConfirmationService, providePrimeNG({ diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 76f29a2..affcfb3 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -14,18 +14,21 @@ export const routes: Routes = [ }, { path: 'teachers/:id', + title: 'פרופיל מורה', loadComponent: () => import('./features/public/teacher-profile/teacher-profile.component').then(m => m.TeacherProfileComponent) }, - { path: 'login', component: LoginComponent }, - { path: 'register', component: RegisterComponent }, + { path: 'login', title: 'התחברות', component: LoginComponent }, + { path: 'register', title: 'הרשמה כמורה', component: RegisterComponent }, { path: 'forgot-password', + title: 'שחזור סיסמה', loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent) }, { path: 'set-password', + title: 'קביעת סיסמה', loadComponent: () => import('./features/auth/set-password/set-password.component').then(m => m.SetPasswordComponent) }, { @@ -38,49 +41,60 @@ export const routes: Routes = [ // מסך ההקמה עצמו מחוץ ל-profileSetupGuard, אחרת הוא היה מפנה אל עצמו בלולאה { path: 'setup', + title: 'הקמת הפרופיל', loadComponent: () => import('./features/teacher/profile-setup/profile-setup.component').then(m => m.ProfileSetupComponent) }, - { path: 'dashboard', component: DashboardComponent }, + { path: 'dashboard', title: 'ראשי', component: DashboardComponent }, { path: 'students', + title: 'תלמידים', loadComponent: () => import('./features/students/students-list/students-list.component').then(m => m.StudentsListComponent) }, { path: 'students/:id', + title: 'כרטיס תלמיד', loadComponent: () => import('./features/students/student-detail/student-detail.component').then(m => m.StudentDetailComponent) }, { path: 'lessons', + title: 'יומן שיעורים', loadComponent: () => import('./features/lessons/lessons-list/lessons-list.component').then(m => m.LessonsListComponent) }, { path: 'payments', + title: 'תשלומים', loadComponent: () => import('./features/payments/payments-list/payments-list.component').then(m => m.PaymentsListComponent) }, { path: 'reports', + title: 'דוחות', loadComponent: () => import('./features/reports/reports.component').then(m => m.ReportsComponent) }, { path: 'contact-requests', + title: 'פניות', loadComponent: () => import('./features/contact-requests/contact-requests.component').then(m => m.ContactRequestsComponent) }, { path: 'messages', + title: 'הודעות', loadComponent: () => import('./features/messages/messages.component').then(m => m.MessagesComponent) }, { path: 'lesson-plan', + title: 'בניית מערך שיעור', loadComponent: () => import('./features/ai/lesson-plan.component').then(m => m.LessonPlanComponent) }, { path: 'profile', + title: 'הגדרות פרופיל', loadComponent: () => import('./features/teacher/profile/profile.component').then(m => m.TeacherProfileSettingsComponent) }, { path: 'account', + title: 'הגדרות חשבון', loadComponent: () => import('./features/teacher/account-settings/account-settings.component').then(m => m.AccountSettingsComponent) } @@ -94,28 +108,34 @@ export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', + title: 'ראשי', loadComponent: () => import('./features/parent-portal/parent-dashboard/parent-dashboard.component').then(m => m.ParentDashboardComponent) }, { path: 'lessons', + title: 'יומן שיעורים', loadComponent: () => import('./features/parent-portal/parent-lessons/parent-lessons.component').then(m => m.ParentLessonsComponent) }, { path: 'notes', + title: 'הערות', loadComponent: () => import('./features/parent-portal/parent-notes/parent-notes.component').then(m => m.ParentNotesComponent) }, { path: 'payments', + title: 'תשלומים', loadComponent: () => import('./features/parent-portal/parent-payments/parent-payments.component').then(m => m.ParentPaymentsComponent) }, { path: 'resources', + title: 'חומרי לימוד', loadComponent: () => import('./features/parent-portal/parent-resources/parent-resources.component').then(m => m.ParentResourcesComponent) }, { path: 'messages', + title: 'הודעות', loadComponent: () => import('./features/messages/portal-messages.component').then(m => m.PortalMessagesComponent) } @@ -129,29 +149,35 @@ export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', + title: 'ראשי', loadComponent: () => import('./features/student-portal/student-dashboard/student-dashboard.component').then(m => m.StudentDashboardComponent) }, { path: 'lessons', + title: 'יומן שיעורים', loadComponent: () => import('./features/student-portal/student-lessons/student-lessons.component').then(m => m.StudentLessonsComponent) }, { path: 'notes', + title: 'הערות', loadComponent: () => import('./features/student-portal/student-notes/student-notes.component').then(m => m.StudentNotesComponent) }, { path: 'resources', + title: 'חומרי לימוד', loadComponent: () => import('./features/student-portal/student-resources/student-resources.component').then(m => m.StudentResourcesComponent) }, { path: 'games', + title: 'משחקים', loadComponent: () => import('./features/student-portal/games/games.component').then(m => m.StudentGamesComponent) }, { path: 'messages', + title: 'הודעות', loadComponent: () => import('./features/messages/portal-messages.component').then(m => m.PortalMessagesComponent) } @@ -165,14 +191,17 @@ export const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'teachers' }, { path: 'teachers', + title: 'מורות', loadComponent: () => import('./features/admin/admin-teachers/admin-teachers.component').then(m => m.AdminTeachersComponent) }, { path: 'subjects', + title: 'תחומי לימוד', loadComponent: () => import('./features/admin/admin-subjects/admin-subjects.component').then(m => m.AdminSubjectsComponent) }, { path: 'feedback', + title: 'הודעות מהאתר', loadComponent: () => import('./features/admin/admin-feedback/admin-feedback.component').then(m => m.AdminFeedbackComponent) } ] diff --git a/frontend/src/app/core/title/app-title.strategy.ts b/frontend/src/app/core/title/app-title.strategy.ts new file mode 100644 index 0000000..e0b1a71 --- /dev/null +++ b/frontend/src/app/core/title/app-title.strategy.ts @@ -0,0 +1,25 @@ +import { Injectable, inject } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; + +const APP_NAME = 'תלמידון'; + +/** + * מעדכן את כותרת הדף לפי המסלול. + * + * באפליקציה של עמוד יחיד הכותרת לא מתחלפת מעצמה, וכל המסכים נראו בלשונית + * הדפדפן ובהיסטוריה בשם אחד — "תלמידון". למי שעובדת עם קורא מסך זו גם ההודעה + * היחידה שאומרת "עברת מסך", כי אין טעינה מחדש שתכריז על זה. + * + * הכותרת של כל מסלול זהה לכותרת שרואים על המסך, כדי שלשונית והמסך יאמרו + * אותו דבר. + */ +@Injectable({ providedIn: 'root' }) +export class AppTitleStrategy extends TitleStrategy { + private readonly title = inject(Title); + + override updateTitle(snapshot: RouterStateSnapshot): void { + const page = this.buildTitle(snapshot); + this.title.setTitle(page ? `${page} · ${APP_NAME}` : APP_NAME); + } +} diff --git a/frontend/src/app/features/contact-requests/contact-requests.component.html b/frontend/src/app/features/contact-requests/contact-requests.component.html index 6572498..9e743ad 100644 --- a/frontend/src/app/features/contact-requests/contact-requests.component.html +++ b/frontend/src/app/features/contact-requests/contact-requests.component.html @@ -63,7 +63,7 @@

{{ contact.fullName }}

@if (contact.status !== Status.Closed) { } - + } diff --git a/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts b/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts index 9e343a2..d84f16d 100644 --- a/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts +++ b/frontend/src/app/features/lessons/lessons-list/lessons-list.component.ts @@ -41,6 +41,7 @@ import { CalendarEventsService } from '../calendar-events.service'; import { CalendarEventItem } from '../calendar-events.models'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; import { LessonAmountSuggestion, lessonDurationMinutes, suggestLessonAmountDetailed } from '../lesson-pricing.util'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-lessons-list', @@ -57,7 +58,7 @@ import { LessonAmountSuggestion, lessonDurationMinutes, suggestLessonAmountDetai SelectModule, TagModule, TextareaModule, - LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe], + LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './lessons-list.component.html' }) export class LessonsListComponent implements OnInit { diff --git a/frontend/src/app/features/messages/messages.component.ts b/frontend/src/app/features/messages/messages.component.ts index 0566003..360e40d 100644 --- a/frontend/src/app/features/messages/messages.component.ts +++ b/frontend/src/app/features/messages/messages.component.ts @@ -15,6 +15,7 @@ import { ConversationComponent } from './conversation.component'; import { MessageAuthor, MessageRecipient, ThreadDetail, ThreadSummary } from './messages.models'; import { MessagesService } from './messages.service'; import { ThreadListComponent } from './thread-list.component'; +import { RestoreFocusOnCloseDirective } from '../../shared/a11y/restore-focus.directive'; /** נמען עם התווית המוכנה — p-select מקבל שם שדה, לא פונקציה. */ type RecipientOption = MessageRecipient & { label: string }; @@ -34,7 +35,7 @@ type RecipientOption = MessageRecipient & { label: string }; PageHeaderComponent, ThreadListComponent, ConversationComponent - ], + , RestoreFocusOnCloseDirective], templateUrl: './messages.component.html', styleUrls: ['./messages.scss', './messages-screen.scss'] }) diff --git a/frontend/src/app/features/messages/portal-messages.component.ts b/frontend/src/app/features/messages/portal-messages.component.ts index 4151563..81358a6 100644 --- a/frontend/src/app/features/messages/portal-messages.component.ts +++ b/frontend/src/app/features/messages/portal-messages.component.ts @@ -16,6 +16,7 @@ import { ConversationComponent } from './conversation.component'; import { MessageAuthor, ThreadDetail, ThreadSummary } from './messages.models'; import { MessagesService } from './messages.service'; import { ThreadListComponent } from './thread-list.component'; +import { RestoreFocusOnCloseDirective } from '../../shared/a11y/restore-focus.directive'; /** * תיבת ההודעות של התלמידה ושל ההורה — אותו מסך לשניהם. @@ -37,7 +38,7 @@ import { ThreadListComponent } from './thread-list.component'; PageHeaderComponent, ThreadListComponent, ConversationComponent - ], + , RestoreFocusOnCloseDirective], templateUrl: './portal-messages.component.html', styleUrls: ['./messages.scss', './messages-screen.scss'] }) diff --git a/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts b/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts index b429547..6af8236 100644 --- a/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts +++ b/frontend/src/app/features/parent-portal/parent-lessons/parent-lessons.component.ts @@ -31,6 +31,7 @@ import { import { MyChild } from '../parent-portal.models'; import { ParentPortalService } from '../parent-portal.service'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-parent-lessons', @@ -42,7 +43,7 @@ import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; InputTextModule, SelectModule, TagModule, - LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe], + LessonCalendarComponent, PageHeaderComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './parent-lessons.component.html' }) export class ParentLessonsComponent implements OnInit { diff --git a/frontend/src/app/features/payments/payments-list/payments-list.component.html b/frontend/src/app/features/payments/payments-list/payments-list.component.html index 6e843dc..38797e1 100644 --- a/frontend/src/app/features/payments/payments-list/payments-list.component.html +++ b/frontend/src/app/features/payments/payments-list/payments-list.component.html @@ -240,7 +240,7 @@ - + diff --git a/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts b/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts index 6194d6a..5983cfc 100644 --- a/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts +++ b/frontend/src/app/features/student-portal/student-lessons/student-lessons.component.ts @@ -19,6 +19,7 @@ import { LESSON_STATUS_CLASS, LESSON_STATUS_LABELS, LESSON_STATUS_SEVERITY, Less import { StudentLesson } from '../student-portal.models'; import { StudentPortalService } from '../student-portal.service'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-student-lessons', @@ -29,7 +30,7 @@ import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; TagModule, TextareaModule, LessonCalendarComponent, - PageHeaderComponent, IsraelDatePipe], + PageHeaderComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './student-lessons.component.html' }) export class StudentLessonsComponent implements OnInit { diff --git a/frontend/src/app/features/students/student-detail/student-detail.component.html b/frontend/src/app/features/students/student-detail/student-detail.component.html index a335804..6ce7d49 100644 --- a/frontend/src/app/features/students/student-detail/student-detail.component.html +++ b/frontend/src/app/features/students/student-detail/student-detail.component.html @@ -54,12 +54,12 @@

הורים מקושרים

{{ parent.fullName }} · {{ parent.email }}
@if (hasWhatsapp(parent.phone)) { - + } @if (hasEmail(parent.email)) { - + } - +
} @@ -109,8 +109,8 @@

הערות פדגוגיות

{{ note.content }}

- - + +
diff --git a/frontend/src/app/features/students/student-detail/student-detail.component.ts b/frontend/src/app/features/students/student-detail/student-detail.component.ts index 0952cbf..c23a03f 100644 --- a/frontend/src/app/features/students/student-detail/student-detail.component.ts +++ b/frontend/src/app/features/students/student-detail/student-detail.component.ts @@ -32,6 +32,7 @@ import { ResourcesService } from '../../resources/resources.service'; import { ParentSummary, StudentDetail } from '../students.models'; import { StudentsService } from '../students.service'; import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-student-detail', @@ -50,7 +51,7 @@ import { IsraelDatePipe } from '../../../core/i18n/israel-date.pipe'; TextareaModule, TooltipModule, EmptyStateComponent, - ResourceListComponent, IsraelDatePipe], + ResourceListComponent, IsraelDatePipe, RestoreFocusOnCloseDirective], templateUrl: './student-detail.component.html' }) export class StudentDetailComponent implements OnInit { diff --git a/frontend/src/app/features/students/students-list/students-list.component.html b/frontend/src/app/features/students/students-list/students-list.component.html index b3db2fe..e13ce0f 100644 --- a/frontend/src/app/features/students/students-list/students-list.component.html +++ b/frontend/src/app/features/students/students-list/students-list.component.html @@ -91,7 +91,7 @@ {{ parent.phone || '—' }} {{ parent.studentCount }} - + diff --git a/frontend/src/app/features/students/students-list/students-list.component.ts b/frontend/src/app/features/students/students-list/students-list.component.ts index 3e3d907..df71c98 100644 --- a/frontend/src/app/features/students/students-list/students-list.component.ts +++ b/frontend/src/app/features/students/students-list/students-list.component.ts @@ -24,6 +24,7 @@ import { Parent } from '../../parents/parents.models'; import { ParentsService } from '../../parents/parents.service'; import { StudentListItem } from '../students.models'; import { StudentsService } from '../students.service'; +import { RestoreFocusOnCloseDirective } from '../../../shared/a11y/restore-focus.directive'; @Component({ selector: 'app-students-list', @@ -36,7 +37,7 @@ import { StudentsService } from '../students.service'; SelectModule, TableModule, TabsModule, - TagModule, PageHeaderComponent, EmptyStateComponent], + TagModule, PageHeaderComponent, EmptyStateComponent, RestoreFocusOnCloseDirective], templateUrl: './students-list.component.html' }) export class StudentsListComponent implements OnInit { diff --git a/frontend/src/app/features/teacher/profile/profile.component.html b/frontend/src/app/features/teacher/profile/profile.component.html index f52e0b1..2fb5356 100644 --- a/frontend/src/app/features/teacher/profile/profile.component.html +++ b/frontend/src/app/features/teacher/profile/profile.component.html @@ -216,10 +216,25 @@

שעות זמינות

@for (win of windowsForDay(day); track $index) {
- + עד - - + +
} @empty { לא זמין diff --git a/frontend/src/app/shared/a11y/restore-focus.directive.ts b/frontend/src/app/shared/a11y/restore-focus.directive.ts new file mode 100644 index 0000000..540421f --- /dev/null +++ b/frontend/src/app/shared/a11y/restore-focus.directive.ts @@ -0,0 +1,86 @@ +import { DOCUMENT, Directive, ElementRef, HostListener, Injectable, inject } from '@angular/core'; + +/** + * זוכר לאן המיקוד הלך לאחרונה, כדי שנדע להחזיר אותו כשדיאלוג נסגר. + * + * מאזין אחד לכל האפליקציה, נרשם בפעם הראשונה שדיאלוג כלשהו מבקש אותו. + * שומר היסטוריה קצרה ולא רק את האחרון, כדי שדיאלוג שנפתח מתוך דיאלוג יוכל + * למצוא את מה שהיה ממוקד לפניו ולא את מה שהיה לפני שניהם. + */ +@Injectable({ providedIn: 'root' }) +export class DialogFocusHistory { + private readonly doc = inject(DOCUMENT); + private readonly history: HTMLElement[] = []; + private listening = false; + + start(): void { + if (this.listening) return; + this.listening = true; + // בשלב הלכידה, כי ‎focusin‎ מבועבע אבל אנחנו רוצים לראות גם מה שנעצר בדרך + this.doc.addEventListener( + 'focusin', + (event) => { + const el = event.target as HTMLElement | null; + if (!el || typeof el.focus !== 'function') return; + this.history.push(el); + if (this.history.length > 5) this.history.shift(); + }, + true, + ); + } + + /** האחרון שהיה ממוקד מחוץ ל-‎container‎, אם הוא עדיין קיים במסמך. */ + lastOutside(container: HTMLElement): HTMLElement | null { + for (let i = this.history.length - 1; i >= 0; i--) { + const el = this.history[i]; + if (el.isConnected && !container.contains(el)) return el; + } + return null; + } +} + +/** + * מחזיר את המיקוד לכפתור שפתח את הדיאלוג, כשהדיאלוג נסגר. + * + * ‎p-dialog‎ מעביר את המיקוד לתוך הדיאלוג בפתיחה (‎focusOnShow‎) אבל לא מחזיר + * אותו בסגירה — אין לו בכלל הגדרה כזאת. התוצאה היא שמי שעובדת במקלדת מאבדת + * את המקום: אחרי Escape המיקוד חוזר ל-‎body‎, וצריך לטאב מראש הדף בכל פעם + * מחדש. מי שמשתמשת בעכבר לא מרגישה בזה בכלל. + * + * הבורר הוא ‎p-dialog‎ עצמו, כך שכל דיאלוג בקומפוננטה שמייבאת את ההנחיה מקבל + * את ההתנהגות בלי לשנות את התבנית. + */ +@Directive({ selector: 'p-dialog' }) +export class RestoreFocusOnCloseDirective { + private readonly doc = inject(DOCUMENT); + private readonly host = inject(ElementRef).nativeElement as HTMLElement; + private readonly focusHistory = inject(DialogFocusHistory); + private trigger: HTMLElement | null = null; + + constructor() { + this.focusHistory.start(); + } + + @HostListener('onShow') + protected rememberTrigger(): void { + this.trigger = this.focusHistory.lastOutside(this.host); + } + + @HostListener('onHide') + protected returnFocus(): void { + const target = this.trigger; + this.trigger = null; + if (!target) return; + + // אחרי סגירה יש עוד סיבוב של אנימציה והסרה מה-DOM; מחכים לו לפני שמחזירים + setTimeout(() => { + // הכפתור שפתח כבר לא קיים — מחיקה, למשל. עדיף להשאיר כמו שהוא מאשר + // לקפוץ למקום שרירותי. + if (!target.isConnected) return; + // מישהו כבר העביר את המיקוד למקום אמיתי — לא גוזלים אותו ממנו + const active = this.doc.activeElement; + if (active && active !== this.doc.body && this.doc.contains(active)) return; + target.focus(); + }); + } +} diff --git a/frontend/src/app/shared/ui/beta-notice.component.ts b/frontend/src/app/shared/ui/beta-notice.component.ts index 261d81f..7f8273d 100644 --- a/frontend/src/app/shared/ui/beta-notice.component.ts +++ b/frontend/src/app/shared/ui/beta-notice.component.ts @@ -8,6 +8,7 @@ import { InputTextModule } from 'primeng/inputtext'; import { TextareaModule } from 'primeng/textarea'; import { environment } from '../../../environments/environment'; import { extractErrorMessage } from '../../core/http/extract-error-message'; +import { RestoreFocusOnCloseDirective } from '../../shared/a11y/restore-focus.directive'; /** * תג "בהרצה" קבוע בפינה השמאלית התחתונה, ובלחיצה — טופס לשליחת רעיון או תקלה. @@ -25,7 +26,7 @@ import { extractErrorMessage } from '../../core/http/extract-error-message'; */ @Component({ selector: 'app-beta-notice', - imports: [FormsModule, ButtonModule, DialogModule, InputTextModule, TextareaModule], + imports: [FormsModule, ButtonModule, DialogModule, InputTextModule, TextareaModule, RestoreFocusOnCloseDirective], template: `