recorder: Add language list component
Extract language list and list item from language picker, which can be used to separate headers and contents when we render language picker as a dialog. Bug: b:377885042 Test: manually Change-Id: I91a20101d6a3e23da6161a3ad9f291f10abdd1fe Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6148113 Reviewed-by: Pi-Hsun Shih <pihsun@chromium.org> Commit-Queue: Jennifer Ling <hsuanling@google.com> Cr-Commit-Position: refs/heads/main@{#1407695}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
c2c3df3e1f
commit
93fd53bca0
ash/webui/recorder_app_ui/resources/components
@ -26,8 +26,10 @@ component_files = [
|
||||
"genai-feedback-buttons.ts",
|
||||
"genai-placeholder.ts",
|
||||
"language-dropdown.ts",
|
||||
"language-selection-dialog.ts",
|
||||
"language-list-item.ts",
|
||||
"language-list.ts",
|
||||
"language-picker.ts",
|
||||
"language-selection-dialog.ts",
|
||||
"mic-selection-button.ts",
|
||||
"onboarding-dialog.ts",
|
||||
"recording-file-list.ts",
|
||||
|
@ -0,0 +1,219 @@
|
||||
// Copyright 2025 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/mwc/@material/web/focus/md-focus-ring.js';
|
||||
import 'chrome://resources/mwc/@material/web/progress/circular-progress.js';
|
||||
import './cra/cra-button.js';
|
||||
import './cra/cra-icon.js';
|
||||
import './settings-row.js';
|
||||
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
PropertyDeclarations,
|
||||
} from 'chrome://resources/mwc/lit/index.js';
|
||||
|
||||
import {i18n} from '../core/i18n.js';
|
||||
import {ModelState} from '../core/on_device_model/types.js';
|
||||
import {ReactiveLitElement} from '../core/reactive/lit.js';
|
||||
import {LangPackInfo} from '../core/soda/language_info.js';
|
||||
import {
|
||||
assertExhaustive,
|
||||
assertNotReached,
|
||||
} from '../core/utils/assert.js';
|
||||
import {stopPropagation, suppressEvent} from '../core/utils/event_handler.js';
|
||||
|
||||
/**
|
||||
* An item in the language selection list.
|
||||
*/
|
||||
export class LanguageListItem extends ReactiveLitElement {
|
||||
static override styles = css`
|
||||
md-focus-ring {
|
||||
--md-focus-ring-outward-offset: -8px;
|
||||
--md-focus-ring-shape: 20px;
|
||||
}
|
||||
|
||||
#root {
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: b/377885042 - Move the circular progress to a separate component.
|
||||
*/
|
||||
settings-row cra-button md-circular-progress {
|
||||
--md-circular-progress-active-indicator-color: var(--cros-sys-disabled);
|
||||
|
||||
/*
|
||||
* This has a lower precedence than the size override in cros-button,
|
||||
* but still need to be set to have correct line width.
|
||||
*/
|
||||
--md-circular-progress-size: 24px;
|
||||
|
||||
/*
|
||||
* This is to override the size setting for slotted element in
|
||||
* cros-button. On figma the circular progress have 2px padding, but
|
||||
* md-circular-progres has a non-configurable 4px padding. Setting a
|
||||
* negative margin so the extra padding doesn't expand the button size.
|
||||
*/
|
||||
height: 24px;
|
||||
margin: -2px;
|
||||
width: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
static override properties: PropertyDeclarations = {
|
||||
langPackInfo: {attribute: false},
|
||||
selected: {type: Boolean},
|
||||
sodaState: {attribute: false},
|
||||
};
|
||||
|
||||
langPackInfo: LangPackInfo|null = null;
|
||||
|
||||
selected = false;
|
||||
|
||||
sodaState: ModelState = {kind: 'unavailable'};
|
||||
|
||||
private onDownload() {
|
||||
if (this.langPackInfo === null) {
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('language-download-click', {
|
||||
detail: this.langPackInfo.languageCode,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private onSelect() {
|
||||
if (this.langPackInfo === null) {
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('language-select-click', {
|
||||
detail: this.langPackInfo.languageCode,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private activateRow() {
|
||||
this.renderRoot.querySelector('settings-row')?.click();
|
||||
}
|
||||
|
||||
private onKeyUp(ev: KeyboardEvent) {
|
||||
if (ev.key === ' ') {
|
||||
suppressEvent(ev);
|
||||
this.activateRow();
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === ' ') {
|
||||
// Prevents page from scroll down.
|
||||
ev.preventDefault();
|
||||
}
|
||||
if (ev.key === 'Enter') {
|
||||
suppressEvent(ev);
|
||||
this.activateRow();
|
||||
}
|
||||
}
|
||||
|
||||
private renderDescriptionAndAction(): RenderResult {
|
||||
const downloadButton = html`
|
||||
<cra-button
|
||||
slot="action"
|
||||
button-style="secondary"
|
||||
tabindex="-1"
|
||||
.label=${i18n.languagePickerLanguageDownloadButton}
|
||||
@keyup=${stopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
@click=${this.onDownload}
|
||||
></cra-button>
|
||||
`;
|
||||
|
||||
const kind = this.sodaState.kind;
|
||||
switch (kind) {
|
||||
case 'notInstalled':
|
||||
return downloadButton;
|
||||
case 'error':
|
||||
// Shows the download button for users to try again.
|
||||
return html`
|
||||
<span slot="description" class="error">
|
||||
${i18n.languagePickerLanguageErrorDescription}
|
||||
</span>
|
||||
${downloadButton}
|
||||
`;
|
||||
case 'installing': {
|
||||
const progressDescription =
|
||||
i18n.languagePickerLanguageDownloadingProgressDescription(
|
||||
this.sodaState.progress,
|
||||
);
|
||||
return html`
|
||||
<span slot="description">${progressDescription}</span>
|
||||
<cra-button
|
||||
slot="action"
|
||||
button-style="secondary"
|
||||
.label=${i18n.languagePickerLanguageDownloadingButton}
|
||||
disabled
|
||||
>
|
||||
<md-circular-progress indeterminate slot="leading-icon">
|
||||
</md-circular-progress>
|
||||
</cra-button>
|
||||
`;
|
||||
}
|
||||
case 'installed':
|
||||
if (this.selected) {
|
||||
// Row is not interactive when the language is selected.
|
||||
return html`
|
||||
<cra-icon slot="action" name="checked" disabled></cra-icon>
|
||||
`;
|
||||
}
|
||||
return html`<span slot="action" @click=${this.onSelect}></span>`;
|
||||
case 'unavailable':
|
||||
return assertNotReached('SODA unavailable but the row is rendered.');
|
||||
default:
|
||||
return assertExhaustive(kind);
|
||||
}
|
||||
}
|
||||
|
||||
private isFocusable() {
|
||||
if (this.sodaState.kind === 'installing' ||
|
||||
(this.sodaState.kind === 'installed' && this.selected)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
override render(): RenderResult {
|
||||
if (this.langPackInfo === null || this.sodaState.kind === 'unavailable') {
|
||||
return nothing;
|
||||
}
|
||||
// TODO: b/384418702 - Add aria label of each state in #root and set
|
||||
// settings-row aria-hidden to `true` to avoid redundant announcement by
|
||||
// screen reader.
|
||||
return html`
|
||||
<div id="root"
|
||||
tabindex=${this.isFocusable() ? 0 : -1}
|
||||
@click=${this.activateRow}
|
||||
@keydown=${this.onKeyDown}
|
||||
@keyup=${this.onKeyUp}
|
||||
>
|
||||
<settings-row>
|
||||
<span slot="label">${this.langPackInfo.displayName}</span>
|
||||
${this.renderDescriptionAndAction()}
|
||||
</settings-row>
|
||||
${this.isFocusable() ? html`<md-focus-ring></md-focus-ring>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('language-list-item', LanguageListItem);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'language-list-item': LanguageListItem;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import './language-list-item.js';
|
||||
|
||||
import {
|
||||
html,
|
||||
map,
|
||||
PropertyDeclarations,
|
||||
} from 'chrome://resources/mwc/lit/index.js';
|
||||
|
||||
import {usePlatformHandler} from '../core/lit/context.js';
|
||||
import {ReactiveLitElement} from '../core/reactive/lit.js';
|
||||
import {LangPackInfo, LanguageCode} from '../core/soda/language_info.js';
|
||||
|
||||
/**
|
||||
* A list of language options.
|
||||
*/
|
||||
export class LanguageList extends ReactiveLitElement {
|
||||
static override properties: PropertyDeclarations = {
|
||||
selectedLanguage: {attribute: false},
|
||||
};
|
||||
|
||||
selectedLanguage: LanguageCode|null = null;
|
||||
|
||||
private readonly platformHandler = usePlatformHandler();
|
||||
|
||||
private renderLanguageRow(langPack: LangPackInfo): RenderResult {
|
||||
const {languageCode} = langPack;
|
||||
const sodaState = this.platformHandler.getSodaState(languageCode).value;
|
||||
|
||||
return html`
|
||||
<language-list-item
|
||||
.langPackInfo=${langPack}
|
||||
.sodaState=${sodaState}
|
||||
?selected=${languageCode === this.selectedLanguage}
|
||||
>
|
||||
</language-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
override render(): RenderResult {
|
||||
const list = this.platformHandler.getLangPackList();
|
||||
return map(
|
||||
list,
|
||||
(langPack) => this.renderLanguageRow(langPack),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('language-list', LanguageList);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'language-list': LanguageList;
|
||||
}
|
||||
}
|
@ -2,20 +2,18 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'chrome://resources/mwc/@material/web/progress/circular-progress.js';
|
||||
import './cra/cra-button.js';
|
||||
import './cra/cra-icon.js';
|
||||
import './cra/cra-icon-button.js';
|
||||
import './settings-row.js';
|
||||
import './language-list.js';
|
||||
|
||||
import {css, html, map, nothing} from 'chrome://resources/mwc/lit/index.js';
|
||||
import {css, html} from 'chrome://resources/mwc/lit/index.js';
|
||||
|
||||
import {i18n} from '../core/i18n.js';
|
||||
import {usePlatformHandler} from '../core/lit/context.js';
|
||||
import {ReactiveLitElement} from '../core/reactive/lit.js';
|
||||
import {LangPackInfo, LanguageCode} from '../core/soda/language_info.js';
|
||||
import {LanguageCode} from '../core/soda/language_info.js';
|
||||
import {setTranscriptionLanguage} from '../core/state/transcription.js';
|
||||
import {assertExhaustive} from '../core/utils/assert.js';
|
||||
|
||||
import {withTooltip} from './directives/with-tooltip.js';
|
||||
|
||||
@ -81,29 +79,6 @@ export class LanguagePicker extends ReactiveLitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: b/377885042 - Move the circular progress to a separate component.
|
||||
*/
|
||||
settings-row cra-button md-circular-progress {
|
||||
--md-circular-progress-active-indicator-color: var(--cros-sys-disabled);
|
||||
|
||||
/*
|
||||
* This has a lower precedence than the size override in cros-button,
|
||||
* but still need to be set to have correct line width.
|
||||
*/
|
||||
--md-circular-progress-size: 24px;
|
||||
|
||||
/*
|
||||
* This is to override the size setting for slotted element in
|
||||
* cros-button. On figma the circular progress have 2px padding, but
|
||||
* md-circular-progres has a non-configurable 4px padding. Setting a
|
||||
* negative margin so the extra padding doesn't expand the button size.
|
||||
*/
|
||||
height: 24px;
|
||||
margin: -2px;
|
||||
width: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly platformHandler = usePlatformHandler();
|
||||
@ -112,130 +87,41 @@ export class LanguagePicker extends ReactiveLitElement {
|
||||
this.dispatchEvent(new Event('close'));
|
||||
}
|
||||
|
||||
private renderLanguageRow(
|
||||
{displayName, languageCode}: LangPackInfo,
|
||||
selectedLanguage: LanguageCode|null,
|
||||
): RenderResult {
|
||||
const sodaState = this.platformHandler.getSodaState(languageCode).value;
|
||||
if (sodaState.kind === 'unavailable') {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = html`<span slot="label">${displayName}</span>`;
|
||||
|
||||
function onSelectAndDownload() {
|
||||
setTranscriptionLanguage(languageCode);
|
||||
}
|
||||
|
||||
const downloadButton = html`
|
||||
<cra-button
|
||||
slot="action"
|
||||
button-style="secondary"
|
||||
.label=${i18n.languagePickerLanguageDownloadButton}
|
||||
@click=${onSelectAndDownload}
|
||||
></cra-button>
|
||||
`;
|
||||
switch (sodaState.kind) {
|
||||
case 'notInstalled': {
|
||||
return html`<settings-row>${name} ${downloadButton}</settings-row>`;
|
||||
}
|
||||
// Shows the download button for users to try again.
|
||||
case 'error': {
|
||||
return html`
|
||||
<settings-row>
|
||||
${name}
|
||||
<span slot="description" class="error">
|
||||
${i18n.languagePickerLanguageErrorDescription}
|
||||
</span>
|
||||
${downloadButton}
|
||||
</settings-row>
|
||||
`;
|
||||
}
|
||||
case 'installing': {
|
||||
const progressDescription =
|
||||
i18n.languagePickerLanguageDownloadingProgressDescription(
|
||||
sodaState.progress,
|
||||
);
|
||||
return html`
|
||||
<settings-row>
|
||||
${name}
|
||||
<span slot="description">${progressDescription}</span>
|
||||
<cra-button
|
||||
slot="action"
|
||||
button-style="secondary"
|
||||
.label=${i18n.languagePickerLanguageDownloadingButton}
|
||||
disabled
|
||||
>
|
||||
<md-circular-progress indeterminate slot="leading-icon">
|
||||
</md-circular-progress>
|
||||
</cra-button>
|
||||
</settings-row>
|
||||
`;
|
||||
}
|
||||
case 'installed': {
|
||||
if (languageCode === selectedLanguage) {
|
||||
return html`
|
||||
<settings-row>
|
||||
${name}
|
||||
<cra-icon slot="action" name="checked"></cra-icon>
|
||||
</settings-row>
|
||||
`;
|
||||
} else {
|
||||
// Set and install the language to avoid inconsistent SODA state.
|
||||
// TODO: b/375306309 - Separate set and install steps when the state
|
||||
// become consistent after implementing `OnSodaUninstalled`.
|
||||
return html`
|
||||
<settings-row>
|
||||
${name}
|
||||
<span slot="action" @click=${onSelectAndDownload}></span>
|
||||
</settings-row>
|
||||
`;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return assertExhaustive(sodaState.kind);
|
||||
}
|
||||
private onSelectAndDownload(ev: CustomEvent<LanguageCode>) {
|
||||
setTranscriptionLanguage(ev.detail);
|
||||
}
|
||||
|
||||
private renderSelectedLanguage(): RenderResult {
|
||||
const selectedLanguage = this.platformHandler.getSelectedLanguage();
|
||||
private renderSelectedLanguage(
|
||||
selectedLanguage: LanguageCode|null,
|
||||
): RenderResult {
|
||||
const noSelectionRow = html`
|
||||
<settings-row>
|
||||
<span slot="label">
|
||||
${i18n.languagePickerSelectedLanguageNoneLabel}
|
||||
</span>
|
||||
</settings-row>
|
||||
`;
|
||||
if (selectedLanguage === null) {
|
||||
return html`
|
||||
<settings-row>
|
||||
<span slot="label">
|
||||
${i18n.languagePickerSelectedLanguageNoneLabel}
|
||||
</span>
|
||||
</settings-row>
|
||||
`;
|
||||
return noSelectionRow;
|
||||
}
|
||||
const sodaState = this.platformHandler.getSodaState(selectedLanguage).value;
|
||||
if (sodaState.kind !== 'installed' && sodaState.kind !== 'installing') {
|
||||
return html`
|
||||
<settings-row>
|
||||
<span slot="label">
|
||||
${i18n.languagePickerSelectedLanguageNoneLabel}
|
||||
</span>
|
||||
</settings-row>
|
||||
`;
|
||||
return noSelectionRow;
|
||||
}
|
||||
return this.renderLanguageRow(
|
||||
this.platformHandler.getLangPackInfo(selectedLanguage),
|
||||
selectedLanguage,
|
||||
);
|
||||
}
|
||||
|
||||
private renderAvailableLanguages(): RenderResult {
|
||||
const list = this.platformHandler.getLangPackList();
|
||||
const selectedLanguage = this.platformHandler.getSelectedLanguage();
|
||||
return map(
|
||||
list,
|
||||
(langPack) => this.renderLanguageRow(langPack, selectedLanguage),
|
||||
);
|
||||
const langPack = this.platformHandler.getLangPackInfo(selectedLanguage);
|
||||
return html`
|
||||
<settings-row>
|
||||
<span slot="label">
|
||||
${langPack.displayName}
|
||||
</span>
|
||||
</settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
override render(): RenderResult {
|
||||
// TODO: b/377885042 - Render "close" button when language picker is not
|
||||
// inside the setting menu.
|
||||
const selectedLanguage = this.platformHandler.getSelectedLanguage();
|
||||
// TODO: b/384418702 - Update back button aria label and language list role
|
||||
// after spec is ready.
|
||||
return html`
|
||||
<div id="root">
|
||||
<div id="header">
|
||||
@ -255,13 +141,21 @@ export class LanguagePicker extends ReactiveLitElement {
|
||||
<div id="content">
|
||||
<div class="section">
|
||||
<h4 class="title">${i18n.languagePickerSelectedLanguageHeader}</h4>
|
||||
<div class="body">${this.renderSelectedLanguage()}</div>
|
||||
<div class="body">
|
||||
${this.renderSelectedLanguage(selectedLanguage)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h4 class="title">
|
||||
${i18n.languagePickerAvailableLanguagesHeader}
|
||||
</h4>
|
||||
<div class="body">${this.renderAvailableLanguages()}</div>
|
||||
<language-list
|
||||
class="body"
|
||||
.selectedLanguage=${selectedLanguage}
|
||||
@language-select-click=${this.onSelectAndDownload}
|
||||
@language-download-click=${this.onSelectAndDownload}
|
||||
>
|
||||
</language-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -132,6 +132,10 @@ export class SettingsRow extends ReactiveLitElement {
|
||||
<slot name="status"></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override click(): void {
|
||||
this.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('settings-row', SettingsRow);
|
||||
|
Reference in New Issue
Block a user