Browse Source

fix(admin-ui): Add missing RTL compatibility to some admin-ui components (#2451)

HoseinGhanbari 2 years ago
parent
commit
ec61b58f96

+ 15 - 8
packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts

@@ -13,6 +13,11 @@ import { I18nService } from '../../providers/i18n/i18n.service';
 import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
 import { ModalService } from '../../providers/modal/modal.service';
 import { UiLanguageSwitcherDialogComponent } from '../ui-language-switcher-dialog/ui-language-switcher-dialog.component';
+import {
+    LocalizationDirectionType,
+    LocalizationLanguageCodeType,
+    LocalizationService,
+} from '../../providers/localization/localization.service';
 
 @Component({
     selector: 'vdr-app-shell',
@@ -22,8 +27,8 @@ import { UiLanguageSwitcherDialogComponent } from '../ui-language-switcher-dialo
 export class AppShellComponent implements OnInit {
     version = ADMIN_UI_VERSION;
     userName$: Observable<string>;
-    uiLanguageAndLocale$: Observable<[LanguageCode, string | undefined]>;
-    direction$: Observable<'ltr' | 'rtl'>;
+    uiLanguageAndLocale$: LocalizationLanguageCodeType;
+    direction$: LocalizationDirectionType;
     availableLanguages: LanguageCode[] = [];
     hideVendureBranding = getAppConfig().hideVendureBranding;
     pageTitle$: Observable<string>;
@@ -38,25 +43,27 @@ export class AppShellComponent implements OnInit {
         private modalService: ModalService,
         private localStorageService: LocalStorageService,
         private breadcrumbService: BreadcrumbService,
+        private localizationService: LocalizationService,
     ) {}
 
     ngOnInit() {
+        this.direction$ = this.localizationService.direction$;
+
+        this.uiLanguageAndLocale$ = this.localizationService.uiLanguageAndLocale$;
+
         this.userName$ = this.dataService.client
             .userStatus()
             .single$.pipe(map(data => data.userStatus.username));
-        this.uiLanguageAndLocale$ = this.dataService.client
-            .uiState()
-            .stream$.pipe(map(({ uiState }) => [uiState.language, uiState.locale ?? undefined]));
+
         this.availableLanguages = this.i18nService.availableLanguages;
+
         this.pageTitle$ = this.breadcrumbService.breadcrumbs$.pipe(
             map(breadcrumbs => breadcrumbs[breadcrumbs.length - 1].label),
         );
+
         this.mainNavExpanded$ = this.dataService.client
             .uiState()
             .stream$.pipe(map(({ uiState }) => uiState.mainNavExpanded));
-        this.direction$ = this.uiLanguageAndLocale$.pipe(
-            map(([languageCode]) => (this.i18nService.isRTL(languageCode) ? 'rtl' : 'ltr')),
-        );
     }
 
     selectUiLanguage() {

+ 8 - 13
packages/admin-ui/src/lib/core/src/components/notification/notification.component.html

@@ -1,15 +1,10 @@
-<div
-    class="notification-wrapper"
-    #wrapper
-    [style.top.px]="offsetTop"
-    [ngClass]="{
-        visible: isVisible,
-        info: type === 'info',
-        success: type === 'success',
-        error: type === 'error',
-        warning: type === 'warning'
-    }"
->
+<div [dir]="direction$ | async" class="notification-wrapper" #wrapper [style.top.px]="offsetTop" [ngClass]="{
+            visible: isVisible,
+            info: type === 'info',
+            success: type === 'success',
+            error: type === 'error',
+            warning: type === 'warning'
+        }">
     <clr-icon [attr.shape]="getIcon()" size="24"></clr-icon>
     {{ stringifyMessage(message) | translate: translationVars }}
-</div>
+</div>

+ 18 - 2
packages/admin-ui/src/lib/core/src/components/notification/notification.component.ts

@@ -1,13 +1,20 @@
-import { Component, ElementRef, HostListener, ViewChild } from '@angular/core';
+import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
 
 import { NotificationType } from '../../providers/notification/notification.service';
 
+import {
+    LocalizationDirectionType,
+    LocalizationService,
+} from '../../providers/localization/localization.service';
+
 @Component({
     selector: 'vdr-notification',
     templateUrl: './notification.component.html',
     styleUrls: ['./notification.component.scss'],
 })
-export class NotificationComponent {
+export class NotificationComponent implements OnInit {
+    direction$: LocalizationDirectionType;
+
     @ViewChild('wrapper', { static: true }) wrapper: ElementRef;
     offsetTop = 0;
     message = '';
@@ -18,6 +25,15 @@ export class NotificationComponent {
         /* */
     };
 
+    /**
+     *
+     */
+    constructor(private localizationService: LocalizationService) {}
+
+    ngOnInit(): void {
+        this.direction$ = this.localizationService.direction$;
+    }
+
     registerOnClickFn(fn: () => void): void {
         this.onClickFn = fn;
     }

+ 15 - 1
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.scss

@@ -1,6 +1,6 @@
 :host {
     display: flex;
-    justify-content: start;
+    justify-content: flex-start;
     align-items: center;
 }
 
@@ -27,10 +27,24 @@ button.theme-toggle {
         left: 0px;
         opacity: 1;
     }
+
     &.default.active {
         color: #d6ae3f;
     }
+
     &.dark.active {
         color: #ffdf3a;
     }
 }
+
+:host-context([dir='rtl']) {
+    .theme-icon {
+        left: auto;
+        right: 6px;
+
+        &.active {
+            left: auto;
+            right: 0px;
+        }
+    }
+}

+ 29 - 0
packages/admin-ui/src/lib/core/src/providers/localization/localization.service.spec.ts

@@ -0,0 +1,29 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { TestingCommonModule } from '../../../../../testing/testing-common.module';
+import { MockI18nService } from '../i18n/i18n.service.mock';
+import { DataService } from '../../data/providers/data.service';
+import { I18nService } from '../../providers/i18n/i18n.service';
+import { LocalizationService } from './localization.service';
+
+describe('LocalizationService', () => {
+    let service: LocalizationService;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            imports: [TestingCommonModule],
+            providers: [
+                LocalizationService,
+                { provide: I18nService, useClass: MockI18nService },
+                { provide: DataService, useClass: class {} },
+            ],
+            schemas: [CUSTOM_ELEMENTS_SCHEMA],
+        });
+        service = TestBed.inject(LocalizationService);
+    });
+
+    it('should be created', () => {
+        expect(service).toBeTruthy();
+    });
+});

+ 34 - 0
packages/admin-ui/src/lib/core/src/providers/localization/localization.service.ts

@@ -0,0 +1,34 @@
+import { Injectable } from '@angular/core';
+import { Observable, map } from 'rxjs';
+
+import { DataService } from '../../data/providers/data.service';
+import { I18nService } from '../../providers/i18n/i18n.service';
+import { LanguageCode } from '../../common/generated-types';
+
+export type LocalizationDirectionType = Observable<'ltr' | 'rtl'>;
+export type LocalizationLanguageCodeType = Observable<[LanguageCode, string | undefined]>;
+
+/**
+ * @description
+ * Provides localization helper functionality.
+ *
+ */
+@Injectable({
+    providedIn: 'root',
+})
+export class LocalizationService {
+    uiLanguageAndLocale$: LocalizationLanguageCodeType;
+    direction$: LocalizationDirectionType;
+
+    constructor(private i18nService: I18nService, private dataService: DataService) {
+        this.uiLanguageAndLocale$ = this.dataService.client
+            ?.uiState()
+            ?.stream$?.pipe(map(({ uiState }) => [uiState.language, uiState.locale ?? undefined]));
+
+        this.direction$ = this.uiLanguageAndLocale$?.pipe(
+            map(([languageCode]) => {
+                return this.i18nService.isRTL(languageCode) ? 'rtl' : 'ltr';
+            }),
+        );
+    }
+}

+ 20 - 19
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts

@@ -1,18 +1,9 @@
-import {
-    ConnectedPosition,
-    HorizontalConnectionPos,
-    Overlay,
-    OverlayRef,
-    PositionStrategy,
-    VerticalConnectionPos,
-} from '@angular/cdk/overlay';
+import { ConnectedPosition, Overlay, OverlayRef, PositionStrategy } from '@angular/cdk/overlay';
 import { TemplatePortal } from '@angular/cdk/portal';
 import {
     AfterViewInit,
     ChangeDetectionStrategy,
     Component,
-    ContentChild,
-    ElementRef,
     HostListener,
     Input,
     OnDestroy,
@@ -23,7 +14,10 @@ import {
 } from '@angular/core';
 import { Subscription } from 'rxjs';
 
-import { DropdownTriggerDirective } from './dropdown-trigger.directive';
+import {
+    LocalizationDirectionType,
+    LocalizationService,
+} from '../../../providers/localization/localization.service';
 import { DropdownComponent } from './dropdown.component';
 
 export type DropdownPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
@@ -41,14 +35,16 @@ export type DropdownPosition = 'top-left' | 'top-right' | 'bottom-left' | 'botto
     selector: 'vdr-dropdown-menu',
     template: `
         <ng-template #menu>
-            <div class="dropdown open">
-                <div class="dropdown-menu" [ngClass]="customClasses">
-                    <div
-                        class="dropdown-content-wrapper"
-                        [cdkTrapFocus]="true"
-                        [cdkTrapFocusAutoCapture]="true"
-                    >
-                        <ng-content></ng-content>
+            <div [dir]="direction$ | async">
+                <div class="dropdown open">
+                    <div class="dropdown-menu" [ngClass]="customClasses">
+                        <div
+                            class="dropdown-content-wrapper"
+                            [cdkTrapFocus]="true"
+                            [cdkTrapFocusAutoCapture]="true"
+                        >
+                            <ng-content></ng-content>
+                        </div>
                     </div>
                 </div>
             </div>
@@ -58,6 +54,8 @@ export type DropdownPosition = 'top-left' | 'top-right' | 'bottom-left' | 'botto
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
+    direction$: LocalizationDirectionType;
+
     @Input('vdrPosition') private position: DropdownPosition = 'bottom-left';
     @Input() customClasses: string;
     @ViewChild('menu', { static: true }) private menuTemplate: TemplateRef<any>;
@@ -104,9 +102,12 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
         private overlay: Overlay,
         private viewContainerRef: ViewContainerRef,
         private dropdown: DropdownComponent,
+        private localizationService: LocalizationService,
     ) {}
 
     ngOnInit(): void {
+        this.direction$ = this.localizationService.direction$;
+
         this.dropdown.onOpenChange(isOpen => {
             if (isOpen) {
                 this.overlayRef.attach(this.menuPortal);

+ 14 - 18
packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.html

@@ -1,18 +1,14 @@
-<clr-modal
-    [clrModalOpen]="true"
-    (clrModalOpenChange)="modalOpenChange($event)"
-    [clrModalClosable]="options?.closable"
-    [clrModalSize]="options?.size"
-    [ngClass]="'modal-valign-' + (options?.verticalAlign || 'center')"
->
-    <h3 class="modal-title"><ng-container *ngTemplateOutlet="(titleTemplateRef$ | async)"></ng-container></h3>
-    <div class="modal-body">
-        <vdr-dialog-component-outlet
-            [component]="childComponentType"
-            (create)="onCreate($event)"
-        ></vdr-dialog-component-outlet>
-    </div>
-    <div class="modal-footer">
-        <ng-container *ngTemplateOutlet="(buttonsTemplateRef$ | async)"></ng-container>
-    </div>
-</clr-modal>
+<div [dir]="direction$ | async">
+    <clr-modal [clrModalOpen]="true" (clrModalOpenChange)="modalOpenChange($event)"
+        [clrModalClosable]="options?.closable" [clrModalSize]="options?.size"
+        [ngClass]="'modal-valign-' + (options?.verticalAlign || 'center')">
+        <h3 class="modal-title"><ng-container *ngTemplateOutlet="(titleTemplateRef$ | async)"></ng-container></h3>
+        <div class="modal-body">
+            <vdr-dialog-component-outlet [component]="childComponentType"
+                (create)="onCreate($event)"></vdr-dialog-component-outlet>
+        </div>
+        <div class="modal-footer">
+            <ng-container *ngTemplateOutlet="(buttonsTemplateRef$ | async)"></ng-container>
+        </div>
+    </clr-modal>
+</div>

+ 18 - 12
packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.ts

@@ -1,15 +1,10 @@
-import {
-    Component,
-    ContentChild,
-    ContentChildren,
-    QueryList,
-    TemplateRef,
-    Type,
-    ViewChild,
-    ViewChildren,
-} from '@angular/core';
-import { Observable, Subject } from 'rxjs';
+import { Component, OnInit, TemplateRef, Type } from '@angular/core';
+import { Subject } from 'rxjs';
 
+import {
+    LocalizationDirectionType,
+    LocalizationService,
+} from '../../../providers/localization/localization.service';
 import { Dialog, ModalOptions } from '../../../providers/modal/modal.types';
 
 import { DialogButtonsDirective } from './dialog-buttons.directive';
@@ -23,13 +18,24 @@ import { DialogButtonsDirective } from './dialog-buttons.directive';
     templateUrl: './modal-dialog.component.html',
     styleUrls: ['./modal-dialog.component.scss'],
 })
-export class ModalDialogComponent<T extends Dialog<any>> {
+export class ModalDialogComponent<T extends Dialog<any>> implements OnInit {
+    direction$: LocalizationDirectionType;
+
     childComponentType: Type<T>;
     closeModal: (result?: any) => void;
     titleTemplateRef$ = new Subject<TemplateRef<any>>();
     buttonsTemplateRef$ = new Subject<TemplateRef<any>>();
     options?: ModalOptions<T>;
 
+    /**
+     *
+     */
+    constructor(private localizationService: LocalizationService) {}
+
+    ngOnInit(): void {
+        this.direction$ = this.localizationService.direction$;
+    }
+
     /**
      * This callback is invoked when the childComponentType is instantiated in the
      * template by the {@link DialogComponentOutletComponent}.