diff --git a/e2e/src/domains.spec.ts b/e2e/src/domains.spec.ts index aaced9356..e77cecbd9 100644 --- a/e2e/src/domains.spec.ts +++ b/e2e/src/domains.spec.ts @@ -193,7 +193,7 @@ test.describe('Domains', () => { // The list is empty while it loads, so wait for the load to end await expect( domains.tabContent.locator('mat-progress-bar'), - ).toHaveClass(/opacity-0/); + ).toBeHidden(); await expect(domains.tabContent).toContainText( 'No users associated with this domain', ); diff --git a/public/assets/locale/en-AU.json b/public/assets/locale/en-AU.json index fb192147f..57e264a00 100644 --- a/public/assets/locale/en-AU.json +++ b/public/assets/locale/en-AU.json @@ -49,6 +49,13 @@ "CANCEL": "Cancel", "CLOSE": "Close", "MENU": "Menu", + "MAIN_MENU": "Main menu", + "SKIP_TO_CONTENT": "Skip to main content", + "LOADING": "Loading", + "MOVE_ROW": "Move row", + "ACTIONS": "Actions", + "SELECT_ALL_ROWS": "Select all rows", + "SELECT_ROW": "Select row", "CLEAR_VALUE": "Clear", "REMOVE": "Remove", "VIEW": "View", @@ -319,6 +326,8 @@ "COPY_WEBHOOK": "Copy webhook", "COPIED_WEBHOOK": "Webhook link copied to clipboard", "TRIGGER_FIELD_NAME": "Name", + "TRIGGER_TRIGGERED": "Triggered", + "TRIGGER_NOT_TRIGGERED": "Not triggered", "TRIGGER_FIELD_COUNT": "Count", "TRIGGER_FIELD_ERRORS": "Errors", "TRIGGER_FIELD_ADDED": "Added", @@ -395,7 +404,12 @@ "STATE": "Module State", "STATE_UPDATE": "Update state", "STATE_LOADING": "Loading module's state...", - "ERROR": "Runtime error in the module" + "ERROR": "Runtime error in the module", + "STATUS_LABEL": "Connection status: {{ status }}", + "STATUS_STOPPED": "Stopped", + "STATUS_CONNECTING": "Connecting", + "STATUS_CONNECTED": "Connected", + "STATUS_DISCONNECTED": "Disconnected" }, "ZONES": { "SINGULAR": "Zone", diff --git a/src/app/admin/admin.component.ts b/src/app/admin/admin.component.ts index c67a3ec25..6e70ce58a 100644 --- a/src/app/admin/admin.component.ts +++ b/src/app/admin/admin.component.ts @@ -23,8 +23,9 @@ import { TranslatePipe } from '../ui/translate.pipe';
- -
+ +
-
+
@@ -113,7 +114,8 @@ const task_details = {}; }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, content: actions_template, size: '3.5rem', sortable: false, @@ -178,6 +180,7 @@ const task_details = {}; `, ], imports: [ + LoadingLabelDirective, SimpleTableComponent, IconComponent, MatRippleModule, diff --git a/src/app/admin/database-details.component.ts b/src/app/admin/database-details.component.ts index cff0d2193..85b43be87 100644 --- a/src/app/admin/database-details.component.ts +++ b/src/app/admin/database-details.component.ts @@ -21,6 +21,7 @@ import { notifyWarn, } from '../common/notifications'; import { openConfirmModal } from '../overlays/confirm-modal.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { ZoneTreeExportModalComponent } from './zone-tree-export-modal.component'; /** How often a running MQTT re-sync is checked on */ @@ -223,7 +224,12 @@ async function queryAllZones(query_params: Record = {}) { } `, ], - imports: [MatProgressSpinnerModule, MatRippleModule, MatDialogModule], + imports: [ + LoadingLabelDirective, + MatProgressSpinnerModule, + MatRippleModule, + MatDialogModule, + ], }) export class PlaceDatabaseDetailsComponent extends AsyncHandler diff --git a/src/app/admin/edge.component.ts b/src/app/admin/edge.component.ts index 3a485fddb..48233fc4e 100644 --- a/src/app/admin/edge.component.ts +++ b/src/app/admin/edge.component.ts @@ -11,6 +11,7 @@ import { escapeHtml } from '../common/general'; import { notifyError, notifySuccess } from '../common/notifications'; import { openConfirmModal } from '../overlays/confirm-modal.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -60,7 +61,7 @@ import { EdgeModalComponent } from './edge-modal.component';
+ + + {{ + (data ? 'COMMON.ONLINE' : 'COMMON.OFFLINE') | translate + }} +
@@ -170,6 +179,7 @@ import { EdgeModalComponent } from './edge-modal.component'; `, ], imports: [ + LoadingLabelDirective, IconComponent, TranslatePipe, MatRippleModule, diff --git a/src/app/admin/extensions.component.ts b/src/app/admin/extensions.component.ts index a5963f390..f7a066863 100644 --- a/src/app/admin/extensions.component.ts +++ b/src/app/admin/extensions.component.ts @@ -22,6 +22,7 @@ import { ConfirmModalData, } from '../overlays/confirm-modal.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; import { AdminDataService } from './admin-data.service'; @@ -83,7 +84,7 @@ const isSameExtension = (a: BackofficeExtension, b?: BackofficeExtension) => }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, size: '6rem', content: actions_template, sortable: false, @@ -172,6 +174,7 @@ const isSameExtension = (a: BackofficeExtension, b?: BackofficeExtension) => `, ], imports: [ + LoadingLabelDirective, IconComponent, MatRippleModule, MatTooltipModule, diff --git a/src/app/admin/interfaces.component.ts b/src/app/admin/interfaces.component.ts index c6aaf071b..b41a9178d 100644 --- a/src/app/admin/interfaces.component.ts +++ b/src/app/admin/interfaces.component.ts @@ -4,6 +4,7 @@ import { listInterfaceRepositories } from '@placeos/ts-client'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { describeError } from '../common/errors'; import { notifyError } from '../common/notifications'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -17,7 +18,7 @@ import { TranslatePipe } from '../ui/translate.pipe'; @if (provider_error()) {
-

+

{{ 'ADMIN.SIGNAGE_PLUGINS_IMPORT' | translate }}

- +
+
-
- {{ - 'ADMIN.UPLOADS_LIB_FIELD_TYPE' - | translate - }} -
- {{ - sorted_by()[1] - ? 'arrow_downward' - : 'arrow_upward' - }} - - +
+
-
- {{ - 'ADMIN.UPLOADS_LIB_FIELD_SIZE' - | translate - }} -
- {{ - sorted_by()[1] - ? 'arrow_downward' - : 'arrow_upward' - }} - - +
+
-
- {{ 'COMMON.CREATED_AT' | translate }} -
- {{ - sorted_by()[1] - ? 'arrow_downward' - : 'arrow_upward' - }} - -
+ +
+
{{ upload.mime_type }}
{{ sizeOf(upload.file_size) }}
-
+
{{ +upload.created_at * 1000 | dateFrom }}
@@ -47,6 +48,7 @@ import { UploadInfo } from './upload-library.component'; `, styles: [``], imports: [ + DialogTitleDirective, MatDialogModule, AuthenticatedImageDirective, IconComponent, diff --git a/src/app/admin/zone-tree-export-modal.component.ts b/src/app/admin/zone-tree-export-modal.component.ts index 4bee61d05..79aac3e2a 100644 --- a/src/app/admin/zone-tree-export-modal.component.ts +++ b/src/app/admin/zone-tree-export-modal.component.ts @@ -9,13 +9,14 @@ import { import { PlaceZone, queryZones } from '@placeos/ts-client'; import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; +import { DialogTitleDirective } from '../ui/dialog-title.directive'; import { IconComponent } from '../ui/icon.component'; @Component({ selector: 'app-zone-tree-export-modal', template: `
-

Export Zone Tree

+

Export Zone Tree

@@ -87,6 +88,7 @@ import { IconComponent } from '../ui/icon.component'; `, imports: [ + DialogTitleDirective, FormsModule, IconComponent, ItemSearchFieldComponent, diff --git a/src/app/app.ts b/src/app/app.ts index 9e9d3ff05..415e63fa7 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -79,9 +79,11 @@ function browserOnline() {
+
@@ -90,6 +92,7 @@ function browserOnline() { @if (!online() && !loading()) { @@ -78,11 +86,12 @@ import { DomainStateService } from './domain-state.service'; > add -
+
`, styles: [``], imports: [ + MatTabNavPanel, IconComponent, MatTooltipModule, MatRipple, diff --git a/src/app/drivers/driver-about.component.ts b/src/app/drivers/driver-about.component.ts index 95f74a0c1..940b284f7 100644 --- a/src/app/drivers/driver-about.component.ts +++ b/src/app/drivers/driver-about.component.ts @@ -9,6 +9,7 @@ import { i18n } from '../common/locale.service'; import { notifyInfo } from '../common/notifications'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -213,6 +214,7 @@ import { DriverStateService } from './driver-state.service'; `, ], imports: [ + LoadingLabelDirective, IconComponent, TranslatePipe, MatRippleModule, diff --git a/src/app/drivers/driver-devices.component.ts b/src/app/drivers/driver-devices.component.ts index f086a7e7f..627949c02 100644 --- a/src/app/drivers/driver-devices.component.ts +++ b/src/app/drivers/driver-devices.component.ts @@ -14,6 +14,7 @@ import { toSignal } from '../common/signals'; import { BindingDirective } from '../ui/binding.directive'; import { CustomTooltipComponent } from '../ui/custom-tooltip.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; import { DriverStateService } from './driver-state.service'; @@ -41,7 +42,7 @@ import { DriverStateService } from './driver-state.service'; -

+

{{ 'DRIVERS.UPDATE' | translate }}
@if (!loading()) { @@ -156,6 +161,8 @@ import { TranslatePipe } from '../ui/translate.pipe'; `, ], imports: [ + LoadingLabelDirective, + DialogTitleDirective, MatDialogModule, TranslatePipe, IconComponent, diff --git a/src/app/drivers/drivers.component.ts b/src/app/drivers/drivers.component.ts index 883ce94a7..4e362a831 100644 --- a/src/app/drivers/drivers.component.ts +++ b/src/app/drivers/drivers.component.ts @@ -1,5 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; +import { MatTabNavPanel } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { extensionsForItem } from '../common/api'; @@ -31,7 +32,11 @@ import { DriverStateService } from './driver-state.service'; [route]="name" [title]="'DRIVERS.PLURAL' | translate" /> -
+
- + + +
}

@@ -91,7 +102,7 @@ import { DriverStateService } from './driver-state.service'; > add -
+ @if (debug_position() === 'below') { @@ -104,6 +115,7 @@ import { DriverStateService } from './driver-state.service'; `, styles: [``], imports: [ + MatTabNavPanel, DebugOutputComponent, IconComponent, TranslatePipe, diff --git a/src/app/groups/group-bulk-add-modal.component.ts b/src/app/groups/group-bulk-add-modal.component.ts index da79639c0..2ad5f86af 100644 --- a/src/app/groups/group-bulk-add-modal.component.ts +++ b/src/app/groups/group-bulk-add-modal.component.ts @@ -10,7 +10,9 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTabsModule } from '@angular/material/tabs'; +import { DialogTitleDirective } from '../ui/dialog-title.directive'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { SettingsToggleComponent } from '../ui/settings-toggle.component'; import { TranslatePipe } from '../ui/translate.pipe'; import { @@ -49,7 +51,10 @@ export interface GroupBulkAddResult {
-

+

{{ title | translate }}

- + `, styles: [``], imports: [ + MatTabNavPanel, IconComponent, MatTooltipModule, MatRipple, diff --git a/src/app/modules/module-about.component.ts b/src/app/modules/module-about.component.ts index 3a725eabd..63ba89432 100644 --- a/src/app/modules/module-about.component.ts +++ b/src/app/modules/module-about.component.ts @@ -11,6 +11,7 @@ import { RouterModule } from '@angular/router'; import { CustomTooltipComponent } from '../ui/custom-tooltip.component'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { ModuleRuntimeErrorsModalComponent } from '../ui/module-runtime-errors-modal.component'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -252,6 +253,7 @@ import { ModuleStateService } from './module-state.service'; `, ], imports: [ + LoadingLabelDirective, TranslatePipe, MatProgressSpinnerModule, SettingsFormComponent, diff --git a/src/app/modules/module-systems.component.ts b/src/app/modules/module-systems.component.ts index 6813b66ee..4f29e281b 100644 --- a/src/app/modules/module-systems.component.ts +++ b/src/app/modules/module-systems.component.ts @@ -6,6 +6,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { RouterModule } from '@angular/router'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -34,7 +35,7 @@ import { ModuleStateService } from './module-state.service'; -
+
- + + +
} @@ -83,7 +94,7 @@ import { ModuleStateService } from './module-state.service'; > add - + @if (debug_position() === 'below') { @@ -96,6 +107,7 @@ import { ModuleStateService } from './module-state.service'; `, styles: [``], imports: [ + MatTabNavPanel, DebugOutputComponent, IconComponent, MatRippleModule, diff --git a/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts b/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts index e82330c09..b0a7a7b72 100644 --- a/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts +++ b/src/app/overlays/bulk-item-modal/bulk-item-modal.component.ts @@ -17,6 +17,7 @@ import { import { unique } from '../../common/general'; import { notifyError, notifyWarn } from '../../common/notifications'; import { HashMap, Identity } from '../../common/types'; +import { DialogTitleDirective } from '../../ui/dialog-title.directive'; import { IconComponent } from '../../ui/icon.component'; import { TranslatePipe } from '../../ui/translate.pipe'; import { CsvUploadComponent } from './csv-upload.component'; @@ -57,7 +58,7 @@ export interface BulkItemModalData> { class="bg-base-200 m-4 mb-2 flex items-center justify-between rounded-sm px-4 py-3" >
-

+

{{ 'COMMON.BULK_ADD' | translate: { type: type } }}

@@ -155,6 +156,7 @@ export interface BulkItemModalData> { `, ], imports: [ + DialogTitleDirective, MatDialogModule, TranslatePipe, IconComponent, diff --git a/src/app/overlays/bulk-item-modal/csv-upload.component.ts b/src/app/overlays/bulk-item-modal/csv-upload.component.ts index 1e786e4b6..c78c50623 100644 --- a/src/app/overlays/bulk-item-modal/csv-upload.component.ts +++ b/src/app/overlays/bulk-item-modal/csv-upload.component.ts @@ -6,6 +6,7 @@ import { csvToJson, downloadFile, jsonToCsv } from '../../common/general'; import { notifyError, notifyWarn } from '../../common/notifications'; import { HashMap } from '../../common/types'; import { IconComponent } from '../../ui/icon.component'; +import { LoadingLabelDirective } from '../../ui/loading-label.directive'; import { TranslatePipe } from '../../ui/translate.pipe'; @Component({ @@ -73,6 +74,7 @@ import { TranslatePipe } from '../../ui/translate.pipe'; `, styles: [``], imports: [ + LoadingLabelDirective, MatRippleModule, TranslatePipe, MatProgressSpinnerModule, diff --git a/src/app/overlays/bulk-item-modal/list.component.ts b/src/app/overlays/bulk-item-modal/list.component.ts index cabd416cf..b0805df84 100644 --- a/src/app/overlays/bulk-item-modal/list.component.ts +++ b/src/app/overlays/bulk-item-modal/list.component.ts @@ -10,6 +10,7 @@ import { import { FormsModule } from '@angular/forms'; import { MatRippleModule } from '@angular/material/core'; +import { i18n } from '../../common/locale.service'; import { HashMap, Identity } from '../../common/types'; import { IconComponent } from '../../ui/icon.component'; import { @@ -234,7 +235,8 @@ export class ListComponent { } columns.push({ key: '_actions', - name: ' ', + name: i18n('COMMON.ACTIONS'), + hide_name: true, content: this._actions_tmpl(), sortable: false, size: '4rem', diff --git a/src/app/overlays/bulk-item-modal/status-list.component.ts b/src/app/overlays/bulk-item-modal/status-list.component.ts index 689dd6eef..526f02991 100644 --- a/src/app/overlays/bulk-item-modal/status-list.component.ts +++ b/src/app/overlays/bulk-item-modal/status-list.component.ts @@ -14,6 +14,7 @@ import { PlaceResource } from '@placeos/ts-client'; import { readError } from '../../common/errors'; import { notifyError } from '../../common/notifications'; import { IconComponent } from '../../ui/icon.component'; +import { LoadingLabelDirective } from '../../ui/loading-label.directive'; import { TranslatePipe } from '../../ui/translate.pipe'; import { VirtualScrollComponent } from '../../ui/virtual-scroll.component'; @@ -116,6 +117,7 @@ const BATCH_SIZE = 5; `, ], imports: [ + LoadingLabelDirective, MatRippleModule, MatProgressSpinnerModule, IconComponent, diff --git a/src/app/overlays/confirm-modal.component.ts b/src/app/overlays/confirm-modal.component.ts index 5116fe456..2b478dd3b 100644 --- a/src/app/overlays/confirm-modal.component.ts +++ b/src/app/overlays/confirm-modal.component.ts @@ -25,7 +25,9 @@ import { i18n } from '../common/locale.service'; import { notifyInfo } from '../common/notifications'; import { waitForEvent } from '../common/signals'; import { ApplicationIcon, DialogEvent } from '../common/types'; +import { DialogTitleDirective } from '../ui/dialog-title.directive'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { TranslatePipe } from '../ui/translate.pipe'; /** Breakdown of what enabling a `ConfirmModalOption` would do */ @@ -179,7 +181,7 @@ export async function openConfirmModal(

-

+

{{ result() ? result().title : (title | translate) }}

@@ -399,6 +401,8 @@ export async function openConfirmModal( `, styles: [``], imports: [ + LoadingLabelDirective, + DialogTitleDirective, MatProgressSpinnerModule, MatCheckboxModule, TranslatePipe, diff --git a/src/app/overlays/duplicate-modal.component.ts b/src/app/overlays/duplicate-modal.component.ts index 172a9788c..52adf6a08 100644 --- a/src/app/overlays/duplicate-modal.component.ts +++ b/src/app/overlays/duplicate-modal.component.ts @@ -20,6 +20,7 @@ import { describeError } from '../common/errors'; import { notifyError } from '../common/notifications'; import { DialogEvent, HashMap } from '../common/types'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; export interface DuplicateModalData { item: HashMap; @@ -163,6 +164,7 @@ export interface DuplicateModalData { `, ], imports: [ + LoadingLabelDirective, MatDialogModule, IconComponent, MatFormFieldModule, diff --git a/src/app/overlays/metadata-details-modal.component.ts b/src/app/overlays/metadata-details-modal.component.ts index 52eeb2ade..c14e3fb3a 100644 --- a/src/app/overlays/metadata-details-modal.component.ts +++ b/src/app/overlays/metadata-details-modal.component.ts @@ -18,6 +18,7 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { addSignalChipItem, removeSignalChipItem } from '../common/forms'; +import { DialogTitleDirective } from '../ui/dialog-title.directive'; import { IconComponent } from '../ui/icon.component'; import type { MetadataFormModel } from '../ui/metadata-display.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -35,7 +36,9 @@ export interface MetadataDetailsModalData {
-

Update metadata details

+

+ Update metadata details +

@@ -77,11 +85,12 @@ import { RepositoriesStateService } from './repositories-state.service'; > add - + `, styles: [``], imports: [ + MatTabNavPanel, IconComponent, MatRippleModule, MatTooltipModule, diff --git a/src/app/repositories/repository-about.component.ts b/src/app/repositories/repository-about.component.ts index fa721f93c..256293419 100644 --- a/src/app/repositories/repository-about.component.ts +++ b/src/app/repositories/repository-about.component.ts @@ -5,6 +5,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { PlaceRepositoryType } from '@placeos/ts-client'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -212,6 +213,7 @@ import { maskUriCredentials } from './repositories.utilities'; `, ], imports: [ + LoadingLabelDirective, MarkdownPipe, TranslatePipe, MatProgressSpinnerModule, diff --git a/src/app/repositories/repository-changelog.component.ts b/src/app/repositories/repository-changelog.component.ts index b37a96b4e..4fb425aff 100644 --- a/src/app/repositories/repository-changelog.component.ts +++ b/src/app/repositories/repository-changelog.component.ts @@ -2,6 +2,7 @@ import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; import { RepositoriesStateService } from './repositories-state.service'; @@ -43,6 +44,7 @@ import { RepositoriesStateService } from './repositories-state.service'; `, ], imports: [ + LoadingLabelDirective, AsyncPipe, IconComponent, MarkdownPipe, diff --git a/src/app/repositories/repository-drivers.component.ts b/src/app/repositories/repository-drivers.component.ts index 0ba53d5eb..92c3f7280 100644 --- a/src/app/repositories/repository-drivers.component.ts +++ b/src/app/repositories/repository-drivers.component.ts @@ -4,6 +4,7 @@ import { MatRippleModule } from '@angular/material/core'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DriverFormatPipe } from '../ui/pipes/driver-format.pipe'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -18,7 +19,7 @@ import { RepositoriesStateService } from './repositories-state.service'; @if (driver_list_error()) {
{ `, ], imports: [ + LoadingLabelDirective, MatFormFieldModule, MatInputModule, MatProgressSpinnerModule, diff --git a/src/app/systems/system-about.component.ts b/src/app/systems/system-about.component.ts index 332ce69ef..7a8bf947e 100644 --- a/src/app/systems/system-about.component.ts +++ b/src/app/systems/system-about.component.ts @@ -5,6 +5,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -231,6 +232,7 @@ import { SystemStateService } from './system-state.service'; `, ], imports: [ + LoadingLabelDirective, TranslatePipe, DateFromPipe, MarkdownPipe, diff --git a/src/app/systems/system-modules.component.ts b/src/app/systems/system-modules.component.ts index 9595125f7..c093a14a1 100644 --- a/src/app/systems/system-modules.component.ts +++ b/src/app/systems/system-modules.component.ts @@ -37,6 +37,7 @@ import { ContextMenuComponent } from '../ui/context-menu.component'; import { ItemSearchFieldComponent } from '../ui/custom-fields/item-search-field.component'; import { ExecuteMethodFieldComponent } from '../ui/custom-fields/system-exec/execute-method-field.component'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { ModuleRuntimeErrorsModalComponent } from '../ui/module-runtime-errors-modal.component'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -99,7 +100,7 @@ import { SystemStateService } from './system-state.service'; @if (active_item()) { @@ -181,7 +182,8 @@ import { SystemStateService } from './system-state.service'; }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, size: '6.5rem', content: actions_template, }, @@ -197,6 +199,11 @@ import { SystemStateService } from './system-state.service';
+ + @let status_label = + 'MODULES.STATUS_LABEL' + | translate + : { status: moduleStatus(row) | translate }; @if (row.running && row.connected === undefined) { E @@ -288,6 +302,9 @@ import { SystemStateService } from './system-state.service'; + + + {{ + (trigger_state[row.id]?.triggered + ? 'SYSTEMS.TRIGGER_TRIGGERED' + : 'SYSTEMS.TRIGGER_NOT_TRIGGERED' + ) | translate + }} +
1, @@ -170,6 +172,7 @@ import { SystemStateService } from './system-state.service'; `, ], imports: [ + LoadingLabelDirective, IconComponent, TranslatePipe, MatRippleModule, diff --git a/src/app/systems/systems.component.ts b/src/app/systems/systems.component.ts index e988df778..d1a002557 100644 --- a/src/app/systems/systems.component.ts +++ b/src/app/systems/systems.component.ts @@ -1,5 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { MatRippleModule } from '@angular/material/core'; +import { MatTabNavPanel } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { PlaceSystem } from '@placeos/ts-client'; @@ -32,7 +33,11 @@ import { SystemStateService } from './system-state.service'; class="hidden lg:block" [title]="'SYSTEMS.PLURAL' | translate" /> -
+
- + + +
}
@@ -91,7 +102,7 @@ import { SystemStateService } from './system-state.service'; > playlist_add - + @if (debug_position() === 'below') { @@ -104,6 +115,7 @@ import { SystemStateService } from './system-state.service'; `, styles: [``], imports: [ + MatTabNavPanel, DebugOutputComponent, IconComponent, MatRippleModule, diff --git a/src/app/triggers/trigger-about.component.ts b/src/app/triggers/trigger-about.component.ts index 933f8b76a..b54fec09e 100644 --- a/src/app/triggers/trigger-about.component.ts +++ b/src/app/triggers/trigger-about.component.ts @@ -128,7 +128,8 @@ import { TriggerStateService } from './trigger-state.service'; }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, size: '6rem', sortable: false, content: actions_template, @@ -148,7 +149,8 @@ import { TriggerStateService } from './trigger-state.service'; }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, size: '6rem', sortable: false, content: actions_template, @@ -227,7 +229,8 @@ import { TriggerStateService } from './trigger-state.service'; }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, size: '6rem', sortable: false, content: fn_actions_template, @@ -248,7 +251,8 @@ import { TriggerStateService } from './trigger-state.service'; }, { key: 'actions', - name: ' ', + name: 'COMMON.ACTIONS' | translate, + hide_name: true, size: '6rem', sortable: false, content: fn_actions_template, diff --git a/src/app/triggers/trigger-instances.component.ts b/src/app/triggers/trigger-instances.component.ts index ef4dab761..c3d39b044 100644 --- a/src/app/triggers/trigger-instances.component.ts +++ b/src/app/triggers/trigger-instances.component.ts @@ -10,6 +10,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { AsyncHandler } from '../common/async-handler.class'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -21,7 +22,7 @@ import { TriggerStateService } from './trigger-state.service'; -
+
- + + +
} @@ -83,7 +94,7 @@ import { TriggerStateService } from './trigger-state.service'; > add - + @if (debug_position() === 'below') { @@ -96,6 +107,7 @@ import { TriggerStateService } from './trigger-state.service'; `, styles: [``], imports: [ + MatTabNavPanel, DebugOutputComponent, IconComponent, TranslatePipe, diff --git a/src/app/ui/context-menu.component.ts b/src/app/ui/context-menu.component.ts index 2e8f076c8..7373ef328 100644 --- a/src/app/ui/context-menu.component.ts +++ b/src/app/ui/context-menu.component.ts @@ -16,7 +16,9 @@ import { selector: '[context-menu]', template: ` + `, imports: [ + LoadingLabelDirective, + DialogTitleDirective, MatProgressSpinnerModule, TranslatePipe, IconComponent, diff --git a/src/app/ui/global-loading.component.ts b/src/app/ui/global-loading.component.ts index 907ab6e0a..31c9ccbce 100644 --- a/src/app/ui/global-loading.component.ts +++ b/src/app/ui/global-loading.component.ts @@ -18,7 +18,9 @@ import { waitForSignalValue } from '../common/signals';
-

{{ message() }}

+

+ {{ message() }} +

diff --git a/src/app/ui/icon.component.ts b/src/app/ui/icon.component.ts index 0cb0ee668..9404199f7 100644 --- a/src/app/ui/icon.component.ts +++ b/src/app/ui/icon.component.ts @@ -65,39 +65,51 @@ export class IconComponent { * Icon fonts render names like "more_vert", which screen readers read * aloud. Inside a button or link, hide the icon when the control has its * own text or label. For icon-only controls with a tooltip, use the - * tooltip as the control's label. + * tooltip as the control's label. Outside a control, an icon with its + * own tooltip (e.g. an info icon) becomes an image named by the tooltip. */ private _labelControl() { const host = this._element.nativeElement; const control = host.parentElement?.closest('button, a'); - if (!control) return; + if (!control) { + if (host.classList.contains('mat-mdc-tooltip-trigger')) { + this._labelFromTooltip(host); + } + return; + } const has_text = !!textWithoutIcons(control); if (control.hasAttribute('aria-label') || has_text) { host.setAttribute('aria-hidden', 'true'); return; } // The tooltip must be on the control itself, not an outer element - const tooltip = this._tooltip; - if ( - !tooltip || - !control.classList.contains('mat-mdc-tooltip-trigger') - ) { - return; + if (control.classList.contains('mat-mdc-tooltip-trigger')) { + this._labelFromTooltip(control); } + } + + /** Keep the label of the target the same as the tooltip text */ + private _labelFromTooltip(target: HTMLElement) { + const tooltip = this._tooltip; + if (!tooltip) return; + const host = this._element.nativeElement; // Tooltip text can change, e.g. "Enable" and "Disable" afterEveryRender( () => { const label = `${tooltip.message || ''}`.trim(); - if (control.getAttribute('aria-label') === (label || null)) { + if (target.getAttribute('aria-label') === (label || null)) { return; } // Without tooltip text the icon name is the only label left if (label) { - control.setAttribute('aria-label', label); - host.setAttribute('aria-hidden', 'true'); + target.setAttribute('aria-label', label); + if (target === host) host.setAttribute('role', 'img'); + else host.setAttribute('aria-hidden', 'true'); } else { - control.removeAttribute('aria-label'); - host.removeAttribute('aria-hidden'); + target.removeAttribute('aria-label'); + host.removeAttribute( + target === host ? 'role' : 'aria-hidden', + ); } }, { injector: this._injector }, diff --git a/src/app/ui/item-selection.component.ts b/src/app/ui/item-selection.component.ts index 21d43fafc..44a0deed4 100644 --- a/src/app/ui/item-selection.component.ts +++ b/src/app/ui/item-selection.component.ts @@ -28,6 +28,7 @@ import { HotkeysService } from '../common/hotkeys.service'; import { ActiveItemService } from '../common/item.service'; import { toSignal } from '../common/signals'; import { IconComponent } from './icon.component'; +import { LoadingLabelDirective } from './loading-label.directive'; import { TranslatePipe } from './translate.pipe'; import { VirtualScrollComponent } from './virtual-scroll.component'; @@ -190,6 +191,7 @@ import { VirtualScrollComponent } from './virtual-scroll.component'; `, ], imports: [ + LoadingLabelDirective, TranslatePipe, IconComponent, VirtualScrollComponent, diff --git a/src/app/ui/item-sidebar.component.ts b/src/app/ui/item-sidebar.component.ts index 16e04b088..32e6f0cb4 100644 --- a/src/app/ui/item-sidebar.component.ts +++ b/src/app/ui/item-sidebar.component.ts @@ -33,7 +33,9 @@ import { ActiveItemService } from '../common/item.service'; import { reportLoadError } from '../common/load-error'; import { toSignal } from '../common/signals'; import { IconComponent } from './icon.component'; +import { LoadingLabelDirective } from './loading-label.directive'; import { SettingsToggleComponent } from './settings-toggle.component'; +import { SkipToContentComponent } from './skip-to-content.component'; import { TranslatePipe } from './translate.pipe'; import { VirtualScrollComponent } from './virtual-scroll.component'; @@ -46,8 +48,9 @@ interface GroupTreeItem { selector: 'item-sidebar', template: ` -
@@ -68,6 +71,8 @@ interface GroupTreeItem { 'COMMON.SEARCH_FOR' | translate: { name: title() } " /> + + @if (loading() || group_hierarchy_loading()) { @if (filter_options()?.length) { -
+ +
@@ -94,6 +103,7 @@ interface GroupTreeItem { > @@ -192,6 +202,11 @@ interface GroupTreeItem { ] : ['/', route(), node.group.id] " + [attr.aria-current]=" + selected_id() === node.group.id + ? 'page' + : null + " (click)="show.set(false)" >
brightness_alert + + {{ + (item.zone_issues === 'system' + ? 'SYSTEMS.MISCONFIGURED' + : 'ZONES.MISCONFIGURED' + ) | translate + }} +
} @if (item.has_runtime_error) { @@ -320,6 +344,9 @@ interface GroupTreeItem { [matTooltip]="'MODULES.ERROR' | translate" > error + + {{ 'MODULES.ERROR' | translate }} +
} @@ -340,7 +367,7 @@ interface GroupTreeItem {
}
-
+ `, styles: [ ` @@ -360,6 +387,7 @@ interface GroupTreeItem { `, ], imports: [ + LoadingLabelDirective, CdkTreeModule, TranslatePipe, MatTooltipModule, @@ -373,6 +401,7 @@ interface GroupTreeItem { VirtualScrollComponent, MatRippleModule, SettingsToggleComponent, + SkipToContentComponent, ], }) export class ItemSidebarComponent diff --git a/src/app/ui/item-tablist.component.ts b/src/app/ui/item-tablist.component.ts index 3592a8822..b847cf7c4 100644 --- a/src/app/ui/item-tablist.component.ts +++ b/src/app/ui/item-tablist.component.ts @@ -7,7 +7,7 @@ import { model, signal, } from '@angular/core'; -import { MatTabsModule } from '@angular/material/tabs'; +import { MatTabNavPanel, MatTabsModule } from '@angular/material/tabs'; import { NavigationCancel, NavigationEnd, @@ -40,7 +40,7 @@ export interface ItemTab { @if (show_application_picker()) { - } + } + @for ( + column of active_columns(); + track column.key; + let i = $index + ) { + @let sorted = sort()?.key === column.key; + @let can_sort_column = + can_sort() && column.sortable !== false; +
+ @if (can_sort_column) { + + } @else { + + {{ column.name || column.key }} + + } +
+ } + @if (virtual_enabled()) {
unfold_more @@ -201,21 +253,26 @@ function sortKey(value: unknown): string { />
} @else { - +
+ +
} } } @if (!paginated_data()?.length) { -
- {{ empty_message() }} +
+
+ {{ empty_message() }} +
} @@ -223,6 +280,7 @@ function sortKey(value: unknown): string { @if (selectable()) {
} @for (column of active_columns(); track column; let j = $index) {
{ const data = this.data(); @@ -582,6 +666,18 @@ export class SimpleTableComponent> { } } + /** Move a row up or down with the keyboard, like a drag and drop */ + public moveRow(index: number, offset: 1 | -1, event: Event) { + event.preventDefault(); + const target = index + offset; + if (target < 0 || target >= this.data_length()) return; + this._pending_move = { + index: target, + row: JSON.stringify(this.data_view()[index]), + }; + this.ondrop.emit([index, target]); + } + public selectAll(state: boolean) { const list = this.data_view(); this.selected.set(state ? list.map((_item, i) => i) : []); diff --git a/src/app/ui/skip-to-content.component.ts b/src/app/ui/skip-to-content.component.ts new file mode 100644 index 000000000..aaeb52953 --- /dev/null +++ b/src/app/ui/skip-to-content.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TranslatePipe } from './translate.pipe'; + +/** + * Button that moves keyboard focus past the menus to the page content. + * It is only visible while it has focus. The page shell must have an element + * with the id `main-content` and `tabindex="-1"`. + */ +@Component({ + selector: 'skip-to-content', + template: ` + + `, + imports: [TranslatePipe], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkipToContentComponent { + public skip() { + document.getElementById('main-content')?.focus(); + } +} diff --git a/src/app/ui/upload-list.component.ts b/src/app/ui/upload-list.component.ts index 07bbd5d4e..23d0fba57 100644 --- a/src/app/ui/upload-list.component.ts +++ b/src/app/ui/upload-list.component.ts @@ -11,6 +11,7 @@ import { SettingsService } from '../common/settings.service'; import { UploadDetails } from '../common/uploads'; import { UploadsService } from '../common/uploads.service'; import { IconComponent } from './icon.component'; +import { LoadingLabelDirective } from './loading-label.directive'; import { TranslatePipe } from './translate.pipe'; @Component({ @@ -200,6 +201,7 @@ import { TranslatePipe } from './translate.pipe'; `, ], imports: [ + LoadingLabelDirective, TranslatePipe, IconComponent, MatRippleModule, diff --git a/src/app/ui/upload-permissions-modal.component.ts b/src/app/ui/upload-permissions-modal.component.ts index 961c51098..969015279 100644 --- a/src/app/ui/upload-permissions-modal.component.ts +++ b/src/app/ui/upload-permissions-modal.component.ts @@ -10,6 +10,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { UploadPermissions } from '../common/uploads'; +import { DialogTitleDirective } from './dialog-title.directive'; import { IconComponent } from './icon.component'; import { SettingsToggleComponent } from './settings-toggle.component'; import { TranslatePipe } from './translate.pipe'; @@ -20,7 +21,7 @@ import { TranslatePipe } from './translate.pipe';
-

+

{{ 'COMMON.UPLOAD_FILE' | translate }}

@@ -92,11 +100,12 @@ import { UsersStateService } from './users-state.service'; > playlist_add -
+ `, styles: [``], imports: [ + MatTabNavPanel, IconComponent, MatRippleModule, MatTooltipModule, diff --git a/src/app/zones/zone-about.component.ts b/src/app/zones/zone-about.component.ts index 182a0adf0..8774c1355 100644 --- a/src/app/zones/zone-about.component.ts +++ b/src/app/zones/zone-about.component.ts @@ -21,6 +21,7 @@ import { AsyncHandler } from '../common/async-handler.class'; import { describeError } from '../common/errors'; import { ExecuteMethodFieldComponent } from '../ui/custom-fields/system-exec/execute-method-field.component'; import { SettingsFormComponent } from '../ui/forms/settings-form.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { DateFromPipe } from '../ui/pipes/date-from.pipe'; import { MarkdownPipe } from '../ui/pipes/markdown.pipe'; import { TranslatePipe } from '../ui/translate.pipe'; @@ -259,6 +260,7 @@ import { TranslatePipe } from '../ui/translate.pipe'; `, ], imports: [ + LoadingLabelDirective, MatProgressSpinnerModule, SettingsFormComponent, TranslatePipe, diff --git a/src/app/zones/zone-children.component.ts b/src/app/zones/zone-children.component.ts index c7f3d8684..fa1c53f2e 100644 --- a/src/app/zones/zone-children.component.ts +++ b/src/app/zones/zone-children.component.ts @@ -7,6 +7,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { RouterModule } from '@angular/router'; import { IconComponent } from '../ui/icon.component'; +import { LoadingLabelDirective } from '../ui/loading-label.directive'; import { SimpleTableComponent } from '../ui/simple-table.component'; import { TranslatePipe } from '../ui/translate.pipe'; import { ZonesStateService } from './zones-state.service'; @@ -33,7 +34,7 @@ import { ZonesStateService } from './zones-state.service'; -
+
- + + +
} @@ -96,7 +107,7 @@ import { ZonesStateService } from './zones-state.service'; > playlist_add - + @if (debug_position() === 'below') { @@ -109,6 +120,7 @@ import { ZonesStateService } from './zones-state.service'; `, styles: [``], imports: [ + MatTabNavPanel, DebugOutputComponent, IconComponent, TranslatePipe, diff --git a/src/tests/common/locale.service.spec.ts b/src/tests/common/locale.service.spec.ts index bbd63463a..57a48e011 100644 --- a/src/tests/common/locale.service.spec.ts +++ b/src/tests/common/locale.service.spec.ts @@ -16,6 +16,7 @@ vi.mock('../../app/common/general', () => ({ import { i18n, localeFromUrl, + LocaleService, setTranslationService, } from '../../app/common/locale.service'; @@ -210,3 +211,27 @@ describe('LocaleService behavior patterns', () => { expect(i18n('items', {}, 5)).toBe('5 items'); }); }); + +describe('LocaleService page language', () => { + afterEach(() => vi.unstubAllGlobals()); + + it('marks the page with the language of the loaded translations', async () => { + vi.stubGlobal( + 'fetch', + vi.fn((url: string) => + Promise.resolve({ + ok: url.endsWith('/fr.json'), + json: () => Promise.resolve({}), + }), + ), + ); + const service = new LocaleService(); + + await service.setLocale('fr'); + expect(document.documentElement.lang).toBe('fr'); + + // Only fr.json exists, so the text falls back to the default locale + await service.setLocale('de-DE'); + expect(document.documentElement.lang).toBe('en-AU'); + }); +}); diff --git a/src/tests/common/notifications.spec.ts b/src/tests/common/notifications.spec.ts index d88771612..18b4cf587 100644 --- a/src/tests/common/notifications.spec.ts +++ b/src/tests/common/notifications.spec.ts @@ -103,6 +103,7 @@ describe('notifications.ts', () => { { panelClass: ['success'], duration: 5000, + politeness: 'polite', }, ); }); @@ -189,6 +190,14 @@ describe('notifications.ts', () => { ).toBeUndefined(); }); + it('should be announced at once', () => { + notifyError('Error message'); + + expect(mock_snackbar.open.mock.calls[0][2].politeness).toBe( + 'assertive', + ); + }); + it('should log to console.error', () => { notifyError('Error message'); diff --git a/src/tests/ui/components/simple-table.component.spec.ts b/src/tests/ui/components/simple-table.component.spec.ts index 3a45b402b..8ab0ee5de 100644 --- a/src/tests/ui/components/simple-table.component.spec.ts +++ b/src/tests/ui/components/simple-table.component.spec.ts @@ -9,6 +9,10 @@ import { TableColumn, } from '../../../app/ui/simple-table.component'; +vi.mock('@placeos/ts-client', () => + vi.importActual('@placeos/ts-client/dist/index.es.js'), +); + describe('SimpleTableComponent', () => { let component: SimpleTableComponent>; let fixture: ComponentFixture< @@ -53,7 +57,7 @@ describe('SimpleTableComponent', () => { fixture.detectChanges(); await fixture.whenStable(); const rows = fixture.nativeElement.querySelectorAll( - '[role="row"]', + 'cdk-virtual-scroll-viewport [role="row"]', ) as NodeListOf; expect(rows.length).toBe(12); expect(rows[0].textContent).toContain('User 500'); @@ -114,6 +118,78 @@ describe('SimpleTableComponent', () => { expect(rows).toEqual([0, 1]); }); + it('groups cells into rows under column headers with sort state', async () => { + fixture.componentRef.setInput('data', [ + { id: '1', name: 'Item 1', value: 1 }, + { id: '2', name: 'Item 2', value: 2 }, + ]); + fixture.componentRef.setInput('columns', [ + { key: 'name', name: 'Name' }, + { key: 'value', name: 'Value', sortable: false }, + ]); + fixture.componentRef.setInput('sortable', true); + component.setSort('name'); + fixture.detectChanges(); + await fixture.whenStable(); + const table: HTMLElement = + fixture.nativeElement.querySelector('[role="table"]'); + const rows = table.querySelectorAll(':scope > [role="row"]'); + expect(rows.length).toBe(3); + const headers = rows[0].querySelectorAll('[role="columnheader"]'); + expect(headers[0].getAttribute('aria-sort')).toBe('ascending'); + // Only sortable columns get a button + expect(headers[0].querySelector('button')).not.toBeNull(); + expect(headers[1].querySelector('button')).toBeNull(); + expect(rows[1].querySelectorAll('[role="cell"]').length).toBe(2); + }); + + it('moves rows with the arrow keys and keeps focus on the moved row', async () => { + // Without ids, rows are tracked by index and their elements do not move + const rows = [ + { name: 'Item 1' }, + { name: 'Item 2' }, + { name: 'Item 3' }, + ]; + fixture.componentRef.setInput('data', rows); + fixture.componentRef.setInput('columns', [ + { key: 'name', name: 'Name' }, + ]); + fixture.componentRef.setInput('can_reorder', true); + fixture.detectChanges(); + await fixture.whenStable(); + document.body.appendChild(fixture.nativeElement); + const moves: [number, number][] = []; + component.ondrop.subscribe((move) => moves.push(move)); + const handle = (i: number) => + fixture.nativeElement.querySelector( + `[data-reorder="${i}"]`, + ) as HTMLElement; + const press = (i: number, key: string) => + handle(i).dispatchEvent(new KeyboardEvent('keydown', { key })); + + press(0, 'ArrowUp'); + press(2, 'ArrowDown'); + expect(moves).toEqual([]); + + handle(0).focus(); + press(0, 'ArrowDown'); + expect(moves).toEqual([[0, 1]]); + // The parent applies the move + fixture.componentRef.setInput('data', [rows[1], rows[0], rows[2]]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(document.activeElement).toBe(handle(1)); + + // A cancelled move leaves the data as it was and focus where it is + press(1, 'ArrowDown'); + handle(0).focus(); + fixture.componentRef.setInput('data', [rows[1], rows[0], rows[2]]); + fixture.detectChanges(); + await fixture.whenStable(); + expect(document.activeElement).toBe(handle(0)); + fixture.nativeElement.remove(); + }); + describe('default state', () => { it('should have empty selected array', () => { expect(component.selected()).toEqual([]);