// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './base_page.js';
import './calibration_component_chip.js';
import './icons.html.js';
import './shimless_rma_shared.css.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {CalibrationComponentChipElement} from './calibration_component_chip.js';
import {ComponentTypeToId} from './data.js';
import {CLICK_CALIBRATION_COMPONENT_BUTTON, ClickCalibrationComponentEvent} from './events.js';
import {getShimlessRmaService} from './mojo_interface_provider.js';
import {getTemplate} from './reimaging_calibration_failed_page.html.js';
import {CalibrationComponentStatus, CalibrationStatus, ComponentType, ShimlessRmaServiceInterface, StateResult} from './shimless_rma.mojom-webui.js';
import {disableNextButton, enableNextButton, executeThenTransitionState, focusPageTitle} from './shimless_rma_util.js';
/**
* @fileoverview
* 'reimaging-calibration-failed-page' is to inform the user which components
* will be calibrated and allow them to skip components if necessary.
* (Skipping components could allow the device to be in a usable, but not fully
* functioning state.)
*/
interface ComponentCheckbox {
component: ComponentType;
uniqueId: number;
id: string;
name: string;
checked: boolean;
failed: boolean;
disabled?: boolean;
isFirstClickableComponent?: boolean;
}
declare global {
interface WindowEventMap {
[CLICK_CALIBRATION_COMPONENT_BUTTON]: ClickCalibrationComponentEvent;
}
}
const NUM_COLUMNS = 1;
const ReimagingCalibrationFailedPageBase = I18nMixin(PolymerElement);
export class ReimagingCalibrationFailedPage extends
ReimagingCalibrationFailedPageBase {
static get is() {
return 'reimaging-calibration-failed-page' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* Set by shimless_rma.ts.
*/
allButtonsDisabled: Boolean,
componentCheckboxes: {
type: Array,
value: () => [],
},
/**
* The index into componentCheckboxes for keyboard navigation between
* components.
*/
focusedComponentIndex: {
type: Number,
value: -1,
},
};
}
allButtonsDisabled: boolean;
shimlessRmaService: ShimlessRmaServiceInterface = getShimlessRmaService();
private componentCheckboxes: ComponentCheckbox[];
private focusedComponentIndex: number;
componentClicked: (event: ClickCalibrationComponentEvent) => void;
handleKeyDownEvent: (event: KeyboardEvent) => void;
onExitButtonClick: () => Promise<{stateResult: StateResult}>;
static get observers() {
return [
'updateIsFirstClickableComponent(componentCheckboxes.*)',
'updateNextButtonAvailability(componentCheckboxes.*)',
];
}
constructor() {
super();
/**
* The componentClickedCallback callback is used to capture events when
* components are clicked, so that the page can put the focus on the
* component that was clicked.
*/
this.componentClicked = (event) => {
const componentIndex = this.componentCheckboxes.findIndex(
component => component.uniqueId === event.detail);
if (componentIndex === -1 ||
this.componentCheckboxes[componentIndex].disabled) {
return;
}
this.focusedComponentIndex = componentIndex;
this.focusOnCurrentComponent();
};
/**
* Handles keyboard navigation over the list of components.
* TODO(240717594): Find a way to avoid duplication of this code in the
* repair components page.
*/
this.handleKeyDownEvent = (event: KeyboardEvent) => {
if (event.key !== 'ArrowRight' && event.key !== 'ArrowDown' &&
event.key !== 'ArrowLeft' && event.key !== 'ArrowUp') {
return;
}
// If there are no selectable components, do nothing.
if (this.focusedComponentIndex === -1) {
return;
}
// Don't use keyboard navigation if the user tabbed out of the
// component list.
if (!this.shadowRoot!.activeElement ||
this.shadowRoot!.activeElement.tagName !==
'CALIBRATION-COMPONENT-CHIP') {
return;
}
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
// The Down button should send you down the column, so we go forward
// by two components, which is the size of the row.
let step = 1;
if (event.key === 'ArrowDown') {
step = NUM_COLUMNS;
}
let newIndex = this.focusedComponentIndex + step;
// Keep skipping disabled components until we encounter one that is
// not disabled.
while (newIndex < this.componentCheckboxes.length &&
this.componentCheckboxes[newIndex].disabled) {
newIndex += step;
}
// Check that we haven't ended up outside of the array before
// applying the changes.
if (newIndex < this.componentCheckboxes.length) {
this.focusedComponentIndex = newIndex;
}
}
// The left and up arrows work similarly to down and right, but go
// backwards.
if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
let step = 1;
if (event.key === 'ArrowUp') {
step = NUM_COLUMNS;
}
let newIndex = this.focusedComponentIndex - step;
while (newIndex >= 0 && this.componentCheckboxes[newIndex].disabled) {
newIndex -= step;
}
if (newIndex >= 0) {
this.focusedComponentIndex = newIndex;
}
}
this.focusOnCurrentComponent();
};
/**
* The "Skip calibration" button on this page is styled and positioned like
* a exit button. So we use the common exit button from shimless_rma.ts
* This function needs to be public, because it's invoked by
* shimless_rma.ts as part of the response to the exit button click.
*/
this.onExitButtonClick = () => {
if (this.tryingToSkipWithFailedComponents()) {
const dialog: CrDialogElement|null =
this.shadowRoot!.querySelector('#failedComponentsDialog');
assert(dialog);
dialog.showModal();
return Promise.reject(
new Error('Attempting to skip with failed components.'));
}
return this.skipCalibration();
};
}
override ready() {
super.ready();
this.getInitialComponentsList();
// Hide the gradient when the list is scrolled to the end.
this.shadowRoot!.querySelector('.scroll-container')!.addEventListener(
'scroll', (event: Event) => {
const gradient =
this.shadowRoot!.querySelector<HTMLElement>('.gradient');
assert(gradient);
const dialog = this.shadowRoot!.querySelector<CrDialogElement>(
'#failedComponentsDialog');
assert(dialog);
dialog.close();
const target = (event.target as HTMLElement);
assert(target);
if (target.scrollHeight - target.scrollTop === target.clientHeight) {
gradient.style.setProperty('visibility', 'hidden');
} else {
gradient.style.setProperty('visibility', 'visible');
}
});
focusPageTitle(this);
}
private getInitialComponentsList(): void {
this.shimlessRmaService.getCalibrationComponentList().then((result) => {
if (!result || !result.hasOwnProperty('components')) {
// TODO(gavindodd): Set an error state?
console.error('Could not get components!');
return;
}
this.componentCheckboxes = result.components.map((item, index) => {
return {
component: item.component,
uniqueId: index,
id: ComponentTypeToId[item.component],
name: this.i18n(ComponentTypeToId[item.component]),
checked: false,
failed: item.status === CalibrationStatus.kCalibrationFailed,
// Disable components that did not fail calibration so they can't be
// selected for calibration again.
disabled: item.status !== CalibrationStatus.kCalibrationFailed,
};
});
// Focus on the first clickable component at the beginning.
this.focusedComponentIndex =
this.componentCheckboxes.findIndex(component => !component.disabled);
});
}
override connectedCallback() {
super.connectedCallback();
window.addEventListener('keydown', this.handleKeyDownEvent);
window.addEventListener(
CLICK_CALIBRATION_COMPONENT_BUTTON, this.componentClicked);
}
override disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this.handleKeyDownEvent);
window.removeEventListener(
CLICK_CALIBRATION_COMPONENT_BUTTON, this.componentClicked);
}
private focusOnCurrentComponent() {
if (this.focusedComponentIndex !== -1) {
const componentChip: CalibrationComponentChipElement|null =
this.shadowRoot!.querySelector(`[unique-id="${
this.componentCheckboxes[this.focusedComponentIndex]
.uniqueId}"]`);
assert(componentChip);
const button = componentChip.shadowRoot!.querySelector<HTMLElement>(
'#componentButton');
assert(button);
button.focus();
}
}
private getComponentsList(): CalibrationComponentStatus[] {
return this.componentCheckboxes.map(item => {
// These statuses tell rmad how to treat each component in this request.
// If the component didn't fail a calibration, its status needs to be
// `kCalibrationComplete`. If the user checked a component, it wants to
// retry its calibration. If the user didn't select a failed component for
// retry, then skip it.
let status;
if (!item.failed) {
status = CalibrationStatus.kCalibrationComplete;
} else if (item.checked) {
status = CalibrationStatus.kCalibrationWaiting;
} else {
status = CalibrationStatus.kCalibrationSkip;
}
return {
component: item.component,
status: status,
progress: 0.0,
};
});
}
private skipCalibration(): Promise<{stateResult: StateResult}> {
const skippedComponents = this.componentCheckboxes.map(item => {
return {
component: item.component,
// This status tells rmad how to treat each component in this request.
// Because the user requested to skip all calibrations, make sure to
// only mark the failed components as `kCalibrationSkip`.
status: item.failed ? CalibrationStatus.kCalibrationSkip :
CalibrationStatus.kCalibrationComplete,
progress: 0.0,
};
});
return this.shimlessRmaService.startCalibration(skippedComponents);
}
onNextButtonClick(): Promise<{stateResult: StateResult}> {
return this.shimlessRmaService.startCalibration(this.getComponentsList());
}
private isComponentDisabled(componentDisabled: boolean): boolean {
return componentDisabled || this.allButtonsDisabled;
}
protected onSkipDialogButtonClicked(): void {
this.closeDialog();
executeThenTransitionState(this, () => this.skipCalibration());
}
protected closeDialog(): void {
const dialog: CrDialogElement|null =
this.shadowRoot!.querySelector('#failedComponentsDialog');
assert(dialog);
dialog.close();
}
private tryingToSkipWithFailedComponents(): boolean {
return this.componentCheckboxes.some(
component => component.failed && !component.checked);
}
private updateIsFirstClickableComponent(): void {
const firstClickableComponent =
this.componentCheckboxes.find(component => !component.disabled);
this.componentCheckboxes.forEach(component => {
component.isFirstClickableComponent =
(component === firstClickableComponent) ? true : false;
});
}
private updateNextButtonAvailability(): void {
if (this.componentCheckboxes.some(component => component.checked)) {
enableNextButton(this);
} else {
disableNextButton(this);
}
}
getComponentsListForTesting(): CalibrationComponentStatus[] {
return this.getComponentsList();
}
}
declare global {
interface HTMLElementTagNameMap {
[ReimagingCalibrationFailedPage.is]: ReimagingCalibrationFailedPage;
}
}
customElements.define(
ReimagingCalibrationFailedPage.is, ReimagingCalibrationFailedPage);