Merge pull request #340 from mathuo/338-options-before-tabs-start

feat: pre-tab actions
This commit is contained in:
mathuo 2023-10-01 20:45:40 +01:00 committed by GitHub
commit 2337373a6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 305 additions and 18 deletions

View File

@ -638,4 +638,214 @@ describe('tabsContainer', () => {
expect(preventDefaultSpy).toBeCalledTimes(1);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
});
test('pre header actions', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div');
cut.setPrefixActionsElement(actions);
result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
const updatedActions = document.createElement('div');
cut.setPrefixActionsElement(updatedActions);
result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setPrefixActionsElement(undefined);
result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
test('left header actions', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div');
cut.setLeftActionsElement(actions);
result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
const updatedActions = document.createElement('div');
cut.setLeftActionsElement(updatedActions);
result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setLeftActionsElement(undefined);
result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
test('right header actions', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
let result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div');
cut.setRightActionsElement(actions);
result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions);
const updatedActions = document.createElement('div');
cut.setRightActionsElement(updatedActions);
result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setRightActionsElement(undefined);
result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0);
});
});

View File

@ -43,6 +43,7 @@ export interface ITabsContainer extends IDisposable {
openPanel: (panel: IDockviewPanel, index?: number) => void;
setRightActionsElement(element: HTMLElement | undefined): void;
setLeftActionsElement(element: HTMLElement | undefined): void;
setPrefixActionsElement(element: HTMLElement | undefined): void;
show(): void;
hide(): void;
}
@ -55,12 +56,14 @@ export class TabsContainer
private readonly tabContainer: HTMLElement;
private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement;
private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex = -1;
private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined;
private preActions: HTMLElement | undefined;
private _hidden = false;
@ -129,6 +132,20 @@ export class TabsContainer
}
}
setPrefixActionsElement(element: HTMLElement | undefined): void {
if (this.preActions === element) {
return;
}
if (this.preActions) {
this.preActions.remove();
this.preActions = undefined;
}
if (element) {
this.preActionsContainer.appendChild(element);
this.preActions = element;
}
}
get element(): HTMLElement {
return this._element;
}
@ -192,11 +209,15 @@ export class TabsContainer
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'left-actions-container';
this.preActionsContainer = document.createElement('div');
this.preActionsContainer.className = 'pre-actions-container';
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tabs-container';
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.preActionsContainer);
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);

View File

@ -92,6 +92,7 @@ export type DockviewComponentUpdateOptions = Pick<
| 'defaultTabComponent'
| 'createLeftHeaderActionsElement'
| 'createRightHeaderActionsElement'
| 'createPrefixHeaderActionsElement'
| 'disableFloatingGroups'
| 'floatingGroupBounds'
>;

View File

@ -144,6 +144,7 @@ export class DockviewGroupPanelModel
private _isFloating = false;
private _rightHeaderActions: IHeaderActionsRenderer | undefined;
private _leftHeaderActions: IHeaderActionsRenderer | undefined;
private _prefixHeaderActions: IHeaderActionsRenderer | undefined;
private mostRecentlyUsed: IDockviewPanel[] = [];
@ -398,6 +399,21 @@ export class DockviewGroupPanelModel
this._leftHeaderActions.element
);
}
if (this.accessor.options.createPrefixHeaderActionsElement) {
this._prefixHeaderActions =
this.accessor.options.createPrefixHeaderActionsElement(
this.groupPanel
);
this.addDisposables(this._prefixHeaderActions);
this._prefixHeaderActions.init({
containerApi: new DockviewApi(this.accessor),
api: this.groupPanel.api,
});
this.tabsContainer.setPrefixActionsElement(
this._prefixHeaderActions.element
);
}
}
public indexOf(panel: IDockviewPanel): number {

View File

@ -84,6 +84,9 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
createLeftHeaderActionsElement?: (
group: DockviewGroupPanel
) => IHeaderActionsRenderer;
createPrefixHeaderActionsElement?: (
group: DockviewGroupPanel
) => IHeaderActionsRenderer;
singleTabMode?: 'fullwidth' | 'default';
parentElement?: HTMLElement;
disableFloatingGroups?: boolean;

View File

@ -67,6 +67,7 @@ export interface IDockviewReactProps {
defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>;
rightHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
leftHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
prefixHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
singleTabMode?: 'fullwidth' | 'default';
disableFloatingGroups?: boolean;
floatingGroupBounds?:
@ -166,6 +167,10 @@ export const DockviewReact = React.forwardRef(
props.rightHeaderActionsComponent,
{ addPortal }
),
createPrefixHeaderActionsElement: createGroupControlElement(
props.prefixHeaderActionsComponent,
{ addPortal }
),
singleTabMode: props.singleTabMode,
disableFloatingGroups: props.disableFloatingGroups,
floatingGroupBounds: props.floatingGroupBounds,
@ -301,6 +306,18 @@ export const DockviewReact = React.forwardRef(
});
}, [props.leftHeaderActionsComponent]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
createPrefixHeaderActionsElement: createGroupControlElement(
props.prefixHeaderActionsComponent,
{ addPortal }
),
});
}, [props.prefixHeaderActionsComponent]);
return (
<div
className={props.className}

View File

@ -59,7 +59,7 @@ import { DockviewReact } from 'dockview';
```
| Property | Type | Optional | Default | Description |
| --------------------------- | ------------------------------------ | -------- | --------- | ----------- |
| ---------------------------- | ------------------------------------ | -------- | --------- | ----------- |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| tabComponents | object | Yes | | |
@ -72,6 +72,7 @@ import { DockviewReact } from 'dockview';
| defaultTabComponent | object | Yes | | |
| leftHeaderActionsComponent | object | Yes | | |
| rightHeaderActionsComponent | object | Yes | | |
| prefixHeaderActionsComponent | object | Yes | | |
| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | |
## Dockview API
@ -804,8 +805,8 @@ better understanding of what this means, try and drag the panels in the example
### Group Controls Panel
`DockviewReact` accepts `leftHeaderActionsComponent` and `rightHeaderActionsComponent` which expect a React component with props `IDockviewHeaderActionsProps`.
These controls are rendered of the left and right side of the space to the right of the tabs in the header bar.
`DockviewReact` accepts `leftHeaderActionsComponent`, `rightHeaderActionsComponent` and `prefixHeaderActionsComponent` which expect a React component with props `IDockviewHeaderActionsProps`.
These controls are rendered to left and right side of the space to the right of the tabs in the header bar as well as before the first tab in the case of the prefix header prop.
```tsx
const Component: React.FunctionComponent<IDockviewHeaderActionsProps> = () => {

View File

@ -191,6 +191,23 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => {
);
};
const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => {
return (
<div
className="group-control"
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
}}
>
<Icon icon="Menu" />
</div>
);
};
const DockviewDemo = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
@ -262,6 +279,7 @@ const DockviewDemo = (props: { theme?: string }) => {
defaultTabComponent={headerComponents.default}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
prefixHeaderActionsComponent={PrefixHeaderControls}
onReady={onReady}
className={props.theme || 'dockview-theme-abyss'}
/>