Skip to content

Commit

Permalink
Add breadcrumbs to tabbar
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <colin.grant@ericsson.com>
  • Loading branch information
colin-grant-work committed Aug 27, 2021
1 parent 980aaa0 commit 8ca31d0
Show file tree
Hide file tree
Showing 35 changed files with 691 additions and 513 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change Log

## v1.18.0 - 9/30/2021

- [core, outline-view, file-system, workspace] added breadcrumbs contribution points and renderers to `core` and contributions to other packages. [#9920](https://github.com/eclipse-theia/theia/pull/9920)

<a name="breaking_changes_1.17.0">[Breaking Changes:](#breaking_changes_1.18.0)</a>

- [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920)

## v1.17.0 - 8/26/2021

[1.17.0 Milestone](https://github.com/eclipse-theia/theia/milestone/23)
Expand Down
98 changes: 51 additions & 47 deletions packages/core/src/browser/breadcrumbs/breadcrumb-popup-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,84 +14,88 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable, postConstruct } from '../../../shared/inversify';
import { Emitter, Event } from '../../common';
import { Disposable, DisposableCollection } from '../../common/disposable';
import { Breadcrumbs } from './breadcrumbs';
import { Coordinate } from '../context-menu-renderer';
import { RendererHost } from '../widgets/react-renderer';
import { Styles } from './breadcrumbs-constants';

export interface BreadcrumbPopupContainerFactory {
(parent: HTMLElement, breadcrumbId: string, position: Coordinate): BreadcrumbPopupContainer;
}
export const BreadcrumbPopupContainerFactory = Symbol('BreadcrumbPopupContainerFactory');

export type BreadcrumbID = string;
export const BreadcrumbID = Symbol('BreadcrumbID');

/**
* This class creates a popup container at the given position
* so that contributions can attach their HTML elements
* as childs of `BreadcrumbPopupContainer#container`.
* as children of `BreadcrumbPopupContainer#container`.
*
* - `dispose()` is called on blur or on hit on escape
*/
@injectable()
export class BreadcrumbPopupContainer implements Disposable {
@inject(RendererHost) protected readonly parent: RendererHost;
@inject(BreadcrumbID) public readonly breadcrumbId: BreadcrumbID;
@inject(Coordinate) protected readonly position: Coordinate;

protected toDispose: DisposableCollection = new DisposableCollection();
protected onDidDisposeEmitter = new Emitter<void>();
protected toDispose: DisposableCollection = new DisposableCollection(this.onDidDisposeEmitter);
get onDidDispose(): Event<void> {
return this.onDidDisposeEmitter.event;
}

protected _container: HTMLElement;
get container(): HTMLElement {
return this._container;
}

readonly container: HTMLElement;
public isOpen: boolean;
protected _isOpen: boolean;
get isOpen(): boolean {
return this._isOpen;
}

constructor(
protected readonly parent: HTMLElement,
public readonly breadcrumbId: string,
position: { x: number, y: number }
) {
this.container = this.createPopupDiv(position);
@postConstruct()
protected init(): void {
this._container = this.createPopupDiv(this.position);
document.addEventListener('keyup', this.escFunction);
this.container.focus();
this.isOpen = true;
this._container.focus();
this._isOpen = true;
}

protected createPopupDiv(position: { x: number, y: number }): HTMLDivElement {
protected createPopupDiv(position: Coordinate): HTMLDivElement {
const result = window.document.createElement('div');
result.className = Breadcrumbs.Styles.BREADCRUMB_POPUP;
result.className = Styles.BREADCRUMB_POPUP;
result.style.left = `${position.x}px`;
result.style.top = `${position.y}px`;
result.tabIndex = 0;
result.onblur = event => this.onBlur(event, this.breadcrumbId);
result.addEventListener('focusout', this.onFocusOut);
this.parent.appendChild(result);
return result;
}

protected onBlur = (event: FocusEvent, breadcrumbId: string) => {
if (event.relatedTarget && event.relatedTarget instanceof HTMLElement) {
// event.relatedTarget is the element that has the focus after this popup looses the focus.
// If a breadcrumb was clicked the following holds the breadcrumb ID of the clicked breadcrumb.
const clickedBreadcrumbId = event.relatedTarget.getAttribute('data-breadcrumb-id');
if (clickedBreadcrumbId && clickedBreadcrumbId === breadcrumbId) {
// This is a click on the breadcrumb that has openend this popup.
// We do not close this popup here but let the click event of the breadcrumb handle this instead
// because it needs to know that this popup is open to decide if it just closes this popup or
// also open a new popup.
return;
}
if (this.container.contains(event.relatedTarget)) {
// A child element gets focus. Set the focus to the container again.
// Otherwise the popup would not be closed when elements outside the popup get the focus.
// A popup content should not relay on getting a focus.
this.container.focus();
return;
}
protected onFocusOut = (event: FocusEvent) => {
if (!(event.relatedTarget instanceof Element) || !this._container.contains(event.relatedTarget)) {
this.dispose();
}
this.dispose();
}
};

protected escFunction = (event: KeyboardEvent) => {
if (event.key === 'Escape' || event.key === 'Esc') {
this.dispose();
}
}
};

dispose(): void {
this.toDispose.dispose();
if (this.parent.contains(this.container)) {
this.parent.removeChild(this.container);
if (!this.toDispose.disposed) {
this.onDidDisposeEmitter.fire();
this.toDispose.dispose();
this._container.remove();
this._isOpen = false;
document.removeEventListener('keyup', this.escFunction);
}
this.isOpen = false;
document.removeEventListener('keyup', this.escFunction);
}

addDisposable(disposable: Disposable | undefined): void {
if (disposable) { this.toDispose.push(disposable); }
}
}
11 changes: 5 additions & 6 deletions packages/core/src/browser/breadcrumbs/breadcrumb-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,22 @@

import * as React from 'react';
import { injectable } from 'inversify';
import { Breadcrumb } from './breadcrumb';
import { Breadcrumbs } from './breadcrumbs';
import { Breadcrumb, Styles } from './breadcrumbs-constants';

export const BreadcrumbRenderer = Symbol('BreadcrumbRenderer');
export interface BreadcrumbRenderer {
/**
* Renders the given breadcrumb. If `onClick` is given, it is called on breadcrumb click.
*/
render(breadcrumb: Breadcrumb, onClick?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode;
render(breadcrumb: Breadcrumb, onMouseDown?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode;
}

@injectable()
export class DefaultBreadcrumbRenderer implements BreadcrumbRenderer {
render(breadcrumb: Breadcrumb, onClick?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode {
render(breadcrumb: Breadcrumb, onMouseDown?: (breadcrumb: Breadcrumb, event: React.MouseEvent) => void): React.ReactNode {
return <li key={breadcrumb.id} title={breadcrumb.longLabel}
className={Breadcrumbs.Styles.BREADCRUMB_ITEM + (!onClick ? '' : ' ' + Breadcrumbs.Styles.BREADCRUMB_ITEM_HAS_POPUP)}
onClick={event => onClick && onClick(breadcrumb, event)}
className={Styles.BREADCRUMB_ITEM + (!onMouseDown ? '' : ' ' + Styles.BREADCRUMB_ITEM_HAS_POPUP)}
onMouseDown={event => onMouseDown && onMouseDown(breadcrumb, event)}
tabIndex={0}
data-breadcrumb-id={breadcrumb.id}
>
Expand Down
34 changes: 0 additions & 34 deletions packages/core/src/browser/breadcrumbs/breadcrumb.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,36 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { MaybePromise, Event } from '../../common';
import { Disposable } from '../../../shared/vscode-languageserver-protocol';
import URI from '../../common/uri';
import { Breadcrumb } from './breadcrumb';
import { Disposable } from '../../common';

export namespace Styles {
export const BREADCRUMBS = 'theia-breadcrumbs';
export const BREADCRUMB_ITEM = 'theia-breadcrumb-item';
export const BREADCRUMB_POPUP_OVERLAY_CONTAINER = 'theia-breadcrumbs-popups-overlay';
export const BREADCRUMB_POPUP = 'theia-breadcrumbs-popup';
export const BREADCRUMB_ITEM_HAS_POPUP = 'theia-breadcrumb-item-haspopup';
}

/** A single breadcrumb in the breadcrumbs bar. */
export interface Breadcrumb {

/** An ID of this breadcrumb that should be unique in the breadcrumbs bar. */
readonly id: string

/** The breadcrumb type. Should be the same as the contribution type `BreadcrumbsContribution#type`. */
readonly type: symbol

/** The text that will be rendered as label. */
readonly label: string

/** A longer text that will be used as tooltip text. */
readonly longLabel: string

/** A CSS class for the icon. */
readonly iconClass?: string
}

export const BreadcrumbsContribution = Symbol('BreadcrumbsContribution');
export interface BreadcrumbsContribution {
Expand All @@ -27,14 +54,19 @@ export interface BreadcrumbsContribution {
readonly type: symbol;

/**
* The priority of this breadcrumbs contribution. Contributions with lower priority are rendered first.
* The priority of this breadcrumbs contribution. Contributions are rendered left to right in order of ascending priority.
*/
readonly priority: number;

/**
* An event emitter that should fire when breadcrumbs change for a given URI.
*/
readonly onDidChangeBreadcrumbs: Event<URI>;

/**
* Computes breadcrumbs for a given URI.
*/
computeBreadcrumbs(uri: URI): Promise<Breadcrumb[]>;
computeBreadcrumbs(uri: URI): MaybePromise<Breadcrumb[]>;

/**
* Attaches the breadcrumb popup content for the given breadcrumb as child to the given parent.
Expand Down
Loading

0 comments on commit 8ca31d0

Please sign in to comment.