feat: use svg icons and CSS color property for icons

This commit is contained in:
mathuo 2022-06-05 20:17:59 +01:00
parent 68e573e5f0
commit 0c28f2dabe
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
19 changed files with 560 additions and 76 deletions

View File

@ -75,7 +75,7 @@ class PanelTabPartTest implements ITabRenderer {
isDisposed: boolean = false;
constructor(public readonly id: string, component: string) {
this.element.classList.add(`testpanel-${id}`);
this.element.className = `panel-tab-part-${id}`;
}
updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void {
@ -1686,7 +1686,293 @@ describe('dockviewComponent', () => {
return disposable.dispose();
});
// group is disposed of when dockview is disposed
// watermark is disposed of when removed
// watermark is disposed of when dockview is disposed
test('load a layout with a non-existant tab id', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2', 'panel3'],
id: 'group-2',
},
size: 500,
},
{
type: 'leaf',
data: { views: ['panel4'], id: 'group-3' },
size: 500,
},
],
size: 250,
},
{
type: 'leaf',
data: { views: ['panel5'], id: 'group-4' },
size: 250,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: {
content: { id: 'default' },
tab: { id: '__non__existant_tab__' },
},
title: 'panel2',
},
panel3: {
id: 'panel3',
view: { content: { id: 'default' } },
title: 'panel3',
},
panel4: {
id: 'panel4',
view: { content: { id: 'default' } },
title: 'panel4',
},
panel5: {
id: 'panel5',
view: { content: { id: 'default' } },
title: 'panel5',
},
},
options: { tabHeight: 25 },
});
});
test('load and persist layout with custom tab header', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: {
content: { id: 'default' },
tab: { id: 'test_tab_id' },
},
title: 'panel2',
},
},
options: { tabHeight: 25 },
});
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: {
content: { id: 'default' },
tab: { id: 'test_tab_id' },
},
title: 'panel2',
},
},
options: { tabHeight: 25 },
});
});
test('#2', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2', 'panel3'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: {
content: { id: 'default' },
tab: { id: 'test_tab_id' },
},
title: 'panel2',
},
panel3: {
id: 'panel3',
view: { content: { id: 'default' } },
title: 'panel3',
},
},
options: { tabHeight: 25 },
});
const group = dockview.getGroupPanel('panel2')!.api.group;
const viewQuery = group.element.querySelectorAll(
'.groupview > .tabs-and-actions-container > .tabs-container > .tab'
);
expect(viewQuery.length).toBe(2);
const viewQuery2 = group.element.querySelectorAll(
'.groupview > .tabs-and-actions-container > .tabs-container > .tab > .default-tab'
);
expect(viewQuery2.length).toBe(1);
const viewQuery3 = group.element.querySelectorAll(
'.groupview > .tabs-and-actions-container > .tabs-container > .tab > .panel-tab-part-test_tab_id'
);
expect(viewQuery3.length).toBe(1);
});
// load a layout with a default tab identifier when react default is present
// load a layout with invialid panel identifier
});

View File

@ -11,20 +11,19 @@
margin: 0px;
justify-content: flex-end;
a:active {
-webkit-mask-size: 100% 100% !important;
mask-size: 100% 100% !important;
}
.close-action {
background-color: white;
height: 16px;
width: 16px;
display: block;
-webkit-mask: var(--dv-tab-close-icon) 50% 50% / 90% 90% no-repeat;
mask: var(--dv-tab-close-icon) 50% 50% / 90% 90% no-repeat;
margin-right: '0.5em';
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
color: var(--dv-activegroup-hiddenpanel-tab-color);
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
}
}
}

View File

@ -38,17 +38,17 @@
display: flex;
min-width: 80px;
align-items: center;
padding-left: 10px;
padding: 0px 8px;
white-space: nowrap;
text-overflow: elipsis;
.tab-content {
padding: 0px 8px;
flex-grow: 1;
}
.action-container {
text-align: right;
width: 28px;
display: flex;
.tab-list {
@ -57,19 +57,17 @@
margin: 0px;
justify-content: flex-end;
a:active:hover {
-webkit-mask-size: 100% 100% !important;
mask-size: 100% 100% !important;
}
.tab-action {
height: 16px;
width: 16px;
display: block;
-webkit-mask: var(--dv-tab-close-icon) 50% 50% / 90% 90%
no-repeat;
mask: var(--dv-tab-close-icon) 50% 50% / 90% 90% no-repeat;
margin-right: '0.5em';
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
&.disable-close {
display: none;

View File

@ -6,6 +6,8 @@ import {
import { addDisposableListener } from '../../../events';
import { PanelUpdateEvent } from '../../../panel/types';
import { GroupPanel } from '../../../groupview/groupviewPanel';
import { createCloseButton } from '../../../svg';
import { DEFAULT_TAB_IDENTIFIER } from '../../../react';
export class DefaultTab extends CompositeDisposable implements ITabRenderer {
private _element: HTMLElement;
@ -24,7 +26,7 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
}
get id() {
return '__DEFAULT_TAB__';
return DEFAULT_TAB_IDENTIFIER;
}
constructor() {
@ -42,8 +44,10 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this._list = document.createElement('ul');
this._list.className = 'tab-list';
//
this.action = document.createElement('a');
this.action = document.createElement('div');
this.action.className = 'tab-action';
this.action.appendChild(createCloseButton());
//
this._element.appendChild(this._content);
this._element.appendChild(this._actionContainer);

View File

@ -8,6 +8,7 @@ import { toggleClass } from '../../../dom';
import { CompositeDisposable } from '../../../lifecycle';
import { GroupPanel } from '../../../groupview/groupviewPanel';
import { PanelUpdateEvent } from '../../../panel/types';
import { createCloseButton } from '../../../svg';
export class Watermark
extends CompositeDisposable
@ -42,8 +43,9 @@ export class Watermark
title.appendChild(emptySpace);
title.appendChild(actions.element);
const closeAnchor = document.createElement('a');
const closeAnchor = document.createElement('div');
closeAnchor.className = 'close-action';
closeAnchor.appendChild(createCloseButton());
actions.add(closeAnchor);

View File

@ -18,24 +18,12 @@
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
.tab-action {
background-color: var(
--dv-activegroup-visiblepanel-tab-color
);
}
}
&.inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
.tab-action {
background-color: var(
--dv-activegroup-hiddenpanel-tab-color
);
}
}
}
}
@ -46,24 +34,12 @@
--dv-inactivegroup-visiblepanel-tab-background-color
);
color: var(--dv-inactivegroup-visiblepanel-tab-color);
.tab-action {
background-color: var(
--dv-inactivegroup-visiblepanel-tab-color
);
}
}
&.inactive-tab {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-background-color
);
color: var(--dv-inactivegroup-hiddenpanel-tab-color);
.tab-action {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-color
);
}
}
}
}

View File

@ -72,6 +72,7 @@ export type DockviewComponentUpdateOptions = Pick<
| 'frameworkTabComponents'
| 'showDndOverlay'
| 'watermarkFrameworkComponent'
| 'defaultTabComponent'
>;
export interface DockviewDropEvent extends GroupviewDropEvent {
@ -751,7 +752,7 @@ export class DockviewComponent
private createPanel(options: AddPanelOptions, group: GroupPanel): IDockviewPanel {
const view = new DefaultGroupPanelView({
content: this.createContentComponent(options.id, options.component),
tab: this.createTabComponent(options.id, options.tabComponent),
tab: this.createTabComponent(options.id, options.tabComponent || this.options.defaultTabComponent),
});
const panel = new DockviewGroupPanel(options.id, this, this._api, group);

View File

@ -62,6 +62,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
tabHeight?: number;
orientation?: Orientation;
styles?: ISplitviewStyles;
defaultTabComponent?: string;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
}

View File

@ -4,11 +4,14 @@ import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { PanelUpdateEvent } from '../panel/types';
import { IPaneHeaderPart, PanePanelInitParameter } from './paneviewPanel';
import { toggleClass } from '../dom';
import { createChevronRightButton, createExpandMoreButton } from '../svg';
export class DefaultHeader
extends CompositeDisposable
implements IPaneHeaderPart
{
private readonly _expandedIcon = createExpandMoreButton();
private readonly _collapsedIcon = createChevronRightButton();
private readonly disposable = new MutableDisposable();
private readonly _element: HTMLElement;
private readonly _content: HTMLElement;
@ -21,11 +24,13 @@ export class DefaultHeader
constructor() {
super();
this._element = document.createElement('div');
this.element.className = 'default-header';
this._content = document.createElement('span');
this._expander = document.createElement('a');
this._expander = document.createElement('div');
this._expander.className = 'dockview-pane-header-icon';
this.element.appendChild(this._expander);
this.element.appendChild(this._content);
@ -41,15 +46,35 @@ export class DefaultHeader
this.apiRef.api = params.api;
this._content.textContent = params.title;
this._expander.textContent = '▼';
toggleClass(this._expander, 'collapsed', !params.api.isExpanded);
this.updateIcon();
this.disposable.value = params.api.onDidExpansionChange((e) => {
toggleClass(this._expander, 'collapsed', !e.isExpanded);
this.disposable.value = params.api.onDidExpansionChange(() => {
this.updateIcon();
});
}
private updateIcon() {
const isExpanded = !!this.apiRef.api?.isExpanded;
toggleClass(this._expander, 'collapsed', !isExpanded);
if (isExpanded) {
if (this._expander.contains(this._collapsedIcon)) {
this._collapsedIcon.remove();
}
if (!this._expander.contains(this._expandedIcon)) {
this._expander.appendChild(this._expandedIcon);
}
} else {
if (this._expander.contains(this._expandedIcon)) {
this._expandedIcon.remove();
}
if (!this._expander.contains(this._collapsedIcon)) {
this._expander.appendChild(this._collapsedIcon);
}
}
}
update(_params: PanelUpdateEvent) {
//
}

View File

@ -31,8 +31,10 @@
padding: 0px 8px;
cursor: pointer;
.collapsed {
transform: rotate(-90deg);
.dockview-pane-header-icon {
display: flex;
justify-content: center;
align-items: center;
}
> span {

View File

@ -7,6 +7,7 @@ import { DockviewApi } from '../api/component.api';
import { DefaultTab } from '../dockview/components/tab/defaultTab';
import { DefaultGroupPanelView } from '../dockview/defaultGroupPanelView';
import { GroupPanel } from '../groupview/groupviewPanel';
import { ITabRenderer } from '../groupview/types';
export class ReactPanelDeserialzier implements IPanelDeserializer {
constructor(private readonly layout: DockviewComponent) {}
@ -21,6 +22,30 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
const suppressClosable = panelData.suppressClosable;
const viewData = panelData.view;
let tab: ITabRenderer;
if (viewData.tab?.id) {
tab = createComponent(
viewData.tab.id,
viewData.tab.id,
this.layout.options.tabComponents,
this.layout.options.frameworkTabComponents,
this.layout.options.frameworkComponentFactory?.tab,
() => new DefaultTab()
);
} else if (this.layout.options.defaultTabComponent) {
tab = createComponent(
this.layout.options.defaultTabComponent,
this.layout.options.defaultTabComponent,
this.layout.options.tabComponents,
this.layout.options.frameworkTabComponents,
this.layout.options.frameworkComponentFactory?.tab,
() => new DefaultTab()
);
} else {
tab = new DefaultTab();
}
const view = new DefaultGroupPanelView({
content: createComponent(
viewData.content.id,
@ -29,15 +54,7 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
this.layout.options.frameworkComponents,
this.layout.options.frameworkComponentFactory?.content
),
tab: viewData.tab?.id
? createComponent(
viewData.tab.id,
viewData.tab.id,
this.layout.options.tabComponents,
this.layout.options.frameworkTabComponents,
this.layout.options.frameworkComponentFactory?.tab
)
: new DefaultTab(),
tab,
});
const panel = new DockviewGroupPanel(

View File

@ -0,0 +1,32 @@
.tab {
.dockview-react-tab {
display: flex;
padding: 0px 8px;
align-items: center;
height: 100%;
.dockview-react-tab-title {
padding: 0px 8px;
flex-grow: 1;
}
.dockview-react-tab-action {
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
}
}
&.inactive-tab:not(:hover) {
.dockview-react-tab-action {
visibility: hidden;
}
}
}

View File

@ -0,0 +1,42 @@
import { IDockviewPanelHeaderProps } from './dockview';
import * as React from 'react';
import { CloseButton } from '../svg';
export type IDockviewDefaultTabProps = IDockviewPanelHeaderProps &
React.DOMAttributes<HTMLDivElement>;
export const DockviewDefaultTab: React.FunctionComponent<IDockviewDefaultTabProps> =
({ api, containerApi: _containerApi, params: _params, ...rest }) => {
const onClose = React.useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
api.close();
},
[api]
);
const onClick = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
api.setActive();
if (rest.onClick) {
rest.onClick(event);
}
},
[api, rest.onClick]
);
const iconClassname = React.useMemo(() => {
const cn = ['dockview-react-tab-action'];
return cn.join(',');
}, [api.suppressClosable]);
return (
<div {...rest} onClick={onClick} className="dockview-react-tab">
<span className="dockview-react-tab-title">{api.title}</span>
<div className={iconClassname} onClick={onClose}>
<CloseButton />
</div>
</div>
);
};

View File

@ -19,6 +19,8 @@ import { PanelCollection, PanelParameters } from '../types';
import { watchElementResize } from '../../dom';
import { IContentRenderer, ITabRenderer } from '../../groupview/types';
export const DEFAULT_TAB_IDENTIFIER = '__default__tab__';
export interface IGroupPanelBaseProps<T extends {} = Record<string, any>>
extends PanelParameters<T> {
api: DockviewPanelApi;
@ -47,6 +49,7 @@ export interface IDockviewReactProps {
hideBorders?: boolean;
className?: string;
disableAutoResizing?: boolean;
defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>;
}
export const DockviewReact = React.forwardRef(
@ -124,9 +127,13 @@ export const DockviewReact = React.forwardRef(
const dockview = new DockviewComponent(element, {
frameworkComponentFactory: factory,
frameworkComponents: props.components,
frameworkTabComponents: props.tabComponents,
frameworkTabComponents: {
...(props.tabComponents || {}),
[DEFAULT_TAB_IDENTIFIER]: props.defaultTabComponent,
},
tabHeight: props.tabHeight,
watermarkFrameworkComponent: props.watermarkComponent,
defaultTabComponent: DEFAULT_TAB_IDENTIFIER,
styles: props.hideBorders
? { separatorBorder: 'transparent' }
: undefined,
@ -222,6 +229,19 @@ export const DockviewReact = React.forwardRef(
};
}, [props.onTabContextMenu]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
defaultTabComponent: DEFAULT_TAB_IDENTIFIER,
frameworkTabComponents: {
...(props.tabComponents || {}),
[DEFAULT_TAB_IDENTIFIER]: props.defaultTabComponent,
},
});
}, [props.defaultTabComponent]);
return (
<div
className={props.className}

View File

@ -1,4 +1,5 @@
export * from './dockview/dockview';
export * from './dockview/defaultTab';
export * from './splitview/splitview';
export * from './gridview/gridview';
export * from './dockview/reactContentPart';

View File

@ -0,0 +1,29 @@
import * as React from 'react';
export const CloseButton = () => (
<svg
height="11"
width="11"
viewBox="0 0 28 28"
aria-hidden={'false'}
focusable={false}
className="dockview-svg"
>
<path d="M2.1 27.3L0 25.2L11.55 13.65L0 2.1L2.1 0L13.65 11.55L25.2 0L27.3 2.1L15.75 13.65L27.3 25.2L25.2 27.3L13.65 15.75L2.1 27.3Z"></path>
</svg>
);
export const ExpandMore = () => {
return (
<svg
width="11"
height="11"
viewBox="0 0 24 15"
aria-hidden={'false'}
focusable={false}
className="dockview-svg"
>
<path d="M12 14.15L0 2.15L2.15 0L12 9.9L21.85 0.0499992L24 2.2L12 14.15Z" />
</svg>
);
};

View File

@ -0,0 +1,7 @@
.dockview-svg {
display: inline-block;
fill: currentcolor;
line-height: 1;
stroke: currentcolor;
stroke-width: 0;
}

View File

@ -0,0 +1,42 @@
const createSvgElementFromPath = (params: {
height: string;
width: string;
viewbox: string;
path: string;
}) => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttributeNS(null, 'height', params.height);
svg.setAttributeNS(null, 'width', params.width);
svg.setAttributeNS(null, 'viewBox', params.viewbox);
svg.setAttributeNS(null, 'aria-hidden', 'false');
svg.setAttributeNS(null, 'focusable', 'false');
svg.classList.add('dockview-svg');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttributeNS(null, 'd', params.path);
svg.appendChild(path);
return svg;
};
export const createCloseButton = () =>
createSvgElementFromPath({
width: '11',
height: '11',
viewbox: '0 0 28 28',
path: 'M2.1 27.3L0 25.2L11.55 13.65L0 2.1L2.1 0L13.65 11.55L25.2 0L27.3 2.1L15.75 13.65L27.3 25.2L25.2 27.3L13.65 15.75L2.1 27.3Z',
});
export const createExpandMoreButton = () =>
createSvgElementFromPath({
width: '11',
height: '11',
viewbox: '0 0 24 15',
path: 'M12 14.15L0 2.15L2.15 0L12 9.9L21.85 0.0499992L24 2.2L12 14.15Z',
});
export const createChevronRightButton = () =>
createSvgElementFromPath({
width: '11',
height: '11',
viewbox: '0 0 15 25',
path: 'M2.15 24.1L0 21.95L9.9 12.05L0 2.15L2.15 0L14.2 12.05L2.15 24.1Z',
});

View File

@ -2,10 +2,10 @@
--dv-paneview-active-outline-color: dodgerblue;
--dv-tabs-and-actions-container-font-size: 13px;
--dv-tabs-and-actions-container-height: 35px;
--dv-tab-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>');
--dv-drag-over-background-color: rgba(83, 89, 93, 0.5);
--dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
}
@mixin dockview-theme-dark-mixin {