Skip to content

Commit 648dfa2

Browse files
authored
SF-3641 Refactor --project-font to app-root (#3565)
1 parent 03bf6db commit 648dfa2

22 files changed

+197
-50
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { DialogService } from 'xforge-common/dialog.service';
2626
import { ErrorReportingService } from 'xforge-common/error-reporting.service';
2727
import { ExternalUrlService } from 'xforge-common/external-url.service';
2828
import { FileService } from 'xforge-common/file.service';
29+
import { FontService } from 'xforge-common/font.service';
2930
import { I18nService } from 'xforge-common/i18n.service';
3031
import { LocationService } from 'xforge-common/location.service';
3132
import { UserDoc } from 'xforge-common/models/user-doc';
@@ -63,6 +64,7 @@ const mockedPwaService = mock(PwaService);
6364
const mockedI18nService = mock(I18nService);
6465
const mockedUrlService = mock(ExternalUrlService);
6566
const mockedFileService = mock(FileService);
67+
const mockedFontService = mock(FontService);
6668
const mockedErrorReportingService = mock(ErrorReportingService);
6769
const mockedDialogService = mock(DialogService);
6870

@@ -117,6 +119,7 @@ describe('AppComponent', () => {
117119
{ provide: ExternalUrlService, useMock: mockedUrlService },
118120
{ provide: OnlineStatusService, useClass: TestOnlineStatusService },
119121
{ provide: FileService, useMock: mockedFileService },
122+
{ provide: FontService, useMock: mockedFontService },
120123
{ provide: ErrorReportingService, useMock: mockedErrorReportingService },
121124
{ provide: BreakpointObserver, useClass: TestBreakpointObserver },
122125
{ provide: DialogService, useMock: mockedDialogService },
@@ -631,6 +634,61 @@ describe('AppComponent', () => {
631634
env.showHideUserMenu();
632635
}));
633636
});
637+
638+
describe('Project Font', () => {
639+
it('sets project font when project is selected', fakeAsync(() => {
640+
const env = new TestEnvironment();
641+
when(mockedFontService.getFontFamilyFromProject(anything())).thenReturn('Charis SIL');
642+
env.navigate(['/projects', 'project01']);
643+
env.init();
644+
645+
const hostElement = env.fixture.nativeElement as HTMLElement;
646+
expect(hostElement.style.getPropertyValue('--project-font')).toEqual('Charis SIL');
647+
verify(mockedFontService.getFontFamilyFromProject(anything())).once();
648+
}));
649+
650+
it('clears project font when no project is selected', fakeAsync(() => {
651+
const env = new TestEnvironment();
652+
when(mockedFontService.getFontFamilyFromProject(anything())).thenReturn('Charis SIL');
653+
env.navigate(['/projects', 'project01']);
654+
env.init();
655+
656+
const hostElement = env.fixture.nativeElement as HTMLElement;
657+
expect(hostElement.style.getPropertyValue('--project-font')).toEqual('Charis SIL');
658+
659+
env.navigate(['/projects']);
660+
env.wait();
661+
662+
// When null, the property should be empty string
663+
expect(hostElement.style.getPropertyValue('--project-font')).toEqual('');
664+
}));
665+
666+
it('updates project font when switching projects', fakeAsync(() => {
667+
const env = new TestEnvironment();
668+
when(mockedFontService.getFontFamilyFromProject(anything())).thenReturn('Charis SIL', 'Andika');
669+
env.navigate(['/projects', 'project01']);
670+
env.init();
671+
672+
const hostElement = env.fixture.nativeElement as HTMLElement;
673+
expect(hostElement.style.getPropertyValue('--project-font')).toEqual('Charis SIL');
674+
675+
env.navigate(['/projects', 'project02']);
676+
env.wait();
677+
678+
expect(hostElement.style.getPropertyValue('--project-font')).toEqual('Andika');
679+
verify(mockedFontService.getFontFamilyFromProject(anything())).twice();
680+
}));
681+
682+
it('sets CSS custom property on host element', fakeAsync(() => {
683+
const env = new TestEnvironment();
684+
when(mockedFontService.getFontFamilyFromProject(anything())).thenReturn('Noto Sans');
685+
env.navigate(['/projects', 'project01']);
686+
env.init();
687+
688+
const hostElement = env.fixture.nativeElement as HTMLElement;
689+
expect(hostElement.style.getPropertyValue('--project-font')).toEqual('Noto Sans');
690+
}));
691+
});
634692
});
635693

636694
class TestEnvironment {
@@ -730,6 +788,7 @@ class TestEnvironment {
730788
when(mockedUrlService.helps).thenReturn('helps');
731789
when(mockedUrlService.announcementPage).thenReturn('community-announcements');
732790
when(mockedUrlService.communitySupport).thenReturn('community-support');
791+
when(mockedFontService.getFontFamilyFromProject(anything())).thenReturn('Charis SIL');
733792

734793
if (initialConnectionStatus === 'offline') {
735794
this.goFullyOffline();

src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BidiModule } from '@angular/cdk/bidi';
22
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
33
import { CdkScrollable } from '@angular/cdk/scrolling';
44
import { AsyncPipe, DOCUMENT } from '@angular/common';
5-
import { Component, DestroyRef, Inject, OnDestroy, OnInit } from '@angular/core';
5+
import { Component, DestroyRef, HostBinding, Inject, OnDestroy, OnInit } from '@angular/core';
66
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
77
import { MatButton, MatIconAnchor, MatIconButton } from '@angular/material/button';
88
import { MatDivider } from '@angular/material/divider';
@@ -33,6 +33,7 @@ import { ExternalUrlService } from 'xforge-common/external-url.service';
3333
import { FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.service';
3434
import { FeatureFlagsDialogComponent } from 'xforge-common/feature-flags/feature-flags-dialog.component';
3535
import { FileService } from 'xforge-common/file.service';
36+
import { FontService } from 'xforge-common/font.service';
3637
import { I18nService } from 'xforge-common/i18n.service';
3738
import { LocationService } from 'xforge-common/location.service';
3839
import { Breakpoint, MediaBreakpointService } from 'xforge-common/media-breakpoints/media-breakpoint.service';
@@ -94,6 +95,9 @@ declare function gtag(...args: any): void;
9495
]
9596
})
9697
export class AppComponent extends DataLoadingComponent implements OnInit, OnDestroy {
98+
@HostBinding('style.--project-font')
99+
protected projectFont: string | null = null;
100+
97101
version: string = versionData.version;
98102
issueEmail: string = environment.issueEmail;
99103
versionNumberClickCount = 0;
@@ -133,6 +137,7 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
133137
readonly featureFlags: FeatureFlagService,
134138
private readonly pwaService: PwaService,
135139
private readonly themeService: ThemeService,
140+
private readonly fontService: FontService,
136141
onlineStatusService: OnlineStatusService,
137142
private destroyRef: DestroyRef
138143
) {
@@ -318,8 +323,10 @@ export class AppComponent extends DataLoadingComponent implements OnInit, OnDest
318323
.subscribe(async (selectedProjectDoc?: SFProjectProfileDoc) => {
319324
this._selectedProjectDoc = selectedProjectDoc;
320325
if (this._selectedProjectDoc == null || !this._selectedProjectDoc.isLoaded) {
326+
this.projectFont = null;
321327
return;
322328
}
329+
this.projectFont = this.fontService.getFontFamilyFromProject(this._selectedProjectDoc.data);
323330
void this.userService.setCurrentProjectId(this.currentUserDoc!, this._selectedProjectDoc.id);
324331
this.projectUserConfigDoc = await this.projectService.getUserConfig(
325332
this._selectedProjectDoc.id,

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ <h3 id="totalAnswersMessage">
9393
}
9494
@if (answer.scriptureText) {
9595
<div class="answer-scripture" dir="auto">
96-
<span class="answer-scripture-text" [style.--project-font]="projectFont">{{
96+
<span class="answer-scripture-text">{{
9797
(answer.selectionStartClipped ? "…" : "") +
9898
(answer.scriptureText || "") +
9999
(answer.selectionEndClipped ? "…" : "")

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-answers.component.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { toVerseRef, VerseRefData } from 'realtime-server/lib/esm/scriptureforge
2626
import { firstValueFrom, Subscription } from 'rxjs';
2727
import { DialogService } from 'xforge-common/dialog.service';
2828
import { FileService } from 'xforge-common/file.service';
29-
import { FontService } from 'xforge-common/font.service';
3029
import { I18nService } from 'xforge-common/i18n.service';
3130
import { FileType } from 'xforge-common/models/file-offline-data';
3231
import { NoticeService } from 'xforge-common/notice.service';
@@ -134,7 +133,6 @@ export class CheckingAnswersComponent implements OnInit {
134133
verseRef?: VerseRef;
135134
answersHighlightStatus: Map<string, boolean> = new Map<string, boolean>();
136135
submittingAnswer: boolean = false;
137-
projectFont?: string;
138136

139137
/** IDs of answers to show to user (so, excluding unshown incoming answers). */
140138
private _answersToShow: string[] = [];
@@ -155,7 +153,6 @@ export class CheckingAnswersComponent implements OnInit {
155153
private readonly questionDialogService: QuestionDialogService,
156154
private readonly i18n: I18nService,
157155
private readonly fileService: FileService,
158-
private readonly fontService: FontService,
159156
private readonly onlineStatusService: OnlineStatusService,
160157
private readonly projectService: SFProjectService,
161158
private destroyRef: DestroyRef
@@ -180,7 +177,6 @@ export class CheckingAnswersComponent implements OnInit {
180177
this.setProjectAdmin();
181178
});
182179
this.setProjectAdmin();
183-
this.projectFont = this.fontService.getFontFamilyFromProject(projectProfileDoc);
184180
}
185181

186182
@Input() set questionDoc(questionDoc: QuestionDoc | undefined) {

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-answers/checking-input-form/checking-input-form.component.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/
1212
import { VerseRefData } from 'realtime-server/lib/esm/scriptureforge/models/verse-ref-data';
1313
import { AutofocusDirective } from 'xforge-common/autofocus.directive';
1414
import { DialogService } from 'xforge-common/dialog.service';
15-
import { FontService } from 'xforge-common/font.service';
1615
import { I18nService } from 'xforge-common/i18n.service';
1716
import { Breakpoint, MediaBreakpointService } from 'xforge-common/media-breakpoints/media-breakpoint.service';
1817
import { NoticeService } from 'xforge-common/notice.service';
@@ -82,7 +81,6 @@ export class CheckingInputFormComponent {
8281
constructor(
8382
readonly noticeService: NoticeService,
8483
private readonly dialogService: DialogService,
85-
private readonly fontService: FontService,
8684
private readonly i18n: I18nService,
8785
private readonly breakpointObserver: BreakpointObserver,
8886
private readonly mediaBreakpointService: MediaBreakpointService,
@@ -121,7 +119,6 @@ export class CheckingInputFormComponent {
121119
chapterNum: (this.verseRef && this.verseRef.chapterNum) || verseRef.chapterNum,
122120
textsByBookId: this.textsByBookId,
123121
projectId: this._questionDoc.data.projectRef,
124-
projectFont: this.fontService.getFontFamilyFromProject(this.project),
125122
isRightToLeft: this.project?.isRightToLeft,
126123
selectedText: this.selectedText || '',
127124
selectedVerses: this.verseRef

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking-text/checking-text.component.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,4 @@
66
(loaded)="onLoaded()"
77
[isRightToLeft]="isRightToLeft"
88
[fontSize]="fontSize"
9-
[style.--project-font]="fontService.getFontFamilyFromProject(projectDoc)"
109
></app-text>

src/SIL.XForge.Scripture/ClientApp/src/app/checking/checking/checking.component.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,24 @@ describe('CheckingComponent', () => {
27082708
verify(chapterAudio.pause()).once();
27092709
expect(env.component.showScriptureAudioPlayer).toBe(true);
27102710
}));
2711+
2712+
it('answer-scripture-text inherits project font from CSS custom property', fakeAsync(() => {
2713+
const env = new TestEnvironment({ user: CHECKER_USER });
2714+
env.selectQuestion(6); // Question 6 has an answer with scripture text
2715+
2716+
const rootElement = document.documentElement;
2717+
rootElement.style.setProperty('--project-font', 'Charis SIL');
2718+
env.fixture.detectChanges();
2719+
tick();
2720+
2721+
const scriptureText = env.fixture.debugElement.query(By.css('.answer-scripture-text'));
2722+
expect(scriptureText).toBeTruthy();
2723+
const computedStyle = window.getComputedStyle(scriptureText.nativeElement);
2724+
expect(computedStyle.fontFamily).toContain('Charis SIL');
2725+
2726+
rootElement.style.removeProperty('--project-font');
2727+
env.waitForAudioPlayer();
2728+
}));
27112729
});
27122730
});
27132731

src/SIL.XForge.Scripture/ClientApp/src/app/text-chooser-dialog/text-chooser-dialog.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ <h2>{{ bookName }} {{ chapterNum }}</h2>
1212
[id]="textDocId"
1313
[multiSegmentSelection]="true"
1414
[isRightToLeft]="isTextRightToLeft"
15-
[style.--project-font]="projectFont"
1615
></app-text>
17-
<div class="selection-preview" [style.--project-font]="projectFont">
16+
<div class="selection-preview">
1817
<p dir="auto">
1918
{{ (startClipped ? "…" : "") + (selectedText || "") + (endClipped ? "…" : "") }}
2019
<span class="selection-reference">{{ referenceForDisplay }}</span>

src/SIL.XForge.Scripture/ClientApp/src/app/text-chooser-dialog/text-chooser-dialog.component.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,24 @@ describe('TextChooserDialogComponent', () => {
330330
expect(env.component.isTextRightToLeft).toBe(true);
331331
env.closeDialog();
332332
}));
333+
334+
it('selection-preview inherits project font from CSS custom property', fakeAsync(() => {
335+
const env = new TestEnvironment({ start: 0, end: TestEnvironment.segmentLen(1) }, 'verse_1_1', 'verse_1_1');
336+
env.fireSelectionChange();
337+
338+
const rootElement = document.documentElement;
339+
rootElement.style.setProperty('--project-font', 'Charis SIL');
340+
env.fixture.detectChanges();
341+
tick();
342+
343+
const selectionPreview = document.querySelector('.selection-preview');
344+
expect(selectionPreview).toBeTruthy();
345+
const computedStyle = window.getComputedStyle(selectionPreview!);
346+
expect(computedStyle.fontFamily).toContain('Charis SIL');
347+
348+
rootElement.style.removeProperty('--project-font');
349+
env.closeDialog();
350+
}));
333351
});
334352

335353
interface SimpleRange {

src/SIL.XForge.Scripture/ClientApp/src/app/text-chooser-dialog/text-chooser-dialog.component.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ import {
2828
ScriptureChooserDialogData
2929
} from '../scripture-chooser-dialog/scripture-chooser-dialog.component';
3030
import { TextComponent } from '../shared/text/text.component';
31+
3132
export interface TextChooserDialogData {
3233
bookNum: number;
3334
chapterNum: number;
34-
projectFont?: string;
3535
projectId: string;
3636
textsByBookId: TextsByBookId;
3737
isRightToLeft?: boolean;
@@ -106,10 +106,6 @@ export class TextChooserDialogComponent {
106106
return this.i18n.localizeBook(this.bookNum);
107107
}
108108

109-
get projectFont(): string {
110-
return this.data.projectFont ?? '';
111-
}
112-
113109
get isTextRightToLeft(): boolean {
114110
return this.data.isRightToLeft == null ? false : this.data.isRightToLeft;
115111
}

0 commit comments

Comments
 (0)