blob: 735beeac5c34f2cb45e5f39e6d25142beffbc374 [file] [log] [blame]
// 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 './composebox_tool_chip.js';
import './context_menu_entrypoint.js';
import './contextual_entrypoint_and_carousel.js';
import './composebox_dropdown.js';
import './error_scrim.js';
import './file_carousel.js';
import './icons.html.js';
import '//resources/cr_components/localized_link/localized_link.js';
import '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {getInstance as getAnnouncerInstance} from '//resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import type {CrIconButtonElement} from '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {I18nMixinLit} from '//resources/cr_elements/i18n_mixin_lit.js';
import {assert} from '//resources/js/assert.js';
import {EventTracker} from '//resources/js/event_tracker.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import {hasKeyModifiers} from '//resources/js/util.js';
import type {PropertyValues} from '//resources/lit/v3_0/lit.rollup.js';
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';
import type {AutocompleteMatch, AutocompleteResult, PageCallbackRouter as SearchboxPageCallbackRouter, PageHandlerRemote as SearchboxPageHandlerRemote, TabInfo} from '//resources/mojo/components/omnibox/browser/searchbox.mojom-webui.js';
import type {BigBuffer} from '//resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js';
import type {UnguessableToken} from '//resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-webui.js';
import type {Url} from '//resources/mojo/url/mojom/url.mojom-webui.js';
import type {ComposeboxFile} from './common.js';
import {getCss} from './composebox.css.js';
import {getHtml} from './composebox.html.js';
import type {PageHandlerRemote} from './composebox.mojom-webui.js';
import type {ComposeboxDropdownElement} from './composebox_dropdown.js';
import {ComposeboxProxyImpl} from './composebox_proxy.js';
import type {FileUploadErrorType} from './composebox_query.mojom-webui.js';
import {FileUploadStatus} from './composebox_query.mojom-webui.js';
import type {ContextualEntrypointAndCarouselElement} from './contextual_entrypoint_and_carousel.js';
import type {ErrorScrimElement} from './error_scrim.js';
export interface ComposeboxElement {
$: {
cancelIcon: CrIconButtonElement,
input: HTMLInputElement,
composebox: HTMLElement,
submitIcon: CrIconButtonElement,
matches: ComposeboxDropdownElement,
context: ContextualEntrypointAndCarouselElement,
errorScrim: ErrorScrimElement,
};
}
export class ComposeboxElement extends I18nMixinLit
(CrLitElement) {
static get is() {
return 'ntp-composebox';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
input_: {type: String},
isCollapsible: {
reflect: true,
type: Boolean,
},
expanded_: {
reflect: true,
type: Boolean,
},
result_: {type: Object},
submitEnabled_: {
reflect: true,
type: Boolean,
},
/**
* Index of the currently selected match, if any.
* Do not modify this. Use <composebox-dropdown> API to change
* selection.
*/
selectedMatchIndex_: {type: Number},
submitting_: {
reflect: true,
type: Boolean,
},
showDropdown_: {
reflect: true,
type: Boolean,
},
enableImageContextualSuggestions_: {
reflect: true,
type: Boolean,
},
inputPlaceholder_: {
reflect: true,
type: String,
},
smartComposeEnabled_: {
reflect: true,
type: Boolean,
},
smartComposeInlineHint_: {type: String},
showFileCarousel_: {
reflect: true,
type: Boolean,
},
inDeepSearchMode_: {
reflect: true,
type: Boolean,
},
inCreateImageMode_: {
reflect: true,
type: Boolean,
},
showContextMenuDescription_: {type: Boolean},
inputsDisabled_: {
reflect: true,
type: Boolean,
},
lensButtonDisabled_: {
reflect: true,
type: Boolean,
},
ntpRealboxNextEnabled: {
type: Boolean,
reflect: true,
},
};
}
accessor ntpRealboxNextEnabled: boolean = false;
// If isCollapsible is set to true, the composebox will be a pill shape until
// it gets focused, at which point it will expand. If false, defaults to the
// expanded state.
protected accessor isCollapsible: boolean = false;
// Whether the composebox is currently expanded. Always true if isCollapsible
// is false.
protected accessor expanded_: boolean = false;
protected accessor input_: string = '';
protected accessor showDropdown_: boolean =
loadTimeData.getBoolean('composeboxShowZps');
protected accessor enableImageContextualSuggestions_: boolean =
loadTimeData.getBoolean('composeboxShowImageSuggest');
// When enabled, the file input buttons will not be rendered.
protected accessor selectedMatchIndex_: number = -1;
protected accessor submitting_: boolean = false;
protected accessor submitEnabled_: boolean = false;
protected accessor result_: AutocompleteResult|null = null;
protected accessor smartComposeInlineHint_: string = '';
protected accessor smartComposeEnabled_: boolean =
loadTimeData.getBoolean('composeboxSmartComposeEnabled');
protected accessor inputPlaceholder_: string =
loadTimeData.getString('searchboxComposePlaceholder');
protected accessor showFileCarousel_: boolean = false;
protected accessor inCreateImageMode_: boolean = false;
protected accessor inDeepSearchMode_: boolean = false;
protected accessor showContextMenuDescription_: boolean = true;
protected accessor inputsDisabled_: boolean = false;
protected accessor lensButtonDisabled_: boolean = false;
private showTypedSuggest_: boolean =
loadTimeData.getBoolean('composeboxShowTypedSuggest');
private showZps: boolean = loadTimeData.getBoolean('composeboxShowZps');
private browserProxy: ComposeboxProxyImpl = ComposeboxProxyImpl.getInstance();
private searchboxCallbackRouter_: SearchboxPageCallbackRouter;
private pageHandler_: PageHandlerRemote;
private searchboxHandler_: SearchboxPageHandlerRemote;
private eventTracker_: EventTracker = new EventTracker();
private searchboxListenerIds: number[] = [];
private composeboxCloseByEscape_: boolean =
loadTimeData.getBoolean('composeboxCloseByEscape');
private contextFilesSize_: number = 0;
private selectedMatch_: AutocompleteMatch|null = null;
private lastQueriedInput_: string = '';
constructor() {
super();
this.pageHandler_ = ComposeboxProxyImpl.getInstance().handler;
this.searchboxCallbackRouter_ =
ComposeboxProxyImpl.getInstance().searchboxCallbackRouter;
this.searchboxHandler_ = ComposeboxProxyImpl.getInstance().searchboxHandler;
}
override connectedCallback() {
super.connectedCallback();
// Set the initial expanded state based on the inputted property.
this.expanded_ = !this.isCollapsible;
this.searchboxListenerIds = [
this.searchboxCallbackRouter_.autocompleteResultChanged.addListener(
this.onAutocompleteResultChanged_.bind(this)),
this.searchboxCallbackRouter_.onContextualInputStatusChanged.addListener(
(token: UnguessableToken, status: FileUploadStatus,
errorType: FileUploadErrorType) => {
const {file, errorMessage} =
this.$.context.updateFileStatus(token, status, errorType);
if (errorMessage) {
this.$.errorScrim.setErrorMessage(errorMessage);
} else if (file) {
if (status === FileUploadStatus.kProcessing && this.showZps &&
(this.enableImageContextualSuggestions_ ||
!file.type.includes('image'))) {
// Query autocomplete to get contextual suggestions for files.
this.queryAutocomplete(/* clearMatches= */ true);
}
if (file.type.includes('image') &&
!this.enableImageContextualSuggestions_) {
this.showDropdown_ = false;
}
if (status === FileUploadStatus.kUploadSuccessful) {
const announcer = getAnnouncerInstance();
announcer.announce(
this.i18n('composeboxFileUploadCompleteText'));
}
}
}),
];
this.eventTracker_.add(this.$.input, 'input', () => {
this.submitEnabled_ = this.input_.trim().length > 0;
});
this.eventTracker_.add(this.$.context, 'on-context-files-changed',
(e: CustomEvent<{files: number}>) => {
this.contextFilesSize_ = e.detail.files;
this.submitEnabled_ = this.contextFilesSize_ > 0;
});
this.$.input.focus();
if (this.showZps) {
this.queryAutocomplete(/* clearMatches= */ false);
}
this.searchboxHandler_.notifySessionStarted();
}
override disconnectedCallback() {
super.disconnectedCallback();
this.searchboxHandler_.notifySessionAbandoned();
this.searchboxListenerIds.forEach(
id => assert(
this.browserProxy.searchboxCallbackRouter.removeListener(id)));
this.searchboxListenerIds = [];
this.eventTracker_.removeAll();
}
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
let showDropdownUpdated = changedPrivateProperties.has('showDropdown_');
// When the result initially gets set check if dropdown should show.
if (changedPrivateProperties.has('input_') ||
changedPrivateProperties.has('result_')) {
const prevValue = this.showDropdown_;
this.showDropdown_ = this.computeShowDropdown_();
showDropdownUpdated ||= this.showDropdown_ !== prevValue;
}
if (this.ntpRealboxNextEnabled && showDropdownUpdated) {
this.dispatchEvent(
new CustomEvent('composebox-dropdown-visible-changed', {
bubbles: true,
composed: true,
detail: {value: this.showDropdown_},
}));
}
}
override updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
const changedPrivateProperties =
changedProperties as Map<PropertyKey, unknown>;
if (changedPrivateProperties.has('selectedMatchIndex_')) {
if (this.selectedMatch_) {
// If the selected match is the default match (typing) the input will
// already have been set by handleInput.
if (!(this.selectedMatchIndex_ === 0 &&
this.selectedMatch_.allowedToBeDefaultMatch)) {
// Update the input.
const text = this.selectedMatch_.fillIntoEdit;
assert(text);
this.input_ = text;
this.submitEnabled_ = true;
}
} else if (!this.lastQueriedInput_) {
// This is for cases when focus leaves the matches/input.
// If there was already text in the input do not clear it.
this.input_ = '';
this.submitEnabled_ = false;
}
}
if (changedPrivateProperties.has('smartComposeInlineHint_')) {
if (this.smartComposeInlineHint_) {
this.adjustInputForSmartCompose();
} else {
// Unset the height override so input can expand through typing.
this.$.input.style.height =
'calc-size(fit-content, min(size + 4px, 190px))';
}
}
}
setContext(files: ComposeboxFile[]) {
this.$.context.setContextFiles(files);
}
getText() {
return this.input_;
}
setText(text: string) {
this.input_ = text;
}
getAndResetContextFiles() {
let files: ComposeboxFile[] = [];
if (this.contextFilesSize_ > 0) {
files = this.$.context.resetContextFiles();
this.contextFilesSize_ = 0;
this.searchboxHandler_.clearFiles();
}
return files;
}
resetModes() {
this.$.context.resetModes();
}
closeDropdown() {
this.clearAutocompleteMatches_();
}
getSmartComposeForTesting() {
return this.smartComposeInlineHint_;
}
protected computeCancelButtonTitle_() {
return this.input_.trim().length > 0 || this.contextFilesSize_ > 0 ?
this.i18n('composeboxCancelButtonTitleInput') :
this.i18n('composeboxCancelButtonTitle');
}
private computeShowDropdown_() {
// Don't show dropdown if there's no results.
if (!this.result_?.matches.length) {
return false;
}
if (this.showTypedSuggest_ && this.input_.trim()) {
// Do not show dropdown for multiline input.
if (this.$.input.scrollHeight <= 48) {
return true;
}
}
// lastQueriedInput_ is used here since the input_ changes based on
// the selected match. If typed suggest is not enabled and input_ is used,
// the dropdown will hide if the user keys down over zps matches.
return this.showZps && !this.lastQueriedInput_;
}
protected shouldShowSuggestionActivityLink_() {
if (!this.result_ || !this.showDropdown_) {
return false;
}
return this.result_.matches.some((match) => match.isNoncannedAimSuggestion);
}
protected shouldShowSmartComposeInlineHint_() {
return !!this.smartComposeInlineHint_;
}
protected onFileValidationError_(e: CustomEvent<{errorMessage: string}>) {
this.$.errorScrim.setErrorMessage(e.detail.errorMessage);
}
protected deleteContext_(e: CustomEvent<{uuid: UnguessableToken}>) {
this.searchboxHandler_.deleteContext(e.detail.uuid);
this.$.input.focus();
this.queryAutocomplete(/* clearMatches= */ true);
}
protected async addFileContext_(e: CustomEvent<{
files: File[], isImage: boolean,
onContextAdded: (files: Map<UnguessableToken, ComposeboxFile>) => void,
}>) {
const composeboxFiles: Map<UnguessableToken, ComposeboxFile> = new Map();
for (const file of e.detail.files) {
const fileBuffer = await file.arrayBuffer();
const bigBuffer:
BigBuffer = {bytes: Array.from(new Uint8Array(fileBuffer))};
const {token} = await this.searchboxHandler_.addFileContext(
{
fileName: file.name,
mimeType: file.type,
selectionTime: new Date(),
},
bigBuffer);
const attachment: ComposeboxFile = {
uuid: token,
name: file.name,
objectUrl: e.detail.isImage ? URL.createObjectURL(file) : null,
type: file.type,
status: FileUploadStatus.kNotUploaded,
url: null,
file: file,
tabId: null,
};
composeboxFiles.set(token, attachment);
const announcer = getAnnouncerInstance();
announcer.announce(this.i18n('composeboxFileUploadStartedText'));
}
e.detail.onContextAdded(composeboxFiles);
this.$.input.focus();
}
protected async addTabContext_(e: CustomEvent<{
id: number, title: string, url: Url,
onContextAdded: (file: ComposeboxFile) => void,
}>) {
const {token} = await this.searchboxHandler_.addTabContext(e.detail.id);
if (!token) {
return;
}
const attachment: ComposeboxFile = {
uuid: token,
name: e.detail.title,
objectUrl: null,
type: 'tab',
status: FileUploadStatus.kNotUploaded,
url: e.detail.url,
file: null,
tabId: e.detail.id,
};
e.detail.onContextAdded(attachment);
this.$.input.focus();
}
protected async refreshTabSuggestions_(
e: CustomEvent<{onRefreshComplete: (tabs: TabInfo[]) => void}>) {
const {tabs} = await this.searchboxHandler_.getRecentTabs();
e.detail.onRefreshComplete(tabs);
}
protected async getTabPreview_(e: CustomEvent<{
tabId: number,
onPreviewFetched: (previewDataUrl: string) => void,
}>) {
const {previewDataUrl} =
await this.searchboxHandler_.getTabPreview(e.detail.tabId);
e.detail.onPreviewFetched(previewDataUrl || '');
}
protected onCancelClick_() {
if (this.input_.trim().length > 0 || this.contextFilesSize_ > 0) {
this.input_ = '';
this.$.context.resetContextFiles();
this.contextFilesSize_ = 0;
this.smartComposeInlineHint_ = '';
this.submitEnabled_ = false;
this.searchboxHandler_.clearFiles();
this.$.input.focus();
this.queryAutocomplete(/* clearMatches= */ true);
} else {
this.closeComposebox_();
}
}
protected onLensClick_() {
this.pageHandler_.handleLensButtonClick();
}
protected onLensIconMouseDown_(e: MouseEvent) {
// Prevent the composebox from expanding due to being focused by capturing
// the mousedown event. This is needed to allow the Lens icon to be
// clicked when the composebox does not have focus without expanding the
// composebox.
e.preventDefault();
}
private updateInputPlaceholder_() {
if (this.inDeepSearchMode_) {
this.inputPlaceholder_ =
loadTimeData.getString('composeDeepSearchPlaceholder');
} else if (this.inCreateImageMode_) {
this.inputPlaceholder_ =
loadTimeData.getString('composeCreateImagePlaceholder');
} else {
this.inputPlaceholder_ =
loadTimeData.getString('searchboxComposePlaceholder');
}
}
protected async setDeepSearchMode_(
e: CustomEvent<{inDeepSearchMode: boolean}>) {
this.pageHandler_.setDeepSearchMode(e.detail.inDeepSearchMode);
this.queryAutocomplete(/* clearMatches= */ true);
this.updateInputPlaceholder_();
await this.updateComplete;
this.$.input.focus();
}
protected async setCreateImageMode_(
e: CustomEvent<{inCreateImageMode: boolean}>) {
this.pageHandler_.setCreateImageMode(e.detail.inCreateImageMode);
this.queryAutocomplete(/* clearMatches= */ true);
this.updateInputPlaceholder_();
await this.updateComplete;
this.$.input.focus();
}
// Sets the input property to compute the cancel button title without using
// "$." syntax as this is not allowed in WillUpdate().
protected handleInput_(e: Event) {
const inputElement = e.target as HTMLInputElement;
this.input_ = inputElement.value;
if (!this.enableImageContextualSuggestions_ &&
this.$.context.hasImageFiles()) {
return;
}
// `clearMatches` is true if input is empty stop any in progress providers
// before requerying for on-focus (zero-suggest) inputs. The searchbox
// doesn't allow zero-suggest requests to be made while the ACController is
// not done.
this.queryAutocomplete(/* clearMatches= */ this.input_ === '');
}
protected onKeydown_(e: KeyboardEvent) {
const KEYDOWN_HANDLED_KEYS = [
'ArrowDown',
'ArrowUp',
'Enter',
'Escape',
'PageDown',
'PageUp',
'Tab',
];
if (!KEYDOWN_HANDLED_KEYS.includes(e.key)) {
return;
}
if (this.shadowRoot.activeElement === this.$.input) {
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') &&
!this.showDropdown_) {
return;
}
if (e.key === 'Tab') {
// If focus leaves the input, unselect the first match.
if (e.shiftKey) {
this.$.matches.unselect();
} else if (this.smartComposeEnabled_ && this.smartComposeInlineHint_) {
this.input_ = this.input_ + this.smartComposeInlineHint_;
this.smartComposeInlineHint_ = '';
e.preventDefault();
this.queryAutocomplete(/* clearMatches= */ true);
}
return;
}
}
if (e.key === 'Enter' && this.submitEnabled_) {
if (this.shadowRoot.activeElement === this.$.matches || !e.shiftKey) {
e.preventDefault();
this.submitQuery_(e);
}
}
if (e.key === 'Escape' && this.composeboxCloseByEscape_) {
this.closeComposebox_();
e.preventDefault();
}
// Do not handle the following keys if there are no matches available.
if (!this.result_ || this.result_.matches.length === 0) {
return;
}
// Do not handle the following keys if there are key modifiers.
if (hasKeyModifiers(e)) {
return;
}
if (e.key === 'ArrowDown') {
this.$.matches.selectNext();
} else if (e.key === 'ArrowUp') {
this.$.matches.selectPrevious();
} else if (e.key === 'Escape' || e.key === 'PageUp') {
this.$.matches.selectFirst();
} else if (e.key === 'PageDown') {
this.$.matches.selectLast();
} else if (e.key === 'Tab') {
// If focus goes past the last match, unselect the last match.
if (this.selectedMatchIndex_ === this.result_.matches.length - 1) {
this.$.matches.unselect();
}
return;
}
e.preventDefault();
// Focus the selected match if focus is currently in the matches.
if (this.shadowRoot.activeElement === this.$.matches) {
this.$.matches.focusSelected();
}
}
protected handleInputFocusIn_() {
// if there's a last queried input, it's guaranteed that at least
// the verbatim match will exist.
if (this.lastQueriedInput_ && this.result_?.matches.length) {
this.$.matches.selectFirst();
}
}
protected handleComposeboxFocusIn_(e: FocusEvent) {
// Exit early if the focus is still within the composebox.
if (this.$.composebox.contains(e.relatedTarget as Node)) {
return;
}
this.expanded_ = true;
this.submitting_ = false;
this.pageHandler_.focusChanged(true);
this.fire('composebox-focus-in');
}
protected handleComposeboxFocusOut_(e: FocusEvent) {
// Exit early if the focus is still within the composebox.
if (this.$.composebox.contains(e.relatedTarget as Node)) {
return;
}
// If the input is blurred and the composebox is expandable, collapse it.
// Else, keep the composebox expanded.
this.expanded_ = !this.isCollapsible;
this.pageHandler_.focusChanged(false);
this.fire('composebox-focus-out');
}
protected handleScroll_() {
const smartCompose =
this.shadowRoot.querySelector<HTMLElement>('#smartCompose');
if (!smartCompose) {
return;
}
smartCompose.scrollTop = this.$.input.scrollTop;
}
protected handleSubmitFocusIn_() {
// Matches should always be greater than 0 due to verbatim match.
if (this.input_ && !this.selectedMatch_) {
this.$.matches.selectFirst();
}
}
private closeComposebox_() {
this.resetModes();
this.fire('close-composebox', {
composeboxText: this.input_,
contextFiles: this.getAndResetContextFiles(),
});
if (this.isCollapsible) {
this.expanded_ = false;
this.$.input.blur();
}
}
protected submitQuery_(e: KeyboardEvent|MouseEvent) {
// Users are allowed to submit queries that consist of only files with no
// input. `selectedMatchIndex_` will be >= 0 when there is non-empty input
// since the verbatim match is present.
assert(
(this.selectedMatchIndex_ >= 0 && this.result_) ||
this.contextFilesSize_ > 0);
// If there is a match that is selected, open that match, else follow the
// non-autocomplete submission flow. The non-autocomplete submission flow
// will not have omnibox metrics recorded for it.
if (this.selectedMatchIndex_ >= 0) {
const match = this.result_!.matches[this.selectedMatchIndex_];
assert(match);
this.searchboxHandler_.openAutocompleteMatch(
this.selectedMatchIndex_, match.destinationUrl,
/* are_matches_showing */ true, (e as MouseEvent).button || 0,
e.altKey, e.ctrlKey, e.metaKey, e.shiftKey);
} else {
this.searchboxHandler_.submitQuery(
this.input_.trim(), (e as MouseEvent).button || 0, e.altKey,
e.ctrlKey, e.metaKey, e.shiftKey);
}
this.submitting_ = true;
// If the composebox is expandable, collapse it and clear the input after
// submitting.
if (this.isCollapsible) {
this.setText('');
this.$.input.blur();
this.submitEnabled_ = false;
}
}
/**
* @param e Event containing index of the match that received focus.
*/
protected onMatchFocusin_(e: CustomEvent<{index: number}>) {
// Select the match that received focus.
this.$.matches.selectIndex(e.detail.index);
}
protected onMatchClick_() {
this.clearAutocompleteMatches_();
}
protected onSelectedMatchIndexChanged_(e: CustomEvent<{value: number}>) {
this.selectedMatchIndex_ = e.detail.value;
this.selectedMatch_ =
this.result_?.matches[this.selectedMatchIndex_] || null;
}
/**
* Clears the autocomplete result on the page and on the autocomplete backend.
*/
private clearAutocompleteMatches_() {
this.showDropdown_ = false;
this.result_ = null;
this.$.matches.unselect();
this.searchboxHandler_.stopAutocomplete(/*clearResult=*/ true);
// Autocomplete sends updates once it is stopped. Invalidate those results
// by setting the |this.lastQueriedInput_| to its default value.
this.lastQueriedInput_ = '';
}
private onAutocompleteResultChanged_(result: AutocompleteResult) {
if (this.lastQueriedInput_ === null ||
this.lastQueriedInput_.trimStart() !== result.input) {
return;
}
this.result_ = result;
const hasMatches = this.result_.matches.length > 0;
const firstMatch = hasMatches ? this.result_.matches[0] : null;
// Zero suggest matches are not allowed to be default. Therefore, this
// makes sure zero suggest results aren't focused when they are returned.
if (firstMatch && firstMatch.allowedToBeDefaultMatch) {
this.$.matches.selectFirst();
} else if (
this.input_.trim() && hasMatches && this.selectedMatchIndex_ >= 0 &&
this.selectedMatchIndex_ < this.result_.matches.length) {
// Restore the selection and update the input. Don't restore when the
// user deletes all their input and autocomplete is queried or else the
// empty input will change to the value of the first result.
this.$.matches.selectIndex(this.selectedMatchIndex_);
// Set the selected match since the `selectedMatchIndex_` does not change
// (and therefore `selectedMatch_` does not get updated since
// `onSelectedMatchIndexChanged_` is not called).
this.selectedMatch_ = this.result_.matches[this.selectedMatchIndex_]!;
this.input_ = this.selectedMatch_.fillIntoEdit;
} else {
this.$.matches.unselect();
}
// Populate the smart compose suggestion.
this.smartComposeInlineHint_ = this.result_.smartComposeInlineHint ?
this.result_.smartComposeInlineHint :
'';
}
private adjustInputForSmartCompose() {
// Checks the scroll height of the input + smart complete hint (ghost div)
// and updates the height of the actual input to be that height so the
// ghost text does not overflow.
const smartCompose =
this.shadowRoot.querySelector<HTMLElement>('#smartCompose');
const ghostHeight = smartCompose!.scrollHeight;
const maxHeight = 190;
this.$.input.style.height = `${Math.min(ghostHeight, maxHeight)}px`;
// If the height of the input + smart complete hint is greater than the max
// height, scroll the smart compose as the input will already scroll. Note
// there is an issue at the break point since the input will not have
// scrolled yet as it does not have enough content. The smart compose will
// display the ghost text below the input and it will be cut off. However,
// the current response only works for queries below the max height.
if (ghostHeight > maxHeight) {
smartCompose!.scrollTop = this.$.input.scrollTop;
}
}
// `queryAutocomplete` updates the `lastQueriedInput_` and makes an
// autocomplete call through the handler. It also optionally clears existing
// matches.
private queryAutocomplete(clearMatches: boolean) {
if (clearMatches) {
this.clearAutocompleteMatches_();
}
this.lastQueriedInput_ = this.input_;
this.searchboxHandler_.queryAutocomplete(this.input_, false);
}
}
declare global {
interface HTMLElementTagNameMap {
'ntp-composebox': ComposeboxElement;
}
}
customElements.define(ComposeboxElement.is, ComposeboxElement);