From 7e4189e15aacf93b3091de68d25d81e8ede21727 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 4 Apr 2022 20:23:12 +0200 Subject: [PATCH 1/4] First draft for a custom select component --- .../src/browser/style/select-component.css | 79 ++++++ .../src/browser/widgets/select-component.tsx | 235 ++++++++++++++++++ .../components/preference-select-input.ts | 64 +++-- 3 files changed, 359 insertions(+), 19 deletions(-) create mode 100644 packages/core/src/browser/style/select-component.css create mode 100644 packages/core/src/browser/widgets/select-component.tsx diff --git a/packages/core/src/browser/style/select-component.css b/packages/core/src/browser/style/select-component.css new file mode 100644 index 0000000000000..7b17edac9f88c --- /dev/null +++ b/packages/core/src/browser/style/select-component.css @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (C) 2022 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.theia-select-component { + background-color: var(--theia-dropdown-background); + outline: var(--theia-dropdown-border) solid 1px; + outline-offset: -1px; + height: 26px; + padding: 0px 8px; + width: 320px; + display: flex; + align-items: center; +} + +.theia-select-component .theia-select-component-label { + width: 100%; + color: var(--theia-dropdown-foreground); +} + +.theia-select-component:focus { + outline-color: var(--theia-focusBorder); +} + +.theia-select-component-dropdown { + font-family: var(--theia-ui-font-family); + font-size: var(--theia-ui-font-size1); + color: var(--theia-foreground); + background-color: var(--theia-settings-dropdownBackground); + outline: var(--theia-focusBorder) solid 1px; + outline-offset: -1px; +} + +.theia-select-component-dropdown .theia-select-component-option { + display: flex; + padding: 2px 5px; +} + +.theia-select-component-dropdown .theia-select-component-description { + padding: 6px 5px; + border-top: 1px solid var(--theia-editorWidget-border); + margin-top: 2px; +} + +.theia-select-component-dropdown .theia-select-component-option .theia-select-component-option-value { + width: 100%; +} + +.theia-select-component-dropdown .theia-select-component-option:not(.selected) .theia-select-component-option-detail { + color: var(--theia-textLink-foreground); +} + +.theia-select-component-dropdown .theia-select-component-option.selected { + color: var(--theia-dropdown-foreground); + background: var(--theia-list-activeSelectionBackground); + outline: var(--theia-focusBorder) solid 1px; + outline-offset: -1px; +} + +.theia-select-component-dropdown .theia-select-component-separator { + width: 100%; + height: 2px; + padding: 5px 3px; + background: var(--theia-dropdown-foreground); +} + +/** color: var(--theia-settings-headerForeground); */ diff --git a/packages/core/src/browser/widgets/select-component.tsx b/packages/core/src/browser/widgets/select-component.tsx new file mode 100644 index 0000000000000..136185ef75254 --- /dev/null +++ b/packages/core/src/browser/widgets/select-component.tsx @@ -0,0 +1,235 @@ +// ***************************************************************************** +// Copyright (C) 2022 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as DOMPurify from 'dompurify'; +import * as markdownit from 'markdown-it'; +import { Event as TheiaEvent } from '../../common/event'; +import { codicon } from './widget'; + +import '../../../src/browser/style/select-component.css'; + +const markdownRenderer = markdownit(); + +export interface SelectOption { + value?: string + label?: string + separator?: boolean + disabled?: boolean + detail?: string + description?: string + markdownDescription?: string + onSelected?: () => void +} + +export interface SelectComponentProps { + options: SelectOption[] + selected: number + onSelected?: (index: number, option: SelectOption) => void + onDidChange?: TheiaEvent +} + +export type SelectComponentDropdownDimensions = { + top: number + left: number + width: number +} | 'hidden'; + +export interface SelectComponentState { + dimensions: SelectComponentDropdownDimensions + selected: number +} + +export const SELECT_COMPONENT_CONTAINER = 'select-component-container'; + +export class SelectComponent extends React.Component { + protected dropdownElement: HTMLElement; + protected fieldRef = React.createRef(); + protected mountedListeners: Map = new Map(); + + constructor(props: SelectComponentProps) { + super(props); + this.state = { + dimensions: 'hidden', + selected: props.selected + }; + + let list = document.getElementById(SELECT_COMPONENT_CONTAINER); + if (!list) { + list = document.createElement('div'); + list.id = SELECT_COMPONENT_CONTAINER; + document.body.appendChild(list); + } + this.dropdownElement = list; + } + + override componentDidMount(): void { + const hide = () => this.hide(); + this.mountedListeners.set('click', hide); + this.mountedListeners.set('scroll', hide); + this.mountedListeners.set('wheel', hide); + + for (const [key, listener] of this.mountedListeners.entries()) { + window.addEventListener(key, listener); + } + } + + override componentWillUnmount(): void { + for (const [key, listener] of this.mountedListeners.entries()) { + window.removeEventListener(key, listener); + } + } + + override render(): React.ReactNode { + const { options } = this.props; + const { selected } = this.state; + const selectedItemLabel = options[selected].label ?? options[selected].value; + return <> +
this.handleClickEvent(e)} + onKeyDown={e => this.handleKeypress(e)} + > +
{selectedItemLabel}
+
+
+ {ReactDOM.createPortal(this.renderDropdown(), this.dropdownElement)} + ; + } + + protected handleKeypress(ev: React.KeyboardEvent): void { + if (!this.fieldRef.current) { + return; + } + if (ev.key === 'ArrowUp') { + let selected = this.state.selected; + if (selected <= 0) { + selected = this.props.options.length - 1; + } else { + selected--; + } + this.setState({ + selected + }); + } else if (ev.key === 'ArrowDown') { + const selected = (this.state.selected + 1) % this.props.options.length; + this.setState({ + selected + }); + } else if (ev.key === 'Enter') { + if (this.state.dimensions === 'hidden') { + this.toggleVisibility(); + } else { + const selected = this.state.selected; + this.selectOption(selected, this.props.options[selected]); + } + } else if (ev.key === 'Escape') { + this.hide(); + } + ev.stopPropagation(); + ev.nativeEvent.stopImmediatePropagation(); + } + + protected handleClickEvent(event: React.MouseEvent): void { + this.toggleVisibility(); + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + } + + protected toggleVisibility(): void { + if (!this.fieldRef.current) { + return; + } + if (this.state.dimensions === 'hidden') { + const rect = this.fieldRef.current.getBoundingClientRect(); + this.setState({ + dimensions: { + top: rect.top + rect.height, + left: rect.left, + width: rect.width + }, + }); + } else { + this.hide(); + } + } + + protected hide(): void { + this.setState({ dimensions: 'hidden' }); + } + + protected renderDropdown(): React.ReactNode { + if (this.state.dimensions === 'hidden') { + return; + } + const { options } = this.props; + const { selected } = this.state; + const description = selected !== undefined && options[selected].description; + const markdownDescription = selected !== undefined && options[selected].markdownDescription; + return
+ { + options.map((item, i) => this.renderOption(i, item)) + } + {markdownDescription && ( +
// eslint-disable-line react/no-danger + )} + {description && !markdownDescription && ( +
+ {description} +
+ )} +
; + } + + protected renderOption(index: number, option: SelectOption): React.ReactNode { + if (option.separator) { + return
; + } + const selected = this.state.selected; + return ( +
{ + this.setState({ + selected: index + }); + }} + onClick={() => { + this.selectOption(index, option); + }} + > +
{option.label ?? option.value}
+ {option.detail &&
{option.detail}
} +
+ ); + } + + protected selectOption(index: number, option: SelectOption): void { + option.onSelected?.(); + this.props.onSelected?.(index, option); + this.hide(); + } +} diff --git a/packages/preferences/src/browser/views/components/preference-select-input.ts b/packages/preferences/src/browser/views/components/preference-select-input.ts index 0f9ac145c4a23..ede22b72260c4 100644 --- a/packages/preferences/src/browser/views/components/preference-select-input.ts +++ b/packages/preferences/src/browser/views/components/preference-select-input.ts @@ -17,29 +17,56 @@ import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer'; import { injectable, interfaces } from '@theia/core/shared/inversify'; import { JSONValue } from '@theia/core/shared/@phosphor/coreutils'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; import { Preference } from '../../util/preference-types'; import { PreferenceLeafNodeRendererContribution } from './preference-node-renderer-creator'; +import * as React from '@theia/core/shared/react'; +import * as ReactDOM from '@theia/core/shared/react-dom'; @injectable() -export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer { +export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer { + + protected readonly onDidChangeEmitter = new Emitter(); + + protected get onDidChange(): Event { + return this.onDidChangeEmitter.event; + } protected get enumValues(): JSONValue[] { return this.preferenceNode.preference.data.enum!; } + protected get selectComponentOptions(): SelectOption[] { + const items: SelectOption[] = []; + const values = this.enumValues; + const defaultValue = this.preferenceNode.preference.data.default; + for (let i = 0; i < values.length; i++) { + const index = i; + const value = `${values[i]}`; + const detail = defaultValue === value ? 'default' : undefined; + const enumDescription = this.preferenceNode.preference.data.enumDescriptions?.[i]; + const markdownEnumDescription = this.preferenceNode.preference.data.markdownEnumDescriptions?.[i]; + items.push({ + value, + detail, + description: enumDescription, + markdownDescription: markdownEnumDescription, + onSelected: () => this.handleUserInteraction(index) + }); + } + return items; + } + protected createInteractable(parent: HTMLElement): void { - const { enumValues } = this; - const interactable = document.createElement('select'); + const interactable = document.createElement('div'); + const selectComponent = React.createElement(SelectComponent, { + options: this.selectComponentOptions, + selected: this.getDataValue(), + onDidChange: this.onDidChange + }); this.interactable = interactable; - interactable.classList.add('theia-select'); - interactable.onchange = this.handleUserInteraction.bind(this); - for (const [index, value] of enumValues.entries()) { - const option = document.createElement('option'); - option.value = index.toString(); - option.textContent = `${value}`; - interactable.appendChild(option); - } - interactable.value = this.getDataValue(); + ReactDOM.render(selectComponent, interactable); parent.appendChild(interactable); } @@ -48,26 +75,25 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer value === currentValue); - return selected > -1 ? selected.toString() : '0'; + return selected > -1 ? selected : 0; } - protected handleUserInteraction(): void { - const value = this.enumValues[Number(this.interactable.value)]; + protected handleUserInteraction(selected: number): void { + const value = this.enumValues[selected]; this.setPreferenceImmediately(value); } } From 26e29077154b8864a4aabf0b0a8c746d6fca2464 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Tue, 5 Apr 2022 01:08:08 +0200 Subject: [PATCH 2/4] Finish select component draft --- packages/core/src/browser/browser.ts | 39 ++++ .../browser/common-frontend-contribution.ts | 28 +++ .../src/browser/style/select-component.css | 27 ++- .../src/browser/widgets/select-component.tsx | 172 ++++++++++++------ .../console/debug-console-contribution.tsx | 53 +++--- packages/debug/src/browser/style/index.css | 3 +- .../view/debug-configuration-widget.tsx | 35 ++-- .../browser/output-toolbar-contribution.tsx | 35 ++-- packages/output/src/browser/style/output.css | 4 + .../preferences/src/browser/style/index.css | 8 + .../components/preference-select-input.ts | 16 +- 11 files changed, 297 insertions(+), 123 deletions(-) diff --git a/packages/core/src/browser/browser.ts b/packages/core/src/browser/browser.ts index 039d876f1ec41..ec80e32a4ca94 100644 --- a/packages/core/src/browser/browser.ts +++ b/packages/core/src/browser/browser.ts @@ -156,3 +156,42 @@ export function preventNavigation(event: WheelEvent): void { event.preventDefault(); event.stopPropagation(); } + +export function measureTextWidth(text: string | string[]): number { + const measureElement = getMeasurementElement(); + text = Array.isArray(text) ? text : [text]; + let width = 0; + for (const item of text) { + measureElement.textContent = item; + width = Math.max(measureElement.getBoundingClientRect().width, width); + } + return width; +} + +export function measureTextHeight(text: string | string[], maxWidth?: number): number { + const measureElement = getMeasurementElement(); + if (maxWidth !== undefined) { + measureElement.style.maxWidth = `${Math.ceil(maxWidth)}px`; + } + text = Array.isArray(text) ? text : [text]; + let height = 0; + for (const item of text) { + measureElement.textContent = item; + height = Math.max(measureElement.getBoundingClientRect().height, height); + } + measureElement.style.maxWidth = 'none'; + return height; +} + +function getMeasurementElement(): HTMLElement { + let measureElement = document.getElementById('measure'); + if (!measureElement) { + measureElement = document.createElement('span'); + measureElement.id = 'measure'; + measureElement.style.fontFamily = 'var(--theia-ui-font-family)'; + measureElement.style.fontSize = 'var(--theia-ui-font-size1)'; + measureElement.style.visibility = 'hidden'; + document.body.appendChild(measureElement); + } + return measureElement; +} diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 58c5ab3d32ff2..26cd0ec9321d4 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -1765,6 +1765,34 @@ export class CommonFrontendContribution implements FrontendApplicationContributi { id: 'welcomePage.buttonHoverBackground', defaults: { dark: Color.rgba(200, 235, 255, .072), light: Color.rgba(0, 0, 0, .10) }, description: 'Hover background color for the buttons on the Welcome page.' }, { id: 'walkThrough.embeddedEditorBackground', defaults: { dark: Color.rgba(0, 0, 0, .4), light: '#f4f4f4' }, description: 'Background color for the embedded editors on the Interactive Playground.' }, + // Dropdown colors should be aligned with https://code.visualstudio.com/api/references/theme-color#dropdown-control + + { + id: 'dropdown.background', defaults: { + light: Color.white, + dark: '#3C3C3C', + hc: Color.black + }, description: 'Dropdown background.' + }, + { + id: 'dropdown.listBackground', defaults: { + hc: Color.black + }, description: 'Dropdown list background.' + }, + { + id: 'dropdown.foreground', defaults: { + dark: '#F0F0F0', + hc: Color.white + }, description: 'Dropdown foreground.' + }, + { + id: 'dropdown.border', defaults: { + light: '#CECECE', + dark: 'dropdown.background', + hc: '#6FC3DF' + }, description: 'Dropdown border.' + }, + // Settings Editor colors should be aligned with https://code.visualstudio.com/api/references/theme-color#settings-editor-colors { id: 'settings.headerForeground', defaults: { diff --git a/packages/core/src/browser/style/select-component.css b/packages/core/src/browser/style/select-component.css index 7b17edac9f88c..394a577a16cce 100644 --- a/packages/core/src/browser/style/select-component.css +++ b/packages/core/src/browser/style/select-component.css @@ -18,16 +18,20 @@ background-color: var(--theia-dropdown-background); outline: var(--theia-dropdown-border) solid 1px; outline-offset: -1px; - height: 26px; + min-height: 23px; + min-width: 90px; padding: 0px 8px; - width: 320px; display: flex; align-items: center; + user-select: none; } .theia-select-component .theia-select-component-label { width: 100%; color: var(--theia-dropdown-foreground); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } .theia-select-component:focus { @@ -41,6 +45,7 @@ background-color: var(--theia-settings-dropdownBackground); outline: var(--theia-focusBorder) solid 1px; outline-offset: -1px; + user-select: none; } .theia-select-component-dropdown .theia-select-component-option { @@ -50,6 +55,14 @@ .theia-select-component-dropdown .theia-select-component-description { padding: 6px 5px; +} + +.theia-select-component-dropdown .theia-select-component-description:first-child { + border-bottom: 1px solid var(--theia-editorWidget-border); + margin-bottom: 2px; +} + +.theia-select-component-dropdown .theia-select-component-description:last-child { border-top: 1px solid var(--theia-editorWidget-border); margin-top: 2px; } @@ -63,17 +76,15 @@ } .theia-select-component-dropdown .theia-select-component-option.selected { - color: var(--theia-dropdown-foreground); + color: var(--theia-list-activeSelectionForeground); background: var(--theia-list-activeSelectionBackground); outline: var(--theia-focusBorder) solid 1px; outline-offset: -1px; } .theia-select-component-dropdown .theia-select-component-separator { - width: 100%; + width: 84px; height: 2px; - padding: 5px 3px; - background: var(--theia-dropdown-foreground); + margin: 5px 3px; + background: var(--theia-foreground); } - -/** color: var(--theia-settings-headerForeground); */ diff --git a/packages/core/src/browser/widgets/select-component.tsx b/packages/core/src/browser/widgets/select-component.tsx index 136185ef75254..0bd9879f6139d 100644 --- a/packages/core/src/browser/widgets/select-component.tsx +++ b/packages/core/src/browser/widgets/select-component.tsx @@ -17,14 +17,12 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as DOMPurify from 'dompurify'; -import * as markdownit from 'markdown-it'; import { Event as TheiaEvent } from '../../common/event'; import { codicon } from './widget'; +import { measureTextHeight, measureTextWidth } from '../browser'; import '../../../src/browser/style/select-component.css'; -const markdownRenderer = markdownit(); - export interface SelectOption { value?: string label?: string @@ -32,26 +30,27 @@ export interface SelectOption { disabled?: boolean detail?: string description?: string - markdownDescription?: string - onSelected?: () => void + markdown?: boolean } export interface SelectComponentProps { options: SelectOption[] selected: number - onSelected?: (index: number, option: SelectOption) => void - onDidChange?: TheiaEvent + onChange?: (option: SelectOption, index: number) => void + onDidSelectedChange?: TheiaEvent } -export type SelectComponentDropdownDimensions = { +export interface SelectComponentDropdownDimensions { top: number left: number width: number -} | 'hidden'; + parentHeight: number +}; export interface SelectComponentState { - dimensions: SelectComponentDropdownDimensions + dimensions?: SelectComponentDropdownDimensions selected: number + original: number } export const SELECT_COMPONENT_CONTAINER = 'select-component-container'; @@ -59,13 +58,16 @@ export const SELECT_COMPONENT_CONTAINER = 'select-component-container'; export class SelectComponent extends React.Component { protected dropdownElement: HTMLElement; protected fieldRef = React.createRef(); - protected mountedListeners: Map = new Map(); + protected mountedListeners: Map = new Map(); + protected optimalWidth = 0; + protected optimalHeight = 0; constructor(props: SelectComponentProps) { super(props); + const selected = Math.max(props.selected, 0); this.state = { - dimensions: 'hidden', - selected: props.selected + selected, + original: selected }; let list = document.getElementById(SELECT_COMPONENT_CONTAINER); @@ -77,37 +79,80 @@ export class SelectComponent extends React.Component this.hide(); - this.mountedListeners.set('click', hide); + getOptimalWidth(): number { + const textWidth = measureTextWidth(this.props.options.map(e => e.label || e.value || '' + (e.detail || ''))); + return Math.ceil(textWidth + 16); + } + + getOptimalHeight(maxWidth?: number): number { + const firstLine = this.props.options.find(e => e.label || e.value || e.detail); + if (!firstLine) { + return 0; + } + if (maxWidth) { + maxWidth -= 10; // Decrease width by 10 due to side padding + } + const descriptionHeight = measureTextHeight(this.props.options.map(e => e.description || ''), maxWidth) + 18; + const singleLineHeight = measureTextHeight(firstLine.label || firstLine.value || firstLine.detail || '') + 6; + const optimal = descriptionHeight + singleLineHeight * this.props.options.length; + return optimal + 20; // Just to be safe, add another 20 pixels here + } + + attachListeners(): void { + const hide = () => { + this.hide(true); + }; this.mountedListeners.set('scroll', hide); this.mountedListeners.set('wheel', hide); + let parent = this.fieldRef.current?.parentElement; + while (parent) { + // Workaround for perfect scrollbar, since using `overflow: hidden` + // neither triggers the `scroll`, `wheel` nor `blur` event + if (parent.classList.contains('ps')) { + parent.addEventListener('ps-scroll-y', hide); + } + parent = parent.parentElement; + } + for (const [key, listener] of this.mountedListeners.entries()) { window.addEventListener(key, listener); } } override componentWillUnmount(): void { - for (const [key, listener] of this.mountedListeners.entries()) { - window.removeEventListener(key, listener); + if (this.mountedListeners.size > 0) { + const eventListener = this.mountedListeners.get('scroll')!; + let parent = this.fieldRef.current?.parentElement; + while (parent) { + parent.removeEventListener('ps-scroll-y', eventListener); + parent = parent.parentElement; + } + for (const [key, listener] of this.mountedListeners.entries()) { + window.removeEventListener(key, listener); + } } } override render(): React.ReactNode { const { options } = this.props; - const { selected } = this.state; + let { selected } = this.state; + while (options[selected]?.separator) { + selected = (selected + 1) % this.props.options.length; + } const selectedItemLabel = options[selected].label ?? options[selected].value; return <>
this.handleClickEvent(e)} + onBlur={() => this.hide(true)} onKeyDown={e => this.handleKeypress(e)} > -
{selectedItemLabel}
-
+
{selectedItemLabel}
+
{ReactDOM.createPortal(this.renderDropdown(), this.dropdownElement)} ; @@ -133,14 +178,14 @@ export class SelectComponent extends React.Component clientHeight - this.state.dimensions.top; const { options } = this.props; const { selected } = this.state; const description = selected !== undefined && options[selected].description; - const markdownDescription = selected !== undefined && options[selected].markdownDescription; - return
this.renderOption(i, item)); + if (description) { + let descriptionNode: React.ReactNode | undefined; + const className = 'theia-select-component-description'; + if (markdown) { + descriptionNode =
; // eslint-disable-line react/no-danger + } else { + descriptionNode =
+ {description} +
; + } + if (invert) { + items.unshift(descriptionNode); + } else { + items.push(descriptionNode); + } + } + return
- { - options.map((item, i) => this.renderOption(i, item)) - } - {markdownDescription && ( -
// eslint-disable-line react/no-danger - )} - {description && !markdownDescription && ( -
- {description} -
- )} + {items}
; } protected renderOption(index: number, option: SelectOption): React.ReactNode { if (option.separator) { - return
; + return
; } const selected = this.state.selected; return (
{ this.setState({ selected: index }); }} - onClick={() => { + onMouseDown={() => { this.selectOption(index, option); }} > -
{option.label ?? option.value}
- {option.detail &&
{option.detail}
} +
{option.label ?? option.value}
+ {option.detail &&
{option.detail}
}
); } protected selectOption(index: number, option: SelectOption): void { - option.onSelected?.(); - this.props.onSelected?.(index, option); + this.props.onChange?.(option, index); this.hide(); } } diff --git a/packages/debug/src/browser/console/debug-console-contribution.tsx b/packages/debug/src/browser/console/debug-console-contribution.tsx index e014aca16fdc9..3cdf8f83324ec 100644 --- a/packages/debug/src/browser/console/debug-console-contribution.tsx +++ b/packages/debug/src/browser/console/debug-console-contribution.tsx @@ -24,6 +24,7 @@ import { Command, CommandRegistry } from '@theia/core/lib/common/command'; import { Severity } from '@theia/core/lib/common/severity'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; +import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; import { DebugSession } from '../debug-session'; import { DebugSessionManager, DidChangeActiveDebugSession } from '../debug-session-manager'; import { DebugConsoleSession, DebugConsoleSessionFactory } from './debug-console-session'; @@ -181,47 +182,43 @@ export class DebugConsoleContribution extends AbstractViewContribution severityElements.push()); - const selectedValue = Severity.toString(this.consoleSessionManager.severity || Severity.Ignore); - - return ; + const severityElements: SelectOption[] = Severity.toArray().map(e => ({ + value: e + })); + + return ; } protected renderDebugConsoleSelector(widget: Widget | undefined): React.ReactNode { - const availableConsoles: React.ReactNode[] = []; + const availableConsoles: SelectOption[] = []; this.consoleSessionManager.all.forEach(e => { if (e instanceof DebugConsoleSession) { - availableConsoles.push(); + availableConsoles.push({ + value: e.id, + label: e.debugSession.label + }); } }); - return ; + + return ; } - protected changeDebugConsole = (event: React.ChangeEvent) => { - const id = event.target.value; + protected changeDebugConsole = (option: SelectOption) => { + const id = option.value!; const session = this.consoleSessionManager.get(id); this.consoleSessionManager.selectedSession = session; }; - protected changeSeverity = (event: React.ChangeEvent) => { - this.consoleSessionManager.severity = Severity.fromValue(event.target.value); + protected changeSeverity = (option: SelectOption) => { + this.consoleSessionManager.severity = Severity.fromValue(option.value); }; protected withWidget(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: ConsoleWidget) => T): T | false { diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index f8b0a5b907a79..36f0bad447178 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -125,9 +125,10 @@ border-bottom: 1px solid var(--theia-sideBarSectionHeader-border); } -.debug-toolbar .debug-configuration { +.debug-toolbar .theia-select-component { width: 100%; min-width: 40px; + margin: 0px 4px; } .debug-toolbar .debug-action { diff --git a/packages/debug/src/browser/view/debug-configuration-widget.tsx b/packages/debug/src/browser/view/debug-configuration-widget.tsx index 7ee7d201ef609..0bda1eb7452f2 100644 --- a/packages/debug/src/browser/view/debug-configuration-widget.tsx +++ b/packages/debug/src/browser/view/debug-configuration-widget.tsx @@ -17,6 +17,7 @@ import * as React from '@theia/core/shared/react'; import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { CommandRegistry, Disposable } from '@theia/core/lib/common'; +import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; import URI from '@theia/core/lib/common/uri'; import { ReactWidget } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; @@ -78,13 +79,10 @@ export class DebugConfigurationWidget extends ReactWidget { render(): React.ReactNode { const { options } = this; + const currentIndex = options.findIndex(e => e.value === this.currentValue); return - + this.setCurrentConfiguration(option)} /> @@ -94,10 +92,25 @@ export class DebugConfigurationWidget extends ReactWidget { const { current } = this.manager; return current ? this.toValue(current) : '__NO_CONF__'; } - protected get options(): React.ReactNode[] { - return Array.from(this.manager.all).map((options, index) => - - ); + protected get options(): SelectOption[] { + const items: SelectOption[] = Array.from(this.manager.all).map(option => ({ + value: this.toValue(option), + label: this.toName(option) + })); + if (items.length === 0) { + items.push({ + value: '__NO_CONF__', + label: nls.localizeByDefault('No Configurations') + }); + } + items.push({ + separator: true + }); + items.push({ + value: '__ADD_CONF__', + label: nls.localizeByDefault('Add Configuration...') + }); + return items; } protected toValue({ configuration, workspaceFolderUri }: DebugSessionOptions): string { if (!workspaceFolderUri) { @@ -112,8 +125,8 @@ export class DebugConfigurationWidget extends ReactWidget { return configuration.name + ' (' + new URI(workspaceFolderUri).path.base + ')'; } - protected readonly setCurrentConfiguration = (event: React.ChangeEvent) => { - const value = event.currentTarget.value; + protected readonly setCurrentConfiguration = (option: SelectOption) => { + const value = option.value!; if (value === '__ADD_CONF__') { this.manager.addConfiguration(); } else { diff --git a/packages/output/src/browser/output-toolbar-contribution.tsx b/packages/output/src/browser/output-toolbar-contribution.tsx index b5b06cf3ba445..cdd30a30f5a5a 100644 --- a/packages/output/src/browser/output-toolbar-contribution.tsx +++ b/packages/output/src/browser/output-toolbar-contribution.tsx @@ -18,6 +18,7 @@ import * as React from '@theia/core/shared/react'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; import { OutputWidget } from './output-widget'; import { OutputCommands } from './output-commands'; import { OutputContribution } from './output-contribution'; @@ -84,27 +85,29 @@ export class OutputToolbarContribution implements TabBarToolbarContribution { protected readonly NONE = ''; protected renderChannelSelector(): React.ReactNode { - const channelOptionElements: React.ReactNode[] = []; - this.outputChannelManager.getVisibleChannels().forEach(channel => { - channelOptionElements.push(); + const channelOptionElements: SelectOption[] = []; + let selected = 0; + this.outputChannelManager.getVisibleChannels().forEach((channel, i) => { + channelOptionElements.push({ + value: channel.name + }); + if (channel === this.outputChannelManager.selectedChannel) { + selected = i; + } }); if (channelOptionElements.length === 0) { - channelOptionElements.push(); + channelOptionElements.push({ + value: this.NONE + }); } - return ; + return
+ this.changeChannel(option)} /> +
; } - protected changeChannel = (event: React.ChangeEvent) => { - const channelName = event.target.value; - if (channelName !== this.NONE) { + protected changeChannel = (option: SelectOption) => { + const channelName = option.value; + if (channelName !== this.NONE && channelName) { this.outputChannelManager.getChannel(channelName).show(); } }; diff --git a/packages/output/src/browser/style/output.css b/packages/output/src/browser/style/output.css index 9abad65c07056..f31ae1f84a1c9 100644 --- a/packages/output/src/browser/style/output.css +++ b/packages/output/src/browser/style/output.css @@ -25,3 +25,7 @@ .theia-output .theia-output-warning { color: var(--theia-editorWarning-foreground); } + +#outputChannelList .theia-select-component { + width: 170px; +} diff --git a/packages/preferences/src/browser/style/index.css b/packages/preferences/src/browser/style/index.css index f573cb7b07d34..9afc76766142e 100644 --- a/packages/preferences/src/browser/style/index.css +++ b/packages/preferences/src/browser/style/index.css @@ -434,3 +434,11 @@ .theia-settings-container .preference-modified-scope-wrapper:not(:last-child)::after { content: ', '; } + +/** Select component */ + +.theia-settings-container .theia-select-component { + height: 26px; + width: 100%; + max-width: 320px; +} diff --git a/packages/preferences/src/browser/views/components/preference-select-input.ts b/packages/preferences/src/browser/views/components/preference-select-input.ts index ede22b72260c4..f68dded9c2b26 100644 --- a/packages/preferences/src/browser/views/components/preference-select-input.ts +++ b/packages/preferences/src/browser/views/components/preference-select-input.ts @@ -29,7 +29,7 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer(); - protected get onDidChange(): Event { + protected get onDidSelectedChange(): Event { return this.onDidChangeEmitter.event; } @@ -42,17 +42,20 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer this.handleUserInteraction(index) + markdown }); } return items; @@ -63,7 +66,8 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer this.handleUserInteraction(index) }); this.interactable = interactable; ReactDOM.render(selectComponent, interactable); From 8b680016d5048e55a732ecc62d991257b3739561 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 6 Apr 2022 14:03:28 +0200 Subject: [PATCH 3/4] Address review comments --- packages/core/src/browser/browser.ts | 47 +++++++++---- .../src/browser/style/select-component.css | 6 ++ .../src/browser/widgets/select-component.tsx | 68 ++++++++++++------- .../components/preference-select-input.ts | 15 ++-- 4 files changed, 91 insertions(+), 45 deletions(-) diff --git a/packages/core/src/browser/browser.ts b/packages/core/src/browser/browser.ts index ec80e32a4ca94..3a054f05f5196 100644 --- a/packages/core/src/browser/browser.ts +++ b/packages/core/src/browser/browser.ts @@ -157,8 +157,18 @@ export function preventNavigation(event: WheelEvent): void { event.stopPropagation(); } -export function measureTextWidth(text: string | string[]): number { - const measureElement = getMeasurementElement(); +export type PartialCSSStyle = Omit, + 'visibility' | + 'display' | + 'parentRule' | + 'getPropertyPriority' | + 'getPropertyValue' | + 'item' | + 'removeProperty' | + 'setProperty'>; + +export function measureTextWidth(text: string | string[], style?: PartialCSSStyle): number { + const measureElement = getMeasurementElement(style); text = Array.isArray(text) ? text : [text]; let width = 0; for (const item of text) { @@ -168,30 +178,43 @@ export function measureTextWidth(text: string | string[]): number { return width; } -export function measureTextHeight(text: string | string[], maxWidth?: number): number { - const measureElement = getMeasurementElement(); - if (maxWidth !== undefined) { - measureElement.style.maxWidth = `${Math.ceil(maxWidth)}px`; - } +export function measureTextHeight(text: string | string[], style?: PartialCSSStyle): number { + const measureElement = getMeasurementElement(style); text = Array.isArray(text) ? text : [text]; let height = 0; for (const item of text) { measureElement.textContent = item; height = Math.max(measureElement.getBoundingClientRect().height, height); } - measureElement.style.maxWidth = 'none'; return height; } -function getMeasurementElement(): HTMLElement { +const defaultStyle = document.createElement('div').style; +defaultStyle.fontFamily = 'var(--theia-ui-font-family)'; +defaultStyle.fontSize = 'var(--theia-ui-font-size1)'; +defaultStyle.visibility = 'hidden'; + +function getMeasurementElement(style?: PartialCSSStyle): HTMLElement { let measureElement = document.getElementById('measure'); if (!measureElement) { measureElement = document.createElement('span'); measureElement.id = 'measure'; - measureElement.style.fontFamily = 'var(--theia-ui-font-family)'; - measureElement.style.fontSize = 'var(--theia-ui-font-size1)'; - measureElement.style.visibility = 'hidden'; + measureElement.style.fontFamily = defaultStyle.fontFamily; + measureElement.style.fontSize = defaultStyle.fontSize; + measureElement.style.visibility = defaultStyle.visibility; document.body.appendChild(measureElement); } + const measureStyle = measureElement.style; + // Reset styling first + for (let i = 0; i < measureStyle.length; i++) { + const property = measureStyle[i]; + measureStyle.setProperty(property, defaultStyle.getPropertyValue(property)); + } + // Apply new styling + if (style) { + for (const [key, value] of Object.entries(style)) { + measureStyle.setProperty(key, value as string); + } + } return measureElement; } diff --git a/packages/core/src/browser/style/select-component.css b/packages/core/src/browser/style/select-component.css index 394a577a16cce..aa6b33baf2380 100644 --- a/packages/core/src/browser/style/select-component.css +++ b/packages/core/src/browser/style/select-component.css @@ -49,6 +49,8 @@ } .theia-select-component-dropdown .theia-select-component-option { + text-overflow: ellipsis; + overflow: hidden; display: flex; padding: 2px 5px; } @@ -71,6 +73,10 @@ width: 100%; } +.theia-select-component-dropdown .theia-select-component-option .theia-select-component-option-detail { + padding-left: 4px; +} + .theia-select-component-dropdown .theia-select-component-option:not(.selected) .theia-select-component-option-detail { color: var(--theia-textLink-foreground); } diff --git a/packages/core/src/browser/widgets/select-component.tsx b/packages/core/src/browser/widgets/select-component.tsx index 0bd9879f6139d..b9b425b748d51 100644 --- a/packages/core/src/browser/widgets/select-component.tsx +++ b/packages/core/src/browser/widgets/select-component.tsx @@ -37,7 +37,7 @@ export interface SelectComponentProps { options: SelectOption[] selected: number onChange?: (option: SelectOption, index: number) => void - onDidSelectedChange?: TheiaEvent + onDidChangeSelected?: TheiaEvent } export interface SelectComponentDropdownDimensions { @@ -51,6 +51,7 @@ export interface SelectComponentState { dimensions?: SelectComponentDropdownDimensions selected: number original: number + hover: number } export const SELECT_COMPONENT_CONTAINER = 'select-component-container'; @@ -67,7 +68,8 @@ export class SelectComponent extends React.Component e.description || ''), maxWidth) + 18; + const descriptionHeight = measureTextHeight(this.props.options.map(e => e.description || ''), { maxWidth: `${maxWidth}px` }) + 18; const singleLineHeight = measureTextHeight(firstLine.label || firstLine.value || firstLine.detail || '') + 6; const optimal = descriptionHeight + singleLineHeight * this.props.options.length; return optimal + 20; // Just to be safe, add another 20 pixels here @@ -100,7 +102,7 @@ export class SelectComponent extends React.Component { - this.hide(true); + this.hide(); }; this.mountedListeners.set('scroll', hide); this.mountedListeners.set('wheel', hide); @@ -148,7 +150,7 @@ export class SelectComponent extends React.Component this.handleClickEvent(e)} - onBlur={() => this.hide(true)} + onBlur={() => this.hide()} onKeyDown={e => this.handleKeypress(e)} >
{selectedItemLabel}
@@ -170,13 +172,23 @@ export class SelectComponent extends React.Component clientHeight - this.state.dimensions.top; + const clientRect = document.getElementById('theia-app-shell')!.getBoundingClientRect(); + const invert = this.optimalHeight > clientRect.height - this.state.dimensions.top; const { options } = this.props; - const { selected } = this.state; - const description = selected !== undefined && options[selected].description; - const markdown = selected !== undefined && options[selected].markdown; + const { hover } = this.state; + const description = options[hover].description; + const markdown = options[hover].markdown; const items = options.map((item, i) => this.renderOption(i, item)); if (description) { let descriptionNode: React.ReactNode | undefined; @@ -259,11 +273,13 @@ export class SelectComponent extends React.Component {items} @@ -274,14 +290,14 @@ export class SelectComponent extends React.Component; } - const selected = this.state.selected; + const selected = this.state.hover; return (
{ this.setState({ - selected: index + hover: index }); }} onMouseDown={() => { @@ -296,6 +312,6 @@ export class SelectComponent extends React.Component { - protected readonly onDidChangeEmitter = new Emitter(); + protected readonly onDidChangeSelectedEmitter = new Emitter(); - protected get onDidSelectedChange(): Event { - return this.onDidChangeEmitter.event; + protected get onDidChangeSelected(): Event { + return this.onDidChangeSelectedEmitter.event; } protected get enumValues(): JSONValue[] { @@ -43,7 +44,7 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer this.handleUserInteraction(index) }); this.interactable = interactable; @@ -83,7 +84,7 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer value === currentValue); + const selected = this.enumValues.findIndex(value => PreferenceProvider.deepEqual(value, currentValue)); return selected > -1 ? selected : 0; } From a3a6edfdf8c8ec77ec6709f8b35b17c8af8b427a Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Thu, 7 Apr 2022 20:33:04 +0000 Subject: [PATCH 4/4] Replace some more select html elements --- .../src/browser/style/select-component.css | 4 +- .../src/browser/widgets/select-component.tsx | 39 +++++++++++++++---- .../console/debug-console-contribution.tsx | 4 +- .../editor/debug-breakpoint-widget.tsx | 31 +++++++-------- .../view/debug-configuration-widget.tsx | 3 +- .../browser/output-toolbar-contribution.tsx | 6 +-- .../components/preference-select-input.ts | 25 +++++------- 7 files changed, 62 insertions(+), 50 deletions(-) diff --git a/packages/core/src/browser/style/select-component.css b/packages/core/src/browser/style/select-component.css index aa6b33baf2380..65b5272046d43 100644 --- a/packages/core/src/browser/style/select-component.css +++ b/packages/core/src/browser/style/select-component.css @@ -90,7 +90,7 @@ .theia-select-component-dropdown .theia-select-component-separator { width: 84px; - height: 2px; - margin: 5px 3px; + height: 1px; + margin: 3px 3px; background: var(--theia-foreground); } diff --git a/packages/core/src/browser/widgets/select-component.tsx b/packages/core/src/browser/widgets/select-component.tsx index b9b425b748d51..14fa649af6c3d 100644 --- a/packages/core/src/browser/widgets/select-component.tsx +++ b/packages/core/src/browser/widgets/select-component.tsx @@ -17,7 +17,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as DOMPurify from 'dompurify'; -import { Event as TheiaEvent } from '../../common/event'; import { codicon } from './widget'; import { measureTextHeight, measureTextWidth } from '../browser'; @@ -35,9 +34,8 @@ export interface SelectOption { export interface SelectComponentProps { options: SelectOption[] - selected: number + value?: string | number onChange?: (option: SelectOption, index: number) => void - onDidChangeSelected?: TheiaEvent } export interface SelectComponentDropdownDimensions { @@ -65,7 +63,12 @@ export class SelectComponent extends React.Component e.value === props.value), 0); + } this.state = { selected, original: selected, @@ -81,18 +84,38 @@ export class SelectComponent extends React.Component e.value === value); + } + if (index >= 0) { + this.setState({ + selected: index, + original: index, + hover: index + }); + } + } + + protected getOptimalWidth(): number { const textWidth = measureTextWidth(this.props.options.map(e => e.label || e.value || '' + (e.detail || ''))); return Math.ceil(textWidth + 16); } - getOptimalHeight(maxWidth?: number): number { + protected getOptimalHeight(maxWidth?: number): number { const firstLine = this.props.options.find(e => e.label || e.value || e.detail); if (!firstLine) { return 0; } if (maxWidth) { - maxWidth = Math.ceil(maxWidth) + 10; // Decrease width by 10 due to side padding + maxWidth = Math.ceil(maxWidth) + 10; // Increase width by 10 due to side padding } const descriptionHeight = measureTextHeight(this.props.options.map(e => e.description || ''), { maxWidth: `${maxWidth}px` }) + 18; const singleLineHeight = measureTextHeight(firstLine.label || firstLine.value || firstLine.detail || '') + 6; @@ -100,7 +123,7 @@ export class SelectComponent extends React.Component { this.hide(); }; diff --git a/packages/debug/src/browser/console/debug-console-contribution.tsx b/packages/debug/src/browser/console/debug-console-contribution.tsx index 3cdf8f83324ec..c29f0c37e7328 100644 --- a/packages/debug/src/browser/console/debug-console-contribution.tsx +++ b/packages/debug/src/browser/console/debug-console-contribution.tsx @@ -189,7 +189,7 @@ export class DebugConsoleContribution extends AbstractViewContribution; } @@ -207,7 +207,7 @@ export class DebugConsoleContribution extends AbstractViewContribution; } diff --git a/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx index cbef00e476919..580f058384afd 100644 --- a/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx +++ b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx @@ -18,7 +18,7 @@ import * as React from '@theia/core/shared/react'; import * as ReactDOM from '@theia/core/shared/react-dom'; import { DebugProtocol } from 'vscode-debugprotocol'; import { injectable, postConstruct, inject } from '@theia/core/shared/inversify'; -import { Disposable, DisposableCollection } from '@theia/core'; +import { Disposable, DisposableCollection, nls } from '@theia/core'; import URI from '@theia/core/lib/common/uri'; import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget'; @@ -35,6 +35,7 @@ import { CompletionItemKind, CompletionContext } from '@theia/monaco-editor-core import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures'; import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; import { TextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel'; +import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; export type ShowDebugBreakpointOptions = DebugSourceBreakpoint | { position: monaco.Position, @@ -212,23 +213,21 @@ export class DebugBreakpointWidget implements Disposable { if (this._input) { this._input.getControl().setValue(this._values[this.context] || ''); } - ReactDOM.render(, this.selectNode); + ReactDOM.render(, this.selectNode); } - protected renderOption(context: DebugBreakpointWidget.Context, label: string): JSX.Element { - return ; - } - protected readonly updateInput = (e: React.ChangeEvent) => { + protected readonly updateInput = (option: SelectOption) => { if (this._input) { this._values[this.context] = this._input.getControl().getValue(); } - this.context = e.currentTarget.value as DebugBreakpointWidget.Context; + this.context = option.value as DebugBreakpointWidget.Context; this.render(); if (this._input) { this._input.focus(); @@ -259,12 +258,12 @@ export class DebugBreakpointWidget implements Disposable { } protected get placeholder(): string { if (this.context === 'logMessage') { - return "Message to log when breakpoint is hit. Expressions within {} are interpolated. 'Enter' to accept, 'esc' to cancel."; + return nls.localizeByDefault("Message to log when breakpoint is hit. Expressions within {} are interpolated. 'Enter' to accept, 'esc' to cancel."); } if (this.context === 'hitCondition') { - return "Break when hit count condition is met. 'Enter' to accept, 'esc' to cancel."; + return nls.localizeByDefault("Break when hit count condition is met. 'Enter' to accept, 'esc' to cancel."); } - return "Break when expression evaluates to true. 'Enter' to accept, 'esc' to cancel."; + return nls.localizeByDefault("Break when expression evaluates to true. 'Enter' to accept, 'esc' to cancel."); } } diff --git a/packages/debug/src/browser/view/debug-configuration-widget.tsx b/packages/debug/src/browser/view/debug-configuration-widget.tsx index 0bda1eb7452f2..ed13be82c4d5e 100644 --- a/packages/debug/src/browser/view/debug-configuration-widget.tsx +++ b/packages/debug/src/browser/view/debug-configuration-widget.tsx @@ -79,10 +79,9 @@ export class DebugConfigurationWidget extends ReactWidget { render(): React.ReactNode { const { options } = this; - const currentIndex = options.findIndex(e => e.value === this.currentValue); return - this.setCurrentConfiguration(option)} /> + this.setCurrentConfiguration(option)} /> diff --git a/packages/output/src/browser/output-toolbar-contribution.tsx b/packages/output/src/browser/output-toolbar-contribution.tsx index cdd30a30f5a5a..92810ca181486 100644 --- a/packages/output/src/browser/output-toolbar-contribution.tsx +++ b/packages/output/src/browser/output-toolbar-contribution.tsx @@ -86,14 +86,10 @@ export class OutputToolbarContribution implements TabBarToolbarContribution { protected renderChannelSelector(): React.ReactNode { const channelOptionElements: SelectOption[] = []; - let selected = 0; this.outputChannelManager.getVisibleChannels().forEach((channel, i) => { channelOptionElements.push({ value: channel.name }); - if (channel === this.outputChannelManager.selectedChannel) { - selected = i; - } }); if (channelOptionElements.length === 0) { channelOptionElements.push({ @@ -101,7 +97,7 @@ export class OutputToolbarContribution implements TabBarToolbarContribution { }); } return
- this.changeChannel(option)} /> + this.changeChannel(option)} />
; } diff --git a/packages/preferences/src/browser/views/components/preference-select-input.ts b/packages/preferences/src/browser/views/components/preference-select-input.ts index d25bb82da05fc..06b77f3889ba3 100644 --- a/packages/preferences/src/browser/views/components/preference-select-input.ts +++ b/packages/preferences/src/browser/views/components/preference-select-input.ts @@ -17,7 +17,6 @@ import { PreferenceLeafNodeRenderer, PreferenceNodeRenderer } from './preference-node-renderer'; import { injectable, interfaces } from '@theia/core/shared/inversify'; import { JSONValue } from '@theia/core/shared/@phosphor/coreutils'; -import { Event, Emitter } from '@theia/core/lib/common/event'; import { PreferenceProvider } from '@theia/core/lib/browser/preferences/preference-provider'; import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; import { Preference } from '../../util/preference-types'; @@ -28,23 +27,19 @@ import * as ReactDOM from '@theia/core/shared/react-dom'; @injectable() export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer { - protected readonly onDidChangeSelectedEmitter = new Emitter(); - - protected get onDidChangeSelected(): Event { - return this.onDidChangeSelectedEmitter.event; - } + protected readonly selectComponent = React.createRef(); protected get enumValues(): JSONValue[] { return this.preferenceNode.preference.data.enum!; } - protected get selectComponentOptions(): SelectOption[] { + protected get selectOptions(): SelectOption[] { const items: SelectOption[] = []; const values = this.enumValues; const defaultValue = this.preferenceNode.preference.data.default; for (let i = 0; i < values.length; i++) { const value = `${values[i]}`; - const detail = PreferenceProvider.deepEqual(defaultValue, value) ? ' default' : undefined; + const detail = PreferenceProvider.deepEqual(defaultValue, value) ? 'default' : undefined; let enumDescription = this.preferenceNode.preference.data.enumDescriptions?.[i]; let markdown = false; const markdownEnumDescription = this.preferenceNode.preference.data.markdownEnumDescriptions?.[i]; @@ -65,10 +60,10 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer this.handleUserInteraction(index) + options: this.selectOptions, + value: this.getDataValue(), + onChange: (_, index) => this.handleUserInteraction(index), + ref: this.selectComponent }); this.interactable = interactable; ReactDOM.render(selectComponent, interactable); @@ -83,8 +78,8 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer PreferenceProvider.deepEqual(value, currentValue)); - return selected > -1 ? selected : 0; + return Math.max(selected, 0); } protected handleUserInteraction(selected: number): void {