Skip to content

Commit 3a55395

Browse files
committed
fix(ui): align tree visuals with VS Code
1 parent 16706c6 commit 3a55395

11 files changed

Lines changed: 1238 additions & 1120 deletions

File tree

packages/ui/README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,24 @@ that mirror native sticky-scroll preferences must read
124124
`workbench.tree.stickyScrollMaxItemCount` in the extension host and send the
125125
values to the webview.
126126

127-
The flat projection in `treeModel.ts` derives order, visibility, hierarchy, and
128-
type navigation from `nodes` and `expandedIds`; collapsed descendants do not
129-
render. The component is not virtualized.
127+
```mermaid
128+
flowchart LR
129+
accTitle: Tree architecture
130+
accDescr: Data and input flow through the pure Tree modules into the React and DOM adapter.
131+
132+
Props[Nodes and controlled props] --> Model[treeModel.ts]
133+
Events[Pointer and keyboard events] --> Policy[treePolicy.ts]
134+
Policy --> Commands[Tree commands]
135+
Model --> Transition[treeTransition.ts]
136+
Commands --> Transition
137+
Transition --> Adapter[useTreeAdapter.ts]
138+
Adapter --> Rows[Tree.tsx and TreeRow.tsx]
139+
Adapter --> Sticky[StickyScroll.tsx]
140+
```
141+
142+
The model, policy, and transitions stay pure. The adapter owns React and DOM
143+
integration. The flat visible model supports future windowing, but the Tree is
144+
not currently virtualized.
130145

131146
Rows are 22px tall and keep the VS Code twistie gutter. For Explorer-style file
132147
trees whose branches have no icons, `variant="explorer"` aligns leaf icons with

packages/ui/src/components/Tree/Tree.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
--ui-tree-row-height: 22px;
44
width: 100%;
55
min-width: 0;
6+
outline: 0;
67
}
78

89
.ui-tree-item {
@@ -27,13 +28,21 @@
2728
top: 0;
2829
z-index: 100;
2930
height: 0;
31+
outline: 0;
3032
}
3133

3234
.ui-tree-sticky__rows {
3335
position: absolute;
3436
inset-inline: 0;
3537
overflow: hidden;
36-
box-shadow: var(--ui-tree-sticky-shadow) 0 6px 6px -6px;
38+
}
39+
40+
.ui-tree-sticky__shadow {
41+
position: absolute;
42+
inset-inline: 0;
43+
height: 3px;
44+
box-shadow: var(--ui-tree-sticky-shadow) 0 6px 6px -6px inset;
45+
pointer-events: none;
3746
}
3847

3948
.ui-tree-sticky__rows > .ui-tree-item {

packages/ui/src/components/Tree/sticky/StickyScroll.tsx

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function StickyScroll({
7272
const pinnedRows = state.ids
7373
.map((id) => rowsById.get(id))
7474
.filter((row) => row !== undefined);
75+
const pinnedHeight = pinnedRows.length * ROW_HEIGHT_PX + state.pushOffset;
7576
const [requestedIndex, setRequestedIndex] = useState(0);
7677
const focusedIndex = Math.max(
7778
0,
@@ -148,31 +149,35 @@ export function StickyScroll({
148149
}}
149150
>
150151
{pinnedRows.length > 0 ? (
151-
<div
152-
className="ui-tree-sticky__rows"
153-
style={{
154-
height: pinnedRows.length * ROW_HEIGHT_PX + state.pushOffset,
155-
}}
156-
>
157-
{pinnedRows.map((row, index) => (
158-
<TreeRow
159-
key={row.node.id}
160-
row={row}
161-
focused={index === focusedIndex}
162-
actionsEnabled={index === focusedIndex && !row.node.disabled}
163-
style={{
164-
top:
165-
index * ROW_HEIGHT_PX +
166-
(index === pinnedRows.length - 1 ? state.pushOffset : 0),
167-
zIndex: pinnedRows.length - index,
168-
}}
169-
onClick={(event, twistie) => {
170-
if (!adapter.isSelectionGesture(event)) reveal(row, index);
171-
adapter.onPointer(row, event, twistie, "sticky");
172-
}}
173-
/>
174-
))}
175-
</div>
152+
<>
153+
<div
154+
className="ui-tree-sticky__rows"
155+
style={{ height: pinnedHeight }}
156+
>
157+
{pinnedRows.map((row, index) => (
158+
<TreeRow
159+
key={row.node.id}
160+
row={row}
161+
focused={index === focusedIndex}
162+
actionsEnabled={index === focusedIndex && !row.node.disabled}
163+
style={{
164+
top:
165+
index * ROW_HEIGHT_PX +
166+
(index === pinnedRows.length - 1 ? state.pushOffset : 0),
167+
zIndex: pinnedRows.length - index,
168+
}}
169+
onClick={(event, twistie) => {
170+
if (!adapter.isSelectionGesture(event)) reveal(row, index);
171+
adapter.onPointer(row, event, twistie, "sticky");
172+
}}
173+
/>
174+
))}
175+
</div>
176+
<div
177+
className="ui-tree-sticky__shadow"
178+
style={{ top: pinnedHeight }}
179+
/>
180+
</>
176181
) : null}
177182
</div>
178183
);

packages/ui/src/vscode-parity.stories.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
VscodeToolbarButton,
1010
} from "@vscode-elements/react-elements";
1111
import { useState } from "react";
12+
import { expect, waitFor } from "storybook/test";
1213

1314
import { Button } from "./components/Button/Button";
1415
import {
@@ -184,11 +185,13 @@ const MenuParity = (): React.JSX.Element => (
184185
style={{
185186
display: "grid",
186187
gridTemplateColumns: "220px 220px",
187-
gap: "16px",
188+
gap: "8px 16px",
188189
alignItems: "start",
189190
fontSize: "13px",
190191
}}
191192
>
193+
<strong>Ours</strong>
194+
<strong>VS Code Elements</strong>
192195
<DropdownMenu>
193196
<DropdownMenuTrigger asChild>
194197
<Button variant="secondary">Menu</Button>
@@ -230,5 +233,15 @@ export const Menu: Story = {
230233
render: () => <MenuParity />,
231234
play: async ({ canvasElement }) => {
232235
await openMenu(canvasElement, "Menu");
236+
const reference = canvasElement.querySelector("vscode-context-menu");
237+
await expect(reference).not.toBeNull();
238+
// Opening our portalled menu clicks outside the reference menu. Reopen
239+
// it after that click so Pixel always captures both implementations.
240+
reference?.setAttribute("show", "");
241+
await waitFor(() =>
242+
expect(
243+
reference?.shadowRoot?.querySelector(".context-menu"),
244+
).not.toBeNull(),
245+
);
233246
},
234247
};

0 commit comments

Comments
 (0)