From 6527a512201e9eddaffbbb3ee0d98cb11d6f6322 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 16 Sep 2020 12:23:41 -0500 Subject: [PATCH] Revert "Merge branch 'roblou/revertSettingsEditorChanges'" This reverts commit 3c448ca5b338ab9ce85949e5435a202e7b1c0185, reversing changes made to 8ce1c41cb910b2dedaa525f04a1c41684e0fb235. --- .../browser/media/settingsEditor2.css | 49 ++++--- .../browser/preferences.contribution.ts | 101 +++++++++++++-- .../preferences/browser/settingsEditor2.ts | 97 ++++++-------- .../preferences/browser/settingsTree.ts | 122 ++++++++++++++---- .../preferences/browser/settingsWidgets.ts | 31 ++++- .../contrib/preferences/browser/tocTree.ts | 33 ++++- 6 files changed, 314 insertions(+), 119 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 02f464009af..8b5017293dc 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -175,14 +175,14 @@ .settings-editor > .settings-body .settings-tree-container .setting-toolbar-container { position: absolute; - left: -32px; + left: -22px; top: 11px; bottom: 0px; width: 26px; } .settings-editor > .settings-body .settings-tree-container .monaco-list-row .mouseover .setting-toolbar-container > .monaco-toolbar .codicon, -.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-item-contents.focused .setting-toolbar-container > .monaco-toolbar .codicon, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row.focused .setting-item-contents .setting-toolbar-container > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container:hover > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container > .monaco-toolbar .active .codicon { opacity: 1; @@ -283,15 +283,34 @@ max-width: 1000px; margin: auto; box-sizing: border-box; - padding-left: 219px; - padding-right: 20px; + padding-left: 204px; + padding-right: 5px; overflow: visible; } +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { + content: ' '; + position: absolute; + left: 0px; + right: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before { + top: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { + bottom: 0px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item-contents { position: relative; - padding-top: 12px; - padding-bottom: 18px; + padding: 12px 15px 18px; white-space: normal; } @@ -299,11 +318,9 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - display: inline-block; - /* size to contents for hover to show context button */ + display: inline-block; /* size to contents for hover to show context button */ } - .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-modified-indicator { display: none; } @@ -315,7 +332,7 @@ width: 6px; border-left-width: 2px; border-left-style: solid; - left: -9px; + left: 5px; top: 15px; bottom: 16px; } @@ -528,12 +545,18 @@ } .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { + display: inline-block; margin: 0px; font-weight: 600; + height: 100%; + box-sizing: border-box; + padding: 10px; + padding-left: 15px; + width: 100%; + position: relative; } .settings-editor > .settings-body > .settings-tree-container .settings-group-level-1 { - padding-top: 23px; font-size: 24px; } @@ -542,10 +565,6 @@ font-size: 20px; } -.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { - padding-top: 7px; -} - .settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count { display: block; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 114dd1d556e..d669a204251 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; +import { InputFocusedContext, IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -40,6 +40,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { AbstractSideBySideEditorInputFactory } from 'vs/workbench/browser/parts/editor/editor.contribution'; +import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -50,6 +51,8 @@ const SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING = 'settings.action.editFocuse const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focusSettingsFromSearch'; const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; const SETTINGS_EDITOR_COMMAND_FOCUS_TOC = 'settings.action.focusTOC'; +const SETTINGS_EDITOR_COMMAND_FOCUS_TOC2 = 'settings.action.focusTOC2'; +const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingControl'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; @@ -507,6 +510,14 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } return null; } + + function settingsEditorFocusSearch(accessor: ServicesAccessor) { + const preferencesEditor = getPreferencesEditor(accessor); + if (preferencesEditor) { + preferencesEditor.focusSearch(); + } + } + registerAction2(class extends Action2 { constructor() { super({ @@ -521,12 +532,24 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon }); } - run(accessor: ServicesAccessor) { - const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusSearch(); - } + run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_SEARCH, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS), + keybinding: { + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib, + when: null + }, + title: nls.localize('settings.focusSearch', "Focus settings search") + }); } + + run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } }); registerAction2(class extends Action2 { @@ -691,16 +714,76 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC, - precondition: CONTEXT_SETTINGS_EDITOR, + keybinding: [ + { + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate()), + }, + { + primary: KeyCode.LeftArrow, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), InputFocusedContext.negate()) + }], title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") }); } run(accessor: ServicesAccessor): void { const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor instanceof SettingsEditor2) { - preferencesEditor.focusTOC(); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; } + + if (document.activeElement?.classList.contains('monaco-list')) { + preferencesEditor.focusTOC(); + } else { + preferencesEditor.focusSettings(); + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), WorkbenchListFocusContextKey), + keybinding: { + primary: KeyCode.Enter, + weight: KeybindingWeight.WorkbenchContrib, + }, + title: nls.localize('settings.focusSettingControl', "Focus setting control") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; + } + + if (document.activeElement?.classList.contains('monaco-list')) { + preferencesEditor.focusSettings(true); + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC2, + + title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; + } + + preferencesEditor.focusTOC(); } }); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 17f5a27bb21..f551e44afaa 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -43,7 +43,7 @@ import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/co import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; +import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers, updateSettingTreeTabOrder } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; @@ -149,6 +149,7 @@ export class SettingsEditor2 extends EditorPane { private editorMemento: IEditorMemento; private tocFocusedElement: SettingsTreeGroupElement | null = null; + private treeFocusedElement: SettingsTreeElement | null = null; private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; @@ -349,7 +350,8 @@ export class SettingsEditor2 extends EditorPane { } } - focusSettings(): void { + focusSettings(focusSettingInput = false): void { + // TODO@roblourens is this in the right place? // Update ARIA global labels const labelElement = this.settingsAriaExtraLabelsContainer.querySelector('#settings_aria_more_actions_shortcut_label'); if (labelElement) { @@ -359,9 +361,18 @@ export class SettingsEditor2 extends EditorPane { } } - const firstFocusable = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); - if (firstFocusable) { - (firstFocusable).focus(); + const focused = this.settingsTree.getFocus(); + if (!focused.length) { + this.settingsTree.focusFirst(); + } + + this.settingsTree.domFocus(); + + if (focusSettingInput) { + const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); + if (controlInFocusedRow) { + (controlInFocusedRow).focus(); + } } } @@ -511,6 +522,11 @@ export class SettingsEditor2 extends EditorPane { this.settingsTree.reveal(elements[0], sourceTop); + // We need to shift focus from the setting that contains the link to the setting that's + // linked. Clicking on the link sets focus on the setting that contains the link, + // which is why we need the setTimeout + setTimeout(() => this.settingsTree.setFocus([elements[0]]), 50); + const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); if (domElements && domElements[0]) { const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); @@ -571,48 +587,7 @@ export class SettingsEditor2 extends EditorPane { })); this.createTOC(bodyContainer); - - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.scrollTop > 0) { - const firstElement = this.settingsTree.firstVisibleElement; - - if (typeof firstElement !== 'undefined') { - this.settingsTree.reveal(firstElement, 0.1); - } - - return true; - } - } else { - const firstControl = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); - if (firstControl) { - (firstControl).focus(); - } - } - - return false; - }, - 'settings list focus helper'); - this.createSettingsTree(bodyContainer); - - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.scrollTop < this.settingsTree.scrollHeight) { - const lastElement = this.settingsTree.lastVisibleElement; - this.settingsTree.reveal(lastElement, 0.9); - return true; - } - } - - return false; - }, - 'settings list focus helper' - ); } private addCtrlAInterceptor(container: HTMLElement): void { @@ -630,19 +605,6 @@ export class SettingsEditor2 extends EditorPane { })); } - private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { - const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); - listFocusSink.setAttribute('aria-label', label); - listFocusSink.tabIndex = 0; - this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { - if (e.relatedTarget && callback(e)) { - e.relatedTarget.focus(); - } - })); - - return listFocusSink; - } - private createTOC(parent: HTMLElement): void { this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState); this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container')); @@ -670,6 +632,7 @@ export class SettingsEditor2 extends EditorPane { } } else if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { this.settingsTree.reveal(element, 0); + this.settingsTree.setFocus([element]); } })); @@ -719,7 +682,6 @@ export class SettingsEditor2 extends EditorPane { this.settingsTreeContainer, this.viewState, this.settingRenderers.allRenderers)); - this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); this._register(this.settingsTree.onDidScroll(() => { if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) { @@ -727,6 +689,7 @@ export class SettingsEditor2 extends EditorPane { } this.settingsTreeScrollTop = this.settingsTree.scrollTop; + updateSettingTreeTabOrder(this.settingsTreeContainer); // setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when // setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync @@ -734,6 +697,20 @@ export class SettingsEditor2 extends EditorPane { this.updateTreeScrollSync(); }, 0); })); + + // There is no different select state in the settings tree + this._register(this.settingsTree.onDidChangeFocus(e => { + const element = e.elements[0]; + if (this.treeFocusedElement === element) { + return; + } + + this.treeFocusedElement = element; + this.settingsTree.setSelection(element ? [element] : []); + + // Wait for rendering to complete + setTimeout(() => updateSettingTreeTabOrder(this.settingsTreeContainer), 0); + })); } private notifyNoSaveNeeded() { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index dc5a734b6f6..df568849718 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -5,7 +5,6 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import * as DOM from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; @@ -16,7 +15,7 @@ import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction, Separator } from 'vs/base/common/actions'; @@ -43,7 +42,7 @@ import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticip import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester, focusedRowBackground, focusedRowBorder, settingsHeaderForeground, rowHoverBackground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; @@ -53,6 +52,9 @@ import { Codicon } from 'vs/base/common/codicons'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = DOM.$; @@ -451,6 +453,45 @@ export interface ISettingOverrideClickEvent { targetKey: string; } +function removeChildrenFromTabOrder(node: Element): void { + const focusableElements = node.querySelectorAll(` + [tabindex="0"], + input:not([tabindex="-1"]), + select:not([tabindex="-1"]), + textarea:not([tabindex="-1"]), + a:not([tabindex="-1"]), + button:not([tabindex="-1"]), + area:not([tabindex="-1"]) + `); + + focusableElements.forEach(element => { + element.setAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR, 'true'); + element.setAttribute('tabindex', '-1'); + }); +} + +function addChildrenToTabOrder(node: Element): void { + const focusableElements = node.querySelectorAll( + `[${AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR}="true"]` + ); + + focusableElements.forEach(element => { + element.removeAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR); + element.setAttribute('tabindex', '0'); + }); +} + +export function updateSettingTreeTabOrder(container: Element): void { + const allRows = [...container.querySelectorAll(AbstractSettingRenderer.ALL_ROWS_SELECTOR)]; + const focusedRow = allRows.find(row => row.classList.contains('focused')); + + allRows.forEach(removeChildrenFromTabOrder); + + if (isDefined(focusedRow)) { + addChildrenToTabOrder(focusedRow); + } +} + export abstract class AbstractSettingRenderer extends Disposable implements ITreeRenderer { /** To override */ abstract get templateId(): string; @@ -459,9 +500,11 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre static readonly CONTROL_SELECTOR = '.' + AbstractSettingRenderer.CONTROL_CLASS; static readonly CONTENTS_CLASS = 'setting-item-contents'; static readonly CONTENTS_SELECTOR = '.' + AbstractSettingRenderer.CONTENTS_CLASS; + static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; static readonly SETTING_KEY_ATTR = 'data-key'; static readonly SETTING_ID_ATTR = 'data-id'; + static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; private readonly _onDidClickOverrideElement = this._register(new Emitter()); readonly onDidClickOverrideElement: Event = this._onDidClickOverrideElement.event; @@ -607,7 +650,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre private fixToolbarIcon(toolbar: ToolBar): void { const button = toolbar.getElement().querySelector('.codicon-toolbar-more'); if (button) { - (button).tabIndex = -1; + (button).tabIndex = 0; // change icon from ellipsis to gear (button).classList.add('codicon-gear'); @@ -1248,6 +1291,15 @@ export class SettingTextRenderer extends AbstractSettingRenderer implements ITre })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + inputBox.inputElement.tabIndex = 0; + + // TODO@9at8: listWidget filters out all key events from input boxes, so we need to come up with a better way + // Disable ArrowUp and ArrowDown behaviour in favor of list navigation + common.toDispose.add(DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, e => { + if (e.equals(KeyCode.UpArrow) || e.equals(KeyCode.DownArrow)) { + e.preventDefault(); + } + })); const template: ISettingTextItemTemplate = { ...common, @@ -1300,6 +1352,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const selectElement = common.controlElement.querySelector('select'); if (selectElement) { selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + selectElement.tabIndex = 0; } common.toDispose.add( @@ -1392,6 +1445,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + inputBox.inputElement.tabIndex = 0; const template: ISettingNumberItemTemplate = { ...common, @@ -1504,13 +1558,6 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre // Prevent clicks from being handled by list toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); - - toDispose.add(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { - if (e.keyCode === KeyCode.Escape) { - e.browserEvent.stopPropagation(); - } - })); - toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); @@ -1834,11 +1881,7 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate extends ObjectTreeModel { } } -export class SettingsTree extends ObjectTree { +export class SettingsTree extends WorkbenchObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, renderers: ITreeRenderer[], + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, @IInstantiationService instantiationService: IInstantiationService, ) { super('SettingsTree', container, new SettingsTreeDelegate(), renderers, { + horizontalScrolling: false, supportDynamicHeights: true, identityProvider: { getId(e) { @@ -1875,9 +1923,6 @@ export class SettingsTree extends ObjectTree { } }, accessibilityProvider: { - getWidgetRole() { - return 'form'; - }, getAriaLabel() { // TODO@roblourens https://github.com/microsoft/vscode/issues/95862 return ''; @@ -1889,9 +1934,16 @@ export class SettingsTree extends ObjectTree { styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), filter: instantiationService.createInstance(SettingsTreeFilter, viewState), smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - }); + multipleSelectionSupport: false, + }, + contextKeyService, + listService, + themeService, + configurationService, + keybindingService, + accessibilityService, + ); - this.disposables.clear(); this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { @@ -1930,6 +1982,26 @@ export class SettingsTree extends ObjectTree { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); } + const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); + if (focusedRowBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .setting-item-contents, + .settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .settings-group-title-label { background-color: ${focusedRowBackgroundColor}; }`); + } + + const rowHoverBackgroundColor = theme.getColor(rowHoverBackground); + if (rowHoverBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row .setting-item-contents:hover, + .settings-editor > .settings-body > .settings-tree-container .monaco-list-row .settings-group-title-label:hover { background-color: ${rowHoverBackgroundColor}; }`); + } + + const focusedRowBorderColor = theme.getColor(focusedRowBorder); + if (focusedRowBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, + .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, + .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); + } + const headerForegroundColor = theme.getColor(settingsHeaderForeground); if (headerForegroundColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); @@ -1940,6 +2012,12 @@ export class SettingsTree extends ObjectTree { if (focusBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); } + + // const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground); + // if (listActiveSelectionBackgroundColor) { + // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .setting-item-contents .setting-item-title { background-color: ${listActiveSelectionBackgroundColor}; }`); + // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .settings-group-title-label { background-color: ${listActiveSelectionBackgroundColor}; }`); + // } })); this.getHTMLElement().classList.add('settings-editor-tree'); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index d7f85b56922..5eaab66e56c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -16,7 +16,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import 'vs/css!./media/settingsWidgets'; import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder } from 'vs/platform/theme/common/colorRegistry'; +import { foreground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder, listFocusBackground, transparent, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { disposableTimeout } from 'vs/base/common/async'; @@ -25,6 +25,7 @@ import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/pr import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { isIOS } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; const $ = DOM.$; export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title.")); @@ -46,15 +47,33 @@ export const settingsCheckboxForeground = registerColor('settings.checkboxForegr export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: simpleCheckboxBorder, light: simpleCheckboxBorder, hc: simpleCheckboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); // Text control colors -export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputBackground = settingsSelectBackground; //registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); // Number control colors -export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputBackground = settingsSelectBackground; // registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); +export const focusedRowBackground = registerColor('settings.focusedRowBackground', { + dark: transparent(PANEL_BORDER, .4), + light: transparent(listFocusBackground, .4), + hc: null +}, localize('focusedRowBackground', "The background color of a cell when the row is focused.")); + +export const rowHoverBackground = registerColor('notebook.rowHoverBackground', { + dark: transparent(focusedRowBackground, .5), + light: transparent(focusedRowBackground, .7), + hc: null +}, localize('notebook.rowHoverBackground', "The background color of a row when the row is hovered.")); + +export const focusedRowBorder = registerColor('notebook.focusedRowBorder', { + dark: Color.white.transparent(0.12), + light: Color.black.transparent(0.12), + hc: focusBorder +}, localize('notebook.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); + registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); if (checkboxBackgroundColor) { @@ -527,7 +546,7 @@ export class ListSettingWidget extends AbstractListSettingWidget valueInput.element.classList.add('setting-list-valueInput'); this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, { - inputBackground: settingsTextInputBackground, + inputBackground: settingsSelectBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -546,7 +565,7 @@ export class ListSettingWidget extends AbstractListSettingWidget siblingInput.element.classList.add('setting-list-siblingInput'); this.listDisposables.add(siblingInput); this.listDisposables.add(attachInputBoxStyler(siblingInput, this.themeService, { - inputBackground: settingsTextInputBackground, + inputBackground: settingsSelectBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -908,7 +927,7 @@ export class ObjectSettingWidget extends AbstractListSettingWidget { +export class TOCTree extends WorkbenchObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IInstantiationService instantiationService: IInstantiationService + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IInstantiationService instantiationService: IInstantiationService, ) { // test open mode const filter = instantiationService.createInstance(SettingsTreeFilter, viewState); - const options: IObjectTreeOptions = { + const options: IWorkbenchObjectTreeOptions = { filter, multipleSelectionSupport: false, identityProvider: { @@ -207,13 +216,23 @@ export class TOCTree extends ObjectTree { }, styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), - collapseByDefault: true + collapseByDefault: true, + horizontalScrolling: false }; - super('SettingsTOC', container, + super( + 'SettingsTOC', + container, new TOCTreeDelegate(), [new TOCRenderer()], - options); + options, + contextKeyService, + listService, + themeService, + configurationService, + keybindingService, + accessibilityService, + ); this.disposables.add(attachStyler(themeService, { listBackground: editorBackground,