Skip to content

Commit 07b9a0f

Browse files
authored
feat: editorTree & editorTabs support scroll into view (#275)
* fix: collapse content no longer contains Scrollable in automatically * fix: improve collapse content calculation * feat: scrollable component support to get ref * feat: editorTree & editorTab support to scroll into view * fix: add data-* for entry in folderTree
1 parent 58888d5 commit 07b9a0f

File tree

10 files changed

+251
-146
lines changed

10 files changed

+251
-146
lines changed

src/components/collapse/index.tsx

+49-38
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import {
1313
collapseExtraClassName,
1414
collapseContentClassName,
1515
} from './base';
16-
import { Scrollable } from '../scrollable';
1716
import { select } from 'mo/common/dom';
1817

1918
type RenderFunctionProps = (data: DataBaseProps) => React.ReactNode;
20-
interface DataBaseProps {
19+
export interface DataBaseProps {
2120
id: React.Key;
2221
name: string;
2322
className?: string;
@@ -50,15 +49,16 @@ export interface ICollapseProps {
5049
}
5150

5251
// default collapse height, only contains header
53-
const HEADER_HEIGTH = 26;
52+
export const HEADER_HEIGTH = 26;
5453
/**
5554
* It's the max height for the item which set the grow to 0
5655
*/
57-
const MAX_GROW_HEIGHT = 220;
56+
export const MAX_GROW_HEIGHT = 220;
5857

5958
export function Collapse(props: ICollapseProps) {
6059
const [activePanelKeys, setActivePanelKeys] = useState<React.Key[]>([]);
6160
const wrapper = React.useRef<HTMLDivElement>(null);
61+
const requestAF = React.useRef<number>();
6262

6363
const {
6464
className,
@@ -98,9 +98,14 @@ export function Collapse(props: ICollapseProps) {
9898
const isActive = activePanelKeys.includes(panel.id);
9999
let isEmpty = true;
100100
if (isActive) {
101-
const contentDom = select(
102-
`.${collapseContentClassName}[data-content='${panel.id}']`
103-
);
101+
const contentDom =
102+
select(
103+
`.${collapseContentClassName}[data-content='${panel.id}']`
104+
)?.querySelector(`[data-content='${panel.id}']`) ||
105+
select(
106+
`.${collapseContentClassName}[data-content='${panel.id}']`
107+
);
108+
104109
isEmpty = !contentDom?.hasChildNodes();
105110
}
106111
panel._isEmpty = isEmpty;
@@ -117,21 +122,20 @@ export function Collapse(props: ICollapseProps) {
117122
`.${collapseItemClassName}[data-content='${panel.id}']`
118123
);
119124

120-
// Only set content height for non-grow-zero panel
121-
// 'Cause when you set height for grow-zero panel, you'll get wrong height next render time
122-
if (panel.config?.grow !== 0) {
123-
const contentDom = select<HTMLElement>(
124-
`.${collapseContentClassName}[data-content='${panel.id}']`
125-
);
126-
if (contentDom) {
127-
contentDom.style.height = `${height - HEADER_HEIGTH - 2}px`;
128-
}
129-
}
130125
if (dom) {
131-
dom.style.height = `${height}px`;
132-
dom.style.top = `${top}px`;
126+
requestAF.current = requestAnimationFrame(() => {
127+
dom.style.height = `${height}px`;
128+
dom.style.top = `${top}px`;
129+
});
133130
}
134131
});
132+
133+
return () => {
134+
if (requestAF.current) {
135+
cancelAnimationFrame(requestAF.current);
136+
requestAF.current = undefined;
137+
}
138+
};
135139
}, [filterData]);
136140

137141
const handleChangeCallback = (key: React.Key) => {
@@ -199,15 +203,22 @@ export function Collapse(props: ICollapseProps) {
199203
const contentDom = select(
200204
`.${collapseContentClassName}[data-content='${key}']`
201205
);
202-
if (contentDom) {
203-
// border-top-width + border-bottom-width = 2
204-
const basisHeight =
205-
contentDom.getBoundingClientRect().height -
206-
2 +
207-
HEADER_HEIGTH;
208-
return basisHeight > 220 ? 220 : basisHeight;
206+
207+
const childrenDom = contentDom?.querySelector(
208+
`[data-content='${key}']`
209+
);
210+
211+
let contentHeight = contentDom?.getBoundingClientRect().height || 0;
212+
213+
if (childrenDom) {
214+
contentHeight = childrenDom.getBoundingClientRect().height;
209215
}
210-
return 0;
216+
217+
// border-top-width + border-bottom-width = 2
218+
const height =
219+
parseInt(contentHeight.toFixed(0)) - 2 + HEADER_HEIGTH;
220+
221+
return height > MAX_GROW_HEIGHT ? MAX_GROW_HEIGHT : height;
211222
});
212223
};
213224

@@ -235,10 +246,12 @@ export function Collapse(props: ICollapseProps) {
235246
// to get current panel content
236247
const contentDom = select(
237248
`.${collapseContentClassName}[data-content='${panel.id}']`
238-
);
249+
)?.querySelector(`[data-content='${panel.id}']`);
250+
239251
if (contentDom) {
240252
const height =
241253
contentDom.getBoundingClientRect().height +
254+
2 +
242255
HEADER_HEIGTH;
243256
res[0] =
244257
height > MAX_GROW_HEIGHT ? MAX_GROW_HEIGHT : height;
@@ -282,7 +295,7 @@ export function Collapse(props: ICollapseProps) {
282295
// In general, the following code will not be excuted
283296
const contentDom = select(
284297
`.${collapseContentClassName}[data-content='${panel.id}']`
285-
);
298+
)?.querySelector(`[data-content='${panel.id}']`);
286299
return contentDom?.hasChildNodes();
287300
}
288301
return false;
@@ -375,15 +388,13 @@ export function Collapse(props: ICollapseProps) {
375388
)}
376389
</div>
377390
</div>
378-
<Scrollable noScrollX isShowShadow>
379-
<div
380-
className={collapseContentClassName}
381-
data-content={panel.id}
382-
tabIndex={0}
383-
>
384-
{renderPanels(panel, panel.renderPanel)}
385-
</div>
386-
</Scrollable>
391+
<div
392+
className={collapseContentClassName}
393+
data-content={panel.id}
394+
tabIndex={0}
395+
>
396+
{renderPanels(panel, panel.renderPanel)}
397+
</div>
387398
</div>
388399
);
389400
})}

src/components/collapse/style.scss

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ $collapse__extra: #{$collapse}__extra;
3333
}
3434
}
3535

36-
&--active {
37-
overflow: auto;
38-
}
39-
4036
&__header {
4137
align-items: center;
4238
border: 1px solid transparent;
@@ -74,6 +70,10 @@ $collapse__extra: #{$collapse}__extra;
7470
border: 1px solid transparent;
7571
width: calc(100% - 3px);
7672

73+
&:not(:empty) {
74+
flex: 1;
75+
}
76+
7777
&:focus {
7878
border-color: var(--list-focusOutline);
7979
}

src/components/scrollable/index.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ const defaultSrollableClassName = prefixClaName('scrollbar');
1616
* the below implementation from this issue:
1717
* https://github.com/xobotyi/react-scrollbars-custom/issues/46
1818
*/
19-
export function Scrollable(props: IScrollbarProps) {
19+
const Scrollable = React.forwardRef<Scrollbar, IScrollbarProps>(function (
20+
props,
21+
ref
22+
) {
2023
const {
2124
className,
2225
children,
@@ -26,6 +29,8 @@ export function Scrollable(props: IScrollbarProps) {
2629
} = props;
2730
const scroller = React.useRef<Scrollbar>(null);
2831

32+
React.useImperativeHandle(ref, () => scroller.current!);
33+
2934
const [isScrolling, setIsScrolling] = useState(false);
3035
const [isMouseOver, setIsMouseOver] = useState(false);
3136
const isShow = isScrolling || isMouseOver;
@@ -105,4 +110,6 @@ export function Scrollable(props: IScrollbarProps) {
105110
{children}
106111
</Scrollbar>
107112
);
108-
}
113+
});
114+
115+
export { Scrollable };

src/components/tabs/tab.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export interface ITabProps<T = any, P = any> extends ITabEvent {
3838

3939
export const tabClassName = prefixClaName('tab');
4040
export const tabItemClassName = getBEMElement(tabClassName, 'item');
41+
export const tabItemActiveClassName = getBEMModifier(
42+
tabItemClassName,
43+
'active'
44+
);
4145

4246
export function Tab<T>(props: ITabProps) {
4347
const {
@@ -115,7 +119,7 @@ export function Tab<T>(props: ITabProps) {
115119
<div
116120
ref={ref}
117121
className={classNames(tabItemClassName, {
118-
[getBEMModifier(tabItemClassName, 'active')]: active,
122+
[tabItemActiveClassName]: active,
119123
})}
120124
onClick={(event: React.MouseEvent) => onSelectTab?.(id)}
121125
onMouseOver={handleMouseOver}

src/controller/explorer/editorTree.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ export class EditorTreeController
6565

6666
this.explorerService.addPanel({
6767
...restEditor,
68-
renderPanel: () => (
68+
renderPanel: (panel) => (
6969
<EditorTreeView
70+
panel={panel}
7071
contextMenu={contextMenu}
7172
headerContextMenu={headerContextMenu}
7273
groupToolbar={groupToolbar}

src/controller/explorer/explorer.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ export class ExplorerController
153153
}
154154
};
155155

156-
public renderFolderTree = () => {
157-
return <FolderTreeView />;
156+
public renderFolderTree = (panel) => {
157+
return <FolderTreeView panel={panel} />;
158158
};
159159
}
160160

src/workbench/editor/group.tsx

+26-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { IEditorController } from 'mo/controller/editor';
1616
import { Menu } from 'mo/components/menu';
1717
import { useContextView } from 'mo/components/contextView';
1818
import { getEventPosition } from 'mo/common/dom';
19+
import Scrollbar from 'react-scrollbars-custom';
20+
import { tabItemActiveClassName } from 'mo/components/tabs/tab';
1921

2022
export interface IEditorGroupProps extends IEditorGroup {
2123
currentGroup?: IEditorGroup;
@@ -38,6 +40,9 @@ export function EditorGroup(props: IEditorGroupProps & IEditorController) {
3840
onUpdateEditorIns,
3941
} = props;
4042

43+
const scrollable = React.useRef<Scrollbar>(null);
44+
const groupTabs = React.useRef<HTMLDivElement>(null);
45+
4146
const isActiveGroup = id === currentGroup?.id;
4247

4348
const contextView = useContextView();
@@ -58,11 +63,30 @@ export function EditorGroup(props: IEditorGroupProps & IEditorController) {
5863
contextView?.dispose();
5964
};
6065
});
66+
67+
// scoller into view
68+
React.useLayoutEffect(() => {
69+
const activeItem = groupTabs.current?.querySelector<HTMLDivElement>(
70+
`.${tabItemActiveClassName}`
71+
);
72+
if (activeItem) {
73+
const width = groupTabs.current?.clientWidth || 0;
74+
const left = activeItem.offsetLeft;
75+
if (left > width) {
76+
scrollable.current?.scrollTo(left, 0);
77+
}
78+
}
79+
}, [currentGroup?.id && currentGroup.tab?.id]);
80+
6181
return (
6282
<div className={groupClassName}>
6383
<div className={groupHeaderClassName}>
64-
<div className={groupTabsClassName}>
65-
<Scrollable noScrollY trackStyle={{ height: 3 }}>
84+
<div className={groupTabsClassName} ref={groupTabs}>
85+
<Scrollable
86+
noScrollY
87+
trackStyle={{ height: 3 }}
88+
ref={scrollable}
89+
>
6690
<Tabs
6791
editable={true}
6892
type="card"

0 commit comments

Comments
 (0)