Skip to content

Commit abfc427

Browse files
feat(portal): add PortalProvider for multi-window support in Eclipse Theia (#12)
Introduce PortalProvider context and usePortalRoot hook to configure portal root elements for floating UI components. This enables proper rendering of dropdowns, tooltips, and menus in Theia secondary windows. Changes: - Add PortalProvider context with usePortalRoot hook - Update Select, Dropdown, Tooltip, ContextMenu, and ButtonGroup to use portal root from context - Export PortalProvider and related types from main package entry - Add comprehensive documentation in CLAUDE.md and README.md - Create dedicated guide pages for VS Code and Eclipse Theia usage - Update navigation with new guide links # Conflicts: # packages/baukasten/src/components/Tooltip/Tooltip.tsx
1 parent c46bbbf commit abfc427

File tree

13 files changed

+711
-6
lines changed

13 files changed

+711
-6
lines changed

CLAUDE.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ Current components (exported from `baukasten`):
216216
- **StatusBar** - VSCode-style status bar
217217
- **Hero** - Hero section component
218218

219+
**Context Providers:**
220+
- **PortalProvider** - Configures portal root for multi-window support (Theia secondary windows)
221+
219222
## Key Technical Details
220223

221224
### Build System
@@ -593,5 +596,55 @@ export const Button: React.FC<ButtonProps> = ({
593596
},
594597
},
595598
});
596-
```
599+
```
600+
601+
## Multi-Window Support (Eclipse Theia)
602+
603+
### The Portal Problem
604+
605+
Portal-based components (Select, Dropdown, Tooltip, ContextMenu, ButtonGroup) use `FloatingPortal` from `@floating-ui/react` to render floating content. By default, portals render to `document.body` of the main window. In Eclipse Theia's multi-window scenarios, this causes dropdowns opened in secondary/popup windows to appear in the main window instead.
606+
607+
### Solution: PortalProvider
608+
609+
The `PortalProvider` context allows specifying a custom root element for all portal-based components:
610+
611+
```tsx
612+
import { PortalProvider, Select } from 'baukasten-ui';
613+
614+
function SecondaryWindowContent() {
615+
const rootRef = useRef<HTMLDivElement>(null);
616+
const [ready, setReady] = useState(false);
617+
618+
useEffect(() => setReady(true), []);
619+
620+
return (
621+
<div ref={rootRef} className="secondary-window-container">
622+
{ready && (
623+
<PortalProvider root={rootRef.current}>
624+
{/* All dropdowns, tooltips, etc. will render in this window */}
625+
<Select options={options} />
626+
<Dropdown trigger={<Button>Menu</Button>}>
627+
<Menu>...</Menu>
628+
</Dropdown>
629+
</PortalProvider>
630+
)}
631+
</div>
632+
);
633+
}
634+
```
635+
636+
### Components Using PortalProvider
637+
638+
These components respect the `PortalProvider` context:
639+
- **Select** - Dropdown options list
640+
- **Dropdown** - Generic dropdown container
641+
- **Tooltip** - Hover tooltips
642+
- **ContextMenu** - Right-click menus
643+
- **ButtonGroup.Dropdown** - Split button dropdowns
644+
645+
### Implementation Notes
646+
647+
1. **Backward Compatible**: If `PortalProvider` is not used, components fall back to default portal behavior (`document.body`)
648+
2. **Hook**: Use `usePortalRoot()` hook to access the portal root in custom components
649+
3. **Context Location**: Defined in `packages/baukasten/src/context/PortalProvider.tsx`
597650

packages/baukasten/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,78 @@ import 'baukasten-ui/dist/baukasten-theia.css'; // For Eclipse Theia
9191
import 'baukasten-ui/dist/baukasten-web.css'; // For standalone web apps
9292
```
9393

94+
## Usage in Eclipse Theia
95+
96+
### Basic Setup
97+
98+
Eclipse Theia applications use the Theia-specific CSS file:
99+
100+
```tsx
101+
import { Button, Input, Badge } from 'baukasten-ui';
102+
import 'baukasten-ui/dist/baukasten-base.css';
103+
import 'baukasten-ui/dist/baukasten-theia.css';
104+
105+
function App() {
106+
return (
107+
<div>
108+
<Button variant="primary">Click me</Button>
109+
<Input label="Username" placeholder="Enter username" />
110+
</div>
111+
);
112+
}
113+
```
114+
115+
### Multi-Window Support (Secondary Windows)
116+
117+
When using Baukasten in Theia secondary/popup windows, portal-based components (Select, Dropdown, Tooltip, etc.) need a `PortalProvider` to ensure dropdowns render in the correct window:
118+
119+
```tsx
120+
import { useRef, useState, useEffect } from 'react';
121+
import { PortalProvider, Select, Dropdown, Button } from 'baukasten-ui';
122+
import 'baukasten-ui/dist/baukasten-base.css';
123+
import 'baukasten-ui/dist/baukasten-theia.css';
124+
125+
function SecondaryWindowContent() {
126+
const rootRef = useRef<HTMLDivElement>(null);
127+
const [ready, setReady] = useState(false);
128+
129+
// Wait for ref to be available
130+
useEffect(() => setReady(true), []);
131+
132+
return (
133+
<div ref={rootRef} style={{ height: '100%' }}>
134+
{ready && (
135+
<PortalProvider root={rootRef.current}>
136+
{/* All portal content will now render in this window */}
137+
<Select
138+
options={[
139+
{ value: '1', label: 'Option 1' },
140+
{ value: '2', label: 'Option 2' },
141+
]}
142+
placeholder="Select an option"
143+
/>
144+
145+
<Dropdown trigger={<Button>Open Menu</Button>}>
146+
<div>Menu content</div>
147+
</Dropdown>
148+
</PortalProvider>
149+
)}
150+
</div>
151+
);
152+
}
153+
```
154+
155+
**Why is this needed?**
156+
157+
By default, portal-based components render floating content (dropdowns, tooltips) to the main window's `document.body`. In Theia's secondary windows, this causes the content to appear on the wrong window. The `PortalProvider` redirects portal content to the correct window.
158+
159+
**Components that use portals:**
160+
- `Select` - dropdown options
161+
- `Dropdown` - dropdown content
162+
- `Tooltip` - tooltip popups
163+
- `ContextMenu` - right-click menus
164+
- `ButtonGroup.Dropdown` - split button menus
165+
94166
## Components
95167

96168
### Button

packages/baukasten/src/components/ButtonGroup/ButtonGroup.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { type Size } from '../../styles';
1919
import { Button, type ButtonVariant } from '../Button';
2020
import { Icon } from '../Icon';
21+
import { usePortalRoot } from '../../context';
2122
import {
2223
buttonGroup,
2324
dropdownTriggerWrapper,
@@ -280,6 +281,9 @@ const ButtonGroupDropdown: React.FC<ButtonGroupDropdownProps> = ({
280281
}
281282
}, [closeOnClick, isControlled, onOpenChange]);
282283

284+
// Get portal root from context (for multi-window support)
285+
const portalRoot = usePortalRoot();
286+
283287
return (
284288
<>
285289
<div
@@ -302,7 +306,7 @@ const ButtonGroupDropdown: React.FC<ButtonGroupDropdownProps> = ({
302306

303307
{/* Portal with transition support for exit animations */}
304308
{isMounted && (
305-
<FloatingPortal>
309+
<FloatingPortal root={portalRoot}>
306310
<FloatingFocusManager context={context} modal={false}>
307311
<div
308312
ref={refs.setFloating}

packages/baukasten/src/components/ContextMenu/ContextMenu.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@floating-ui/react';
1414
import { Menu } from '../Menu';
1515
import type { MenuProps } from '../Menu';
16+
import { usePortalRoot } from '../../context';
1617
import { menuWrapper, styledMenu, triggerWrapper } from './ContextMenu.css';
1718

1819
/**
@@ -169,6 +170,9 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
169170
setIsOpen(false);
170171
}, []);
171172

173+
// Get portal root from context (for multi-window support)
174+
const portalRoot = usePortalRoot();
175+
172176
return (
173177
<>
174178
<div className={triggerWrapper} onContextMenu={handleContextMenu}>
@@ -177,7 +181,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
177181

178182
{/* Portal with transition support for exit animations */}
179183
{isMounted && (
180-
<FloatingPortal>
184+
<FloatingPortal root={portalRoot}>
181185
<FloatingFocusManager context={context} modal={false}>
182186
<div
183187
ref={refs.setFloating}

packages/baukasten/src/components/Dropdown/Dropdown.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
FloatingFocusManager,
1616
type Placement,
1717
} from '@floating-ui/react';
18+
import { usePortalRoot } from '../../context';
1819
import { dropdownWrapper, triggerWrapper, portalContent } from './Dropdown.css';
1920

2021
/**
@@ -238,6 +239,9 @@ export const Dropdown: React.FC<DropdownProps> = ({
238239
}
239240
}, [closeOnClick, isControlled, onOpenChange]);
240241

242+
// Get portal root from context (for multi-window support)
243+
const portalRoot = usePortalRoot();
244+
241245
return (
242246
<>
243247
<div
@@ -252,7 +256,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
252256

253257
{/* Portal with transition support for exit animations */}
254258
{isMounted && (
255-
<FloatingPortal>
259+
<FloatingPortal root={portalRoot}>
256260
<FloatingFocusManager context={context} modal={modal}>
257261
<div
258262
ref={refs.setFloating}

packages/baukasten/src/components/Select/Select.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '@floating-ui/react';
1717
import { type Size } from '../../styles';
1818
import { Icon } from '../Icon';
19+
import { usePortalRoot } from '../../context';
1920
import * as styles from './Select.css';
2021

2122
// Floating UI numeric values (required by Floating UI API)
@@ -535,6 +536,9 @@ export function Select<T = string>({
535536

536537
// Floating UI handles click-outside and position updates automatically via autoUpdate and useDismiss
537538

539+
// Get portal root from context (for multi-window support)
540+
const portalRoot = usePortalRoot();
541+
538542
const containerClassName = className
539543
? `${styles.selectContainer({ fullWidth })} ${className}`
540544
: styles.selectContainer({ fullWidth });
@@ -574,7 +578,7 @@ export function Select<T = string>({
574578
</button>
575579

576580
{isMounted && (
577-
<FloatingPortal>
581+
<FloatingPortal root={portalRoot}>
578582
<div
579583
ref={refs.setFloating}
580584
className={`${styles.floatingWrapper}${dropdownClassName ? ` ${dropdownClassName}` : ''}`}

packages/baukasten/src/components/Tooltip/Tooltip.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
type Placement,
1717
} from '@floating-ui/react';
1818
import React, { useRef, useState } from 'react';
19+
import { usePortalRoot } from '../../context';
1920
import * as styles from './Tooltip.css';
2021

2122
// Floating UI numeric values (required by Floating UI API)
@@ -207,6 +208,8 @@ export const Tooltip: React.FC<TooltipProps> = ({
207208
error: 'var(--bk-color-danger)',
208209
info: 'var(--bk-color-info)',
209210
}[variant];
211+
// Get portal root from context (for multi-window support)
212+
const portalRoot = usePortalRoot();
210213

211214
return (
212215
<>
@@ -219,7 +222,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
219222
</div>
220223

221224
{isMounted && (
222-
<FloatingPortal>
225+
<FloatingPortal root={portalRoot}>
223226
<div
224227
ref={refs.setFloating}
225228
style={{

0 commit comments

Comments
 (0)