Skip to content

Commit

Permalink
File dialog enhancements
Browse files Browse the repository at this point in the history
- Add text input to locationList Renderer
- Add 'navigate upward' icon
- Fix icon focus behavior when disabled

Signed-off-by: Kenneth Marut <kenneth.marut@ericsson.com>
  • Loading branch information
kenneth-marut-work committed Nov 13, 2020
1 parent 476c8df commit fbc77c3
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [filesystem] refactored file watchers: [#8546](https://github.com/eclipse-theia/theia/pull/8546)
- Added `FileSystemWatcherService` component that should be a singleton centralizing watch requests for all clients.
- Added `FileSystemWatcherServiceDispatcher` to register yourself and listen to file change events.
- [filesystem] add text input and nagivate up icon to file dialog [#8748](https://github.com/eclipse-theia/theia/pull/8748)
- [git] updated `commit details` and `diff view` rendering to respect `list` and `tree` modes [#8084] (https://github.com/eclipse-theia/theia/pull/8084)
- [markers] updated and enhanced the 'problem-manager' tests [#8604](https://github.com/eclipse-theia/theia/pull/8604)
- [mini-browser] updated deprecated `scrElement` usage to `target` [#8663](https://github.com/eclipse-theia/theia/pull/8663)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,12 @@ export class FileDialogModel extends FileTreeModel {
private isFileStatNodeSelectable(node: FileStatNode): boolean {
return !(!node.fileStat.isDirectory && this._disableFileSelection);
}

canNavigateUpward(): boolean {
const treeRoot = this.tree.root;
if (FileStatNode.is(treeRoot)) {
return !treeRoot.uri.path.isRoot;
}
return false;
}
}
25 changes: 25 additions & 0 deletions packages/filesystem/src/browser/file-dialog/file-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const NAVIGATION_PANEL_CLASS = 'theia-NavigationPanel';
export const NAVIGATION_BACK_CLASS = 'theia-NavigationBack';
export const NAVIGATION_FORWARD_CLASS = 'theia-NavigationForward';
export const NAVIGATION_HOME_CLASS = 'theia-NavigationHome';
export const NAVIGATION_UP_CLASS = 'theia-NavigationUp';
export const NAVIGATION_LOCATION_LIST_PANEL_CLASS = 'theia-LocationListPanel';

export const FILTERS_PANEL_CLASS = 'theia-FiltersPanel';
Expand Down Expand Up @@ -116,6 +117,7 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
protected readonly back: HTMLSpanElement;
protected readonly forward: HTMLSpanElement;
protected readonly home: HTMLSpanElement;
protected readonly up: HTMLSpanElement;
protected readonly locationListRenderer: LocationListRenderer;
protected readonly treeFiltersRenderer: FileDialogTreeFiltersRenderer | undefined;
protected readonly treePanel: Panel;
Expand Down Expand Up @@ -145,6 +147,9 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
navigationPanel.appendChild(this.home = createIconButton('fa', 'fa-home'));
this.home.classList.add(NAVIGATION_HOME_CLASS);
this.home.title = 'Go To Initial Location';
navigationPanel.appendChild(this.up = createIconButton('fa', 'fa-level-up'));
this.up.classList.add(NAVIGATION_UP_CLASS);
this.up.title = 'Navigate Up One Directory';

this.locationListRenderer = this.createLocationListRenderer();
this.locationListRenderer.host.classList.add(NAVIGATION_LOCATION_LIST_PANEL_CLASS);
Expand Down Expand Up @@ -176,6 +181,7 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
setEnabled(this.home, !!this.model.initialLocation
&& !!this.model.location
&& this.model.initialLocation.toString() !== this.model.location.toString());
setEnabled(this.up, this.model.canNavigateUpward());
this.locationListRenderer.render();

if (this.treeFiltersRenderer) {
Expand All @@ -185,6 +191,20 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
this.widget.update();
}

protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLInputElement) {
return false;
}
this.accept();
}

protected handleEscape(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLInputElement) {
return false;
}
this.close();
}

protected appendFiltersPanel(): void {
if (this.treeFiltersRenderer) {
const filtersPanel = document.createElement('div');
Expand Down Expand Up @@ -223,6 +243,11 @@ export abstract class FileDialog<T> extends AbstractDialog<T> {
this.model.location = this.model.initialLocation;
}
}, 'click');
this.addKeyListener(this.up, Key.ENTER, () => {
if (this.model.location) {
this.model.location = this.model.location.parent;
}
}, 'click');
super.onAfterAttach(msg);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/filesystem/src/browser/file-tree/file-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class FileTreeModel extends TreeModelImpl implements LocationService {
const node = DirNode.createRoot(fileStat);
this.navigateTo(node);
}
}).catch(() => {
// no-op, allow failures for file dialog text input
});
} else {
this.navigateTo(undefined);
Expand Down
87 changes: 80 additions & 7 deletions packages/filesystem/src/browser/location/location-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import URI from '@theia/core/lib/common/uri';
import { LocationService } from './location-service';
import { ReactRenderer } from '@theia/core/lib/browser/widgets/react-renderer';
import * as React from 'react';

import ReactDOM = require('react-dom');
export class LocationListRenderer extends ReactRenderer {

protected _drives: URI[] | undefined;

protected doShowTextInput: boolean = false;
protected lastUniqueTextInputLocation = '';
constructor(
protected readonly service: LocationService,
host?: HTMLElement
Expand All @@ -31,21 +32,61 @@ export class LocationListRenderer extends ReactRenderer {
this.doLoadDrives();
}

render(): void {
super.render();
protected doAfterRender = (): void => {
const locationList = this.locationList;
const locationListTextInput = this.locationTextInput;
if (locationList) {
const currentLocation = this.service.location;
locationList.value = currentLocation ? currentLocation.toString() : '';
} else if (locationListTextInput) {
locationListTextInput.focus();
}
};

render(): void {
ReactDOM.render(<React.Fragment>{this.doRender()}</React.Fragment>, this.host, this.doAfterRender);
}

protected readonly handleLocationChanged = (e: React.ChangeEvent<HTMLSelectElement>) => this.onLocationChanged(e);
protected readonly handleTextInputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => this.onTextInputChanged(e);
protected readonly handleTextInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => this.onTextInputKeyDown(e);
protected readonly handleTextInputToggleClick = (e: React.MouseEvent<HTMLSpanElement>) => this.onTextInputToggle();
protected readonly handleTextInputOnBlur = (e: React.FocusEvent<HTMLInputElement>) => this.onTextInputToggle();
protected doRender(): React.ReactNode {
const options = this.collectLocations().map(value => this.renderLocation(value));
return <select className={'theia-select ' + LocationListRenderer.Styles.LOCATION_LIST_CLASS} onChange={this.handleLocationChanged}>{...options}</select>;
return (
<>
{ this.doShowTextInput ?
<input className={'theia-select ' + LocationListRenderer.Styles.LOCATION_TEXT_INPUT_CLASS} type='text'
defaultValue={this.service.location?.path.toString()}
onChange={this.handleTextInputOnChange}
onKeyDown={this.handleTextInputKeyDown}
spellCheck={false}
onBlur={this.handleTextInputOnBlur}
/>
: <>
<span onClick={this.handleTextInputToggleClick}
className={LocationListRenderer.Styles.LOCATION_INPUT_TOGGLE_CLASS}
tabIndex={0}
id={LocationListRenderer.Styles.LOCATION_INPUT_TOGGLE_CLASS}
>
<i className='fa fa-edit' />
</span>
<select className={'theia-select ' + LocationListRenderer.Styles.LOCATION_LIST_CLASS}
onChange={this.handleLocationChanged}>
{...options}
</select>
</>
}
</>
);
}

protected onTextInputToggle(): void {
this.doShowTextInput = !this.doShowTextInput;
this.render();
};

/**
* Collects the available locations based on the currently selected, and appends the available drives to it.
*/
Expand Down Expand Up @@ -103,11 +144,33 @@ export class LocationListRenderer extends ReactRenderer {
const locationList = this.locationList;
if (locationList) {
const value = locationList.value;
this.lastUniqueTextInputLocation = value;
const uri = new URI(value);
this.service.location = uri;
e.preventDefault();
e.stopPropagation();
}
}

protected onTextInputChanged(e: React.ChangeEvent<HTMLInputElement>): void {
const locationTextInput = this.locationTextInput;
if (locationTextInput) {
// discount all paths that end in trailing slashes or periods to prevent duplicate paths from
// being added to location history, and to prevent tree root to be rendered as '' or '.'
const sanitizedInput = locationTextInput.value.trim().replace(/[\/\\.]*$/, '');
if (sanitizedInput !== this.lastUniqueTextInputLocation) {
this.lastUniqueTextInputLocation = sanitizedInput;
const uri = new URI(sanitizedInput);
this.service.location = uri;
}
e.stopPropagation();
}
}

protected onTextInputKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void {
if (e.key === 'Enter' || e.key === 'Escape') {
this.onTextInputToggle();
}
e.preventDefault();
e.stopPropagation();
}

get locationList(): HTMLSelectElement | undefined {
Expand All @@ -118,12 +181,22 @@ export class LocationListRenderer extends ReactRenderer {
return undefined;
}

get locationTextInput(): HTMLInputElement | undefined {
const locationTextInput = this.host.getElementsByClassName(LocationListRenderer.Styles.LOCATION_TEXT_INPUT_CLASS)[0];
if (locationTextInput instanceof HTMLInputElement) {
return locationTextInput;
}
return undefined;
}

}

export namespace LocationListRenderer {

export namespace Styles {
export const LOCATION_LIST_CLASS = 'theia-LocationList';
export const LOCATION_INPUT_TOGGLE_CLASS = 'theia-LocationInputToggle';
export const LOCATION_TEXT_INPUT_CLASS = 'theia-LocationTextInput';
}

export interface Location {
Expand Down
64 changes: 59 additions & 5 deletions packages/filesystem/src/browser/style/file-dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@

.dialogContent .theia-NavigationBack,
.dialogContent .theia-NavigationForward,
.dialogContent .theia-NavigationHome {
.dialogContent .theia-NavigationHome,
.dialogContent .theia-NavigationUp,
.dialogContent .theia-LocationInputToggle
{
position: absolute;
top: 0px;
line-height: 23px;
Expand All @@ -69,12 +72,24 @@

.dialogContent .theia-NavigationBack:focus,
.dialogContent .theia-NavigationForward:focus,
.dialogContent .theia-NavigationHome:focus {
.dialogContent .theia-NavigationHome:focus,
.dialogContent .theia-NavigationUp:focus,
.dialogContent .theia-LocationInputToggle:focus
{
outline: none;
border: none;
box-shadow: none;
}

.dialogContent .theia-NavigationBack.theia-mod-disabled:focus,
.dialogContent .theia-NavigationForward.theia-mod-disabled:focus,
.dialogContent .theia-NavigationHome.theia-mod-disabled:focus,
.dialogContent .theia-NavigationUp.theia-mod-disabled:focus,
.dialogContent .theia-LocationInputToggle.theia-mod-disabled:focus
{
opacity: var(--theia-mod-disabled-opacity) !important;
}

.dialogContent .theia-NavigationBack {
left: auto;
}
Expand All @@ -87,17 +102,56 @@
left: 41px;
}

.dialogContent .theia-NavigationUp {
left: 61px;
}

.dialogContent .theia-LocationListPanel {
position: absolute;
left: 72px;
display: flex;
left: 82px;
top: 1px;
width: 417px;
height: 21px;
}

.dialogContent .theia-LocationList {
width: 427px;
.dialogContent .theia-LocationInputToggle {
text-align: center;
left: 0;
width: 21px;
height: 21px;
z-index: 1;
}

.dialogContent .theia-LocationInputToggle > i {
width: 12px;
}

.dialogContent .theia-LocationList,
.dialogContent .theia-LocationTextInput
{
box-sizing: content-box;
padding: unset;
position: absolute;
top: 0;
left: 0;
height: 21px;
border: var(--theia-border-width) solid var(--theia-input-border);
}

.dialogContent .theia-LocationList
{
padding-left: 21px;
width: calc(100% - 21px);
}

.dialogContent .theia-LocationTextInput
{
width: calc(100% - var(--theia-ui-padding));
padding-left: var(--theia-ui-padding);
}


/*
* Filters panel items
*/
Expand Down

0 comments on commit fbc77c3

Please sign in to comment.