Left-align menu entries, add a Dropdown field, scrollable and searchable menus
Menu entries are left-aligned and live in a ScrollContainer capped at 320 px, so a
long list (the parent picker holds every object of the scene) scrolls instead of
running off the screen. `Menu(items, { searchable: true })` puts a filter box above
the scroll area; Enter takes the first match. Filtering forces a relayout because
two queries with the same hit count leave the height unchanged, which would keep
the stale row positions.
Fields that open a menu are a new `Dropdown` widget: the value sits flush left and
the chevron is drawn flush right instead of being written into the label. A menu
opened below a field is never narrower than that field.
New debug hooks `overlayTexts()` / `overlayRect()` make the popup contents
assertable; e2e step 11 covers the cap, the scrolling and the search box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d54a620ebd
commit
15ffcd5739
16 files changed
+297
-39
No files matched your search
@@ -171,10 +171,10 @@ export async function graph(t) {
|
||||
await holding;
|
||||
await stop();
|
||||
|
||||
// 13g. Variables: "+ Variable ▾" → Number adds "var1" with default 0; undo removes it.
|
||||
// 13g. Variables: the "+ Variable" dropdown → Number adds "var1" with default 0; undo removes it.
|
||||
await select({ kind: 'asset', assetId: 'mover' });
|
||||
await sleep(150);
|
||||
await clickText('+ Variable ▾');
|
||||
await clickText('+ Variable');
|
||||
await clickText('Number');
|
||||
await sleep(100);
|
||||
check(
|
||||
|
||||
@@ -90,18 +90,33 @@ export async function parenting(t) {
|
||||
JSON.stringify(o2),
|
||||
);
|
||||
|
||||
// 14c. Parent via the inspector menu, then "Unparent" from the hierarchy context menu.
|
||||
// 14c. Parent via the inspector menu: the picker is searchable, Enter takes the single match.
|
||||
await clickText('Blue crate');
|
||||
await clickText('None ▾');
|
||||
const menuEntry = await locate('Red crate', true); // the popup is drawn last; the hierarchy row has the same text
|
||||
await click(menuEntry.x, menuEntry.y);
|
||||
await sleep(100);
|
||||
await clickText('None'); // the Parent dropdown, labelled with the current parent
|
||||
const entries = await json('__gm.overlayTexts()');
|
||||
check(
|
||||
'the parent picker offers a search box and lists the candidates',
|
||||
entries.includes('Search…') && entries.includes('None') && entries.includes('Red crate'),
|
||||
JSON.stringify(entries),
|
||||
);
|
||||
await typeText('red');
|
||||
await sleep(150);
|
||||
const filtered = await json('__gm.overlayTexts()');
|
||||
check(
|
||||
'typing filters the picker down to the matching entries',
|
||||
same(filtered, ['red', 'Red crate']),
|
||||
JSON.stringify(filtered),
|
||||
);
|
||||
await shot('30-parent-search');
|
||||
await press('Enter'); // takes the first match
|
||||
await sleep(150);
|
||||
o2 = await object('o2');
|
||||
check(
|
||||
'inspector Parent menu parents the object',
|
||||
o2.parentId === 'o1' && o2.position.x === 270,
|
||||
JSON.stringify(o2),
|
||||
);
|
||||
check('the picker closed after Enter', same(await json('__gm.overlayTexts()'), []));
|
||||
const blueRow2 = await locate('Blue crate');
|
||||
await click(blueRow2.x, blueRow2.y, 'right');
|
||||
await clickText('Unparent (move to top level)');
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function colliders(t) {
|
||||
'collider is drawn in the overlay layer',
|
||||
colObj && (await evaluate(`__gm.layerOf('${colObj.id}')`)) === 'overlay',
|
||||
);
|
||||
check('collider inspector shows the shape menu', Boolean(await evaluate(`__gm.locate('Box ▾')`)));
|
||||
check('collider inspector shows the shape menu', Boolean(await evaluate(`__gm.locate('Box')`)));
|
||||
await shot('21-collider');
|
||||
await undo();
|
||||
const colliderAssetGone = colAsset ? !(await asset(colAsset.id)) : false;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Menus that can grow with the project. Runs last: it fills the scene with throwaway objects
|
||||
* to make the parent picker overflow, which the earlier steps' object counts would not survive.
|
||||
*/
|
||||
export async function menus(t) {
|
||||
const { check, sleep, json, shot, click, locate, mouse, mutate } = t;
|
||||
|
||||
await mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
const source = scene.objects.find(o => o.id === 'o1');
|
||||
for (let i = 1; i <= 40; i++) {
|
||||
const copy = structuredClone(source);
|
||||
copy.id = 'filler' + i;
|
||||
copy.name = 'Filler ' + i;
|
||||
copy.parentId = '';
|
||||
scene.objects.push(copy);
|
||||
}
|
||||
__gm.state.select({ kind: 'object', sceneId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||||
await sleep(150);
|
||||
|
||||
// 18. A picker with 40+ entries is capped in height and stays fully on screen instead of
|
||||
// running off the bottom edge.
|
||||
// The dropdown sits right of the 64 px label column; locate() gives the label's text centre.
|
||||
const parentRow = await locate('Parent');
|
||||
await click(parentRow.x + 55, parentRow.y);
|
||||
const rect = await json('__gm.overlayRect()');
|
||||
const screenHeight = await json('window.innerHeight');
|
||||
check(
|
||||
'a long parent picker is capped instead of growing with the object count',
|
||||
rect && rect.h < 420,
|
||||
JSON.stringify(rect),
|
||||
);
|
||||
check(
|
||||
'the capped picker stays fully on screen',
|
||||
rect && rect.y >= 0 && rect.y + rect.h <= screenHeight,
|
||||
JSON.stringify({ rect, screenHeight }),
|
||||
);
|
||||
await shot('31-long-parent-menu');
|
||||
|
||||
// 18a. The entries scroll under the wheel; the search box above them does not move.
|
||||
const firstBefore = await locate('None', true);
|
||||
const searchBefore = await locate('Search…', true);
|
||||
await mouse('mouseWheel', rect.x + rect.w / 2, rect.y + rect.h / 2, 'none', { deltaX: 0, deltaY: 240 });
|
||||
await sleep(150);
|
||||
const firstAfter = await locate('None', true);
|
||||
const searchAfter = await locate('Search…', true);
|
||||
check(
|
||||
'the wheel scrolls the entries',
|
||||
firstAfter.y < firstBefore.y - 100,
|
||||
JSON.stringify({ firstBefore, firstAfter }),
|
||||
);
|
||||
check('the search box stays put while the entries scroll', searchAfter.y === searchBefore.y);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { parenting } from './07-parenting.mjs';
|
||||
import { colliders } from './08-colliders.mjs';
|
||||
import { containers } from './09-containers.mjs';
|
||||
import { textAndEnabled } from './10-text-and-enabled.mjs';
|
||||
import { menus } from './11-menus.mjs';
|
||||
|
||||
export const STEPS = [
|
||||
hierarchy,
|
||||
@@ -25,4 +26,5 @@ export const STEPS = [
|
||||
colliders,
|
||||
containers,
|
||||
textAndEnabled,
|
||||
menus,
|
||||
];
|
||||
Reference in new issue
Block a user