Skip to content

Commit 8dcc297

Browse files
authored
SF-3451 Fix Lynx overlay RTL styles (#3367)
1 parent 7940f4c commit 8dcc297

File tree

6 files changed

+70
-13
lines changed

6 files changed

+70
-13
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-overlay.service.spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentPortal } from '@angular/cdk/portal';
33
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
44
import { Subject } from 'rxjs';
55
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';
6+
import { I18nService } from 'xforge-common/i18n.service';
67
import { configureTestingModule } from 'xforge-common/test-utils';
78
import { LynxEditor, LynxTextModelConverter } from './lynx-editor';
89
import { LynxInsight } from './lynx-insight';
@@ -11,13 +12,15 @@ import { LynxInsightOverlayComponent } from './lynx-insight-overlay/lynx-insight
1112

1213
const mockOverlay = mock(Overlay);
1314
const mockScrollDispatcher = mock(ScrollDispatcher);
15+
const mockedI18nService = mock(I18nService);
1416

1517
describe('LynxInsightOverlayService', () => {
1618
configureTestingModule(() => ({
1719
providers: [
1820
LynxInsightOverlayService,
1921
{ provide: Overlay, useMock: mockOverlay },
20-
{ provide: ScrollDispatcher, useMock: mockScrollDispatcher }
22+
{ provide: ScrollDispatcher, useMock: mockScrollDispatcher },
23+
{ provide: I18nService, useMock: mockedI18nService }
2124
]
2225
}));
2326

@@ -157,6 +160,28 @@ describe('LynxInsightOverlayService', () => {
157160
expect(env.service.close).not.toHaveBeenCalled();
158161
}));
159162
});
163+
164+
describe('direction handling', () => {
165+
it('should use rtl i18n direction when creating overlay config', fakeAsync(() => {
166+
const env = new TestEnvironment();
167+
when(mockedI18nService.direction).thenReturn('rtl');
168+
169+
env.openOverlay();
170+
171+
const capturedConfig = env.captureOverlayConfig();
172+
expect(capturedConfig.direction).toBe('rtl');
173+
}));
174+
175+
it('should use ltr i18n direction when creating overlay config', fakeAsync(() => {
176+
const env = new TestEnvironment();
177+
when(mockedI18nService.direction).thenReturn('ltr');
178+
179+
env.openOverlay();
180+
181+
const capturedConfig = env.captureOverlayConfig();
182+
expect(capturedConfig.direction).toBe('ltr');
183+
}));
184+
});
160185
});
161186

162187
class TestEnvironment {
@@ -345,6 +370,11 @@ class TestEnvironment {
345370
return portalCaptor.last()[0];
346371
}
347372

373+
captureOverlayConfig(): any {
374+
const configCaptor = capture(mockOverlay.create);
375+
return configCaptor.last()[0];
376+
}
377+
348378
/**
349379
* Simulates a click with optional element targeting.
350380
* @param selector If null, simulates a click outside. Otherwise, simulates a click on the given selector.

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-overlay.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentPortal } from '@angular/cdk/portal';
33
import { ScrollDispatcher } from '@angular/cdk/scrolling';
44
import { Injectable, NgZone } from '@angular/core';
55
import { asyncScheduler, observeOn, Subject, take, takeUntil } from 'rxjs';
6+
import { I18nService } from 'xforge-common/i18n.service';
67
import { LynxEditor, LynxTextModelConverter } from './lynx-editor';
78
import { LynxInsight } from './lynx-insight';
89
import { LynxInsightOverlayComponent } from './lynx-insight-overlay/lynx-insight-overlay.component';
@@ -23,7 +24,8 @@ export class LynxInsightOverlayService {
2324
constructor(
2425
private overlay: Overlay,
2526
private scrollDispatcher: ScrollDispatcher,
26-
private ngZone: NgZone
27+
private ngZone: NgZone,
28+
private i18n: I18nService
2729
) {}
2830

2931
get isOpen(): boolean {
@@ -125,7 +127,8 @@ export class LynxInsightOverlayService {
125127
return {
126128
positionStrategy: this.getPositionStrategy(origin),
127129
panelClass: 'lynx-insight-overlay-panel',
128-
scrollStrategy: this.overlay.scrollStrategies.reposition()
130+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
131+
direction: this.i18n.direction
129132
};
130133
}
131134

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-overlay/lynx-insight-overlay.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ <h1>
2525
}
2626
<div class="secondary-actions">
2727
@if (menuActions.length > 0) {
28-
<mat-icon [matTooltip]="t('tooltip_actions')" [matMenuTriggerFor]="actionMenu" class="action-menu-trigger">
28+
<mat-icon
29+
[matTooltip]="t('tooltip_actions')"
30+
[matMenuTriggerFor]="actionMenu"
31+
[dir]="i18n.direction"
32+
class="action-menu-trigger"
33+
>
2934
more_horiz
3035
</mat-icon>
3136
}

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-overlay/lynx-insight-overlay.component.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mat-icon {
3131

3232
.main-section {
3333
background-color: $mainSectionBGColor;
34+
position: relative;
3435

3536
// When prompt is for single insight, or insight is selected from multi-picker
3637
&.focused {
@@ -79,6 +80,8 @@ mat-icon {
7980

8081
h1 {
8182
font-size: 0.8em;
83+
padding-inline-end: 1em;
84+
margin-inline-end: auto;
8285
}
8386

8487
&:not(:last-child) {
@@ -102,8 +105,6 @@ mat-icon {
102105

103106
.ellipsis-icon {
104107
color: $secondaryTextColor;
105-
margin-inline-start: auto;
106-
padding-inline-start: 1em;
107108
flex-shrink: 0;
108109
}
109110
}
@@ -162,6 +163,7 @@ h1 {
162163
position: absolute;
163164
inset-inline-start: 0;
164165
font-size: 1.7em;
166+
direction: inherit;
165167
}
166168

167169
&:hover {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-overlay/lynx-insight-overlay.component.spec.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testi
33
import { By } from '@angular/platform-browser';
44
import { TextDocId } from 'src/app/core/models/text-doc';
55
import { anything, instance, mock, verify, when } from 'ts-mockito';
6+
import { I18nService } from 'xforge-common/i18n.service';
67
import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils';
78
import { UICommonModule } from 'xforge-common/ui-common.module';
89
import { LynxEditor, LynxTextModelConverter } from '../lynx-editor';
@@ -17,6 +18,7 @@ const mockLynxInsightOverlayService = mock(LynxInsightOverlayService);
1718
const mockLynxWorkspaceService = mock(LynxWorkspaceService);
1819
const mockLynxEditor = mock<LynxEditor>();
1920
const mockTextModelConverter = mock<LynxTextModelConverter>();
21+
const mockedI18nService = mock(I18nService);
2022

2123
// Default insight config
2224
const defaultInsightConfig: LynxInsightConfig = {
@@ -51,15 +53,11 @@ describe('LynxInsightOverlayComponent', () => {
5153
{ provide: LynxInsightStateService, useMock: mockLynxInsightStateService },
5254
{ provide: LynxInsightOverlayService, useMock: mockLynxInsightOverlayService },
5355
{ provide: LynxWorkspaceService, useMock: mockLynxWorkspaceService },
56+
{ provide: I18nService, useMock: mockedI18nService },
5457
{ provide: EDITOR_INSIGHT_DEFAULTS, useValue: defaultInsightConfig }
5558
]
5659
}));
5760

58-
it('should create', fakeAsync(() => {
59-
const env = new TestEnvironment();
60-
expect(env.component).toBeTruthy();
61-
}));
62-
6361
it('should update contents when primary action clicked', fakeAsync(() => {
6462
const env = new TestEnvironment();
6563

@@ -69,6 +67,22 @@ describe('LynxInsightOverlayComponent', () => {
6967
verify(mockLynxEditor.updateContents(anything(), 'user')).once();
7068
expect().nothing();
7169
}));
70+
71+
it('should use rtl direction when i18n.direction is rtl', fakeAsync(() => {
72+
when(mockedI18nService.direction).thenReturn('rtl');
73+
const env = new TestEnvironment();
74+
75+
const menuTrigger = env.fixture.debugElement.query(By.css('.action-menu-trigger'));
76+
expect(menuTrigger.nativeElement.getAttribute('dir')).toBe('rtl');
77+
}));
78+
79+
it('should use ltr direction when i18n.direction is ltr', fakeAsync(() => {
80+
when(mockedI18nService.direction).thenReturn('ltr');
81+
const env = new TestEnvironment();
82+
83+
const menuTrigger = env.fixture.debugElement.query(By.css('.action-menu-trigger'));
84+
expect(menuTrigger.nativeElement.getAttribute('dir')).toBe('ltr');
85+
}));
7286
});
7387

7488
class TestEnvironment {
@@ -89,15 +103,16 @@ class TestEnvironment {
89103
const textModelConverter = instance(mockTextModelConverter);
90104

91105
const insight = this.createTestInsight();
92-
const action = this.createTestAction(insight, true);
106+
const primaryAction = this.createTestAction(insight, true);
107+
const secondaryAction = this.createTestAction(insight, false);
93108

94109
// Set up mocks for the editor root element
95110
const mockRoot = document.createElement('div');
96111
when(mockLynxEditor.getRoot()).thenReturn(mockRoot);
97112
when(mockLynxEditor.focus()).thenReturn();
98113
when(mockLynxEditor.updateContents(anything(), anything())).thenReturn();
99114
when(mockTextModelConverter.dataDeltaToEditorDelta(anything())).thenCall(delta => delta);
100-
when(mockLynxWorkspaceService.getActions(anything())).thenResolve([action]);
115+
when(mockLynxWorkspaceService.getActions(anything())).thenResolve([primaryAction, secondaryAction]);
101116

102117
this.hostComponent.insights = [insight];
103118
this.hostComponent.editor = editor;

src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-overlay/lynx-insight-overlay.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Component, DestroyRef, ElementRef, EventEmitter, Inject, Input, OnInit,
33
import { MAT_TOOLTIP_DEFAULT_OPTIONS } from '@angular/material/tooltip';
44
import { Delta } from 'quill';
55
import { fromEvent } from 'rxjs';
6+
import { I18nService } from 'xforge-common/i18n.service';
67
import { quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util';
78
import { LynxEditor, LynxTextModelConverter } from '../lynx-editor';
89
import { EDITOR_INSIGHT_DEFAULTS, LynxInsight, LynxInsightAction, LynxInsightConfig } from '../lynx-insight';
@@ -58,6 +59,7 @@ export class LynxInsightOverlayComponent implements OnInit {
5859
private readonly insightState: LynxInsightStateService,
5960
private readonly overlayService: LynxInsightOverlayService,
6061
private readonly lynxWorkspaceService: LynxWorkspaceService,
62+
readonly i18n: I18nService,
6163
@Inject(DOCUMENT) private readonly document: Document,
6264
@Inject(EDITOR_INSIGHT_DEFAULTS) private readonly config: LynxInsightConfig
6365
) {}

0 commit comments

Comments
 (0)