mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-09 18:04:55 +00:00
fix(app): close a plugin picker when its trigger moves to the overflow
The measuring slot keeps toolbar plugin items mounted, so a picker that was open when the toolbar condensed lost its trigger but kept its popup, which portals to the overlay root. Mark the slot's items inactive while they are out of the row, the contract retained panels already use to close their popups.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
expectToolbarNeverCondensedOrClipped,
|
||||
recordToolbarFramesFromLoad,
|
||||
withComposerControlsExample,
|
||||
withPickerComposerItem,
|
||||
withToggleAndWatcherComposerItems,
|
||||
} from "../support/helpers/plugin-composer-items";
|
||||
|
||||
@@ -43,6 +44,17 @@ test("a toolbar without room moves a plugin toggle to the overflow sheet and bac
|
||||
});
|
||||
});
|
||||
|
||||
test("an open plugin picker closes when its trigger moves to the overflow", async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await withPickerComposerItem(page, async (composer) => {
|
||||
await composer.setWindowWidth(1000);
|
||||
await composer.openInlinePicker();
|
||||
await composer.setWindowWidth(880);
|
||||
await composer.expectToolbarWidthBetween(300, 395);
|
||||
await composer.expectPickerClosed();
|
||||
});
|
||||
});
|
||||
|
||||
test("the toolbar settles on one density without flipping as the window widens", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -90,6 +90,33 @@ export default function contribute(client) {
|
||||
return () => {};
|
||||
}`;
|
||||
|
||||
const PICKER_NAME = "Review state";
|
||||
const PICKER_PLUGIN_SOURCE = `import { Select } from "@getpaseo/plugin/client/ui";
|
||||
import { useState } from "react";
|
||||
|
||||
const OPTIONS = [
|
||||
{ value: "draft", label: "Draft" },
|
||||
{ value: "ready", label: "Ready" },
|
||||
];
|
||||
|
||||
function ReviewState() {
|
||||
const [value, setValue] = useState("draft");
|
||||
return (
|
||||
<Select
|
||||
title="${PICKER_NAME}"
|
||||
icon="CircleDot"
|
||||
value={value}
|
||||
options={OPTIONS}
|
||||
onValueChange={({ value: next }) => setValue(next)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function contribute(client) {
|
||||
client.addComposerItem({ id: "review-state", Component: ReviewState });
|
||||
return () => {};
|
||||
}`;
|
||||
|
||||
function toolbar(page: Page) {
|
||||
return page.getByTestId("agent-controls-toolbar").filter({ visible: true }).first();
|
||||
}
|
||||
@@ -136,18 +163,38 @@ function agentFeaturesButton(page: Page) {
|
||||
return page.getByRole("button", { name: "Open agent features", exact: true });
|
||||
}
|
||||
|
||||
interface ToolbarPluginComposer {
|
||||
setWindowWidth(width: number): Promise<void>;
|
||||
expectToolbarWidthBetween(min: number, max: number): Promise<void>;
|
||||
expectToggleInlineAtFullDensity(): Promise<void>;
|
||||
expectToggleInOverflow(): Promise<void>;
|
||||
closeOverflowSheet(): Promise<void>;
|
||||
startRecordingDensity(): Promise<void>;
|
||||
expectAtMostOneDensityChange(): Promise<void>;
|
||||
openInlinePicker(): Promise<void>;
|
||||
expectPickerClosed(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Runs `run` in an agent's composer with one icon toggle and one item that renders nothing. */
|
||||
export async function withToggleAndWatcherComposerItems(
|
||||
export function withToggleAndWatcherComposerItems(
|
||||
page: Page,
|
||||
run: (composer: {
|
||||
setWindowWidth(width: number): Promise<void>;
|
||||
expectToolbarWidthBetween(min: number, max: number): Promise<void>;
|
||||
expectToggleInlineAtFullDensity(): Promise<void>;
|
||||
expectToggleInOverflow(): Promise<void>;
|
||||
closeOverflowSheet(): Promise<void>;
|
||||
startRecordingDensity(): Promise<void>;
|
||||
expectAtMostOneDensityChange(): Promise<void>;
|
||||
}) => Promise<void>,
|
||||
run: (composer: ToolbarPluginComposer) => Promise<void>,
|
||||
) {
|
||||
return withToolbarPlugin(page, ONE_TOGGLE_PLUGIN_SOURCE, run);
|
||||
}
|
||||
|
||||
/** Runs `run` in an agent's composer with one plugin `Select` in the toolbar. */
|
||||
export function withPickerComposerItem(
|
||||
page: Page,
|
||||
run: (composer: ToolbarPluginComposer) => Promise<void>,
|
||||
) {
|
||||
return withToolbarPlugin(page, PICKER_PLUGIN_SOURCE, run);
|
||||
}
|
||||
|
||||
async function withToolbarPlugin(
|
||||
page: Page,
|
||||
pluginSource: string,
|
||||
run: (composer: ToolbarPluginComposer) => Promise<void>,
|
||||
) {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "composer-measured-items-" });
|
||||
const agent = await workspace.client.createAgent({
|
||||
@@ -165,7 +212,7 @@ export async function withToggleAndWatcherComposerItems(
|
||||
path.join(directory, "paseo-plugin.json"),
|
||||
JSON.stringify({ id: ONE_TOGGLE_PLUGIN_ID, requirements: pluginRequirements }),
|
||||
);
|
||||
await writeFile(path.join(directory, "index.client.tsx"), ONE_TOGGLE_PLUGIN_SOURCE);
|
||||
await writeFile(path.join(directory, "index.client.tsx"), pluginSource);
|
||||
try {
|
||||
await client.patchDaemonConfig({ pluginsEnabled: true });
|
||||
await client.installDirectoryPlugin(directory);
|
||||
@@ -229,6 +276,14 @@ export async function withToggleAndWatcherComposerItems(
|
||||
});
|
||||
expect(changes).toBeLessThanOrEqual(1);
|
||||
}),
|
||||
openInlinePicker: () =>
|
||||
test.step("open the plugin picker from the toolbar", async () => {
|
||||
await toolbar(page).getByRole("button", { name: PICKER_NAME, exact: true }).click();
|
||||
await expect(page.getByTestId("combobox-desktop-container")).toBeVisible();
|
||||
}),
|
||||
expectPickerClosed: () =>
|
||||
test.step("the plugin picker has closed", () =>
|
||||
expect(page.getByTestId("combobox-desktop-container")).toHaveCount(0)),
|
||||
});
|
||||
} finally {
|
||||
await client.removePlugin(ONE_TOGGLE_PLUGIN_ID);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { type LayoutChangeEvent, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { RetainedPanelActivity } from "@/components/retained-panel";
|
||||
import { TOOLBAR_CONTROL_GAP } from "@/components/ui/control-geometry";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { PluginComposerItems } from "@/plugins/composer-items";
|
||||
@@ -29,7 +30,7 @@ export function usePluginToolbarWidth(): {
|
||||
* is an arbitrary component, so its width is known only from rendering it, and it is always the
|
||||
* toolbar form that is measured: the width never depends on the density it decides, so moving the
|
||||
* items to the overflow sheet cannot change the answer. Out of the row, the same node stays
|
||||
* mounted and measured, unseen and out of reach.
|
||||
* mounted and measured, unseen and out of reach, and inactive so an open picker closes with it.
|
||||
*/
|
||||
export function PluginToolbarSlot({
|
||||
serverId,
|
||||
@@ -54,7 +55,9 @@ export function PluginToolbarSlot({
|
||||
importantForAccessibility={inline ? "auto" : "no-hide-descendants"}
|
||||
aria-hidden={!inline}
|
||||
>
|
||||
<PluginComposerItems serverId={serverId} surface="toolbar" />
|
||||
<RetainedPanelActivity active={inline}>
|
||||
<PluginComposerItems serverId={serverId} surface="toolbar" />
|
||||
</RetainedPanelActivity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user