Skip to content

Commit 7796e43

Browse files
author
DavidQ
committed
Polish Input Mapping V2 selection highlighting and context handling - PR_26140_103-polish-input-mapping-v2-selection-and-context
1 parent 8a6c93e commit 7796e43

9 files changed

Lines changed: 235 additions & 53 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Input Mapping V2 Selection And Context Report
2+
3+
PR: `PR_26140_103-polish-input-mapping-v2-selection-and-context`
4+
5+
## Source Of Truth
6+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before execution.
7+
- Read `.codex/skills/repo-build/SKILL.md`.
8+
- The active `docs/pr/BUILD_PR.md` still describes an older Level 18 rebase workflow, so the explicit PR103 user request was used as the build source of truth.
9+
- No schema changes were required.
10+
- No sample JSON files were touched.
11+
12+
## Changes
13+
- Removed the Captured Mappings `Delete All` button and its UI wiring completely.
14+
- Kept `Delete Action` and `Delete Mappings` in the Captured Mappings action area and centered them horizontally.
15+
- Preserved the reduced `6px` border-radius styling for Captured Mappings action buttons.
16+
- Added a `Disable Context` checkbox on the right side of the Capture accordion header.
17+
- Scoped context-menu suppression to the `.input-mapping-v2` tool workspace only when `Disable Context` is enabled.
18+
- Restored normal context-menu behavior when the checkbox is disabled.
19+
- Strengthened selected action input highlighting in the Capture area:
20+
- selected keyboard mappings show highlighted used keyboard controls,
21+
- selected mouse mappings show highlighted used mouse controls,
22+
- selected game controller mappings show highlighted used game controller controls,
23+
- combo mappings expose per-device used controls where they can be parsed from the captured combo.
24+
25+
## Playwright Impact
26+
Playwright impacted: Yes.
27+
28+
Behavior validated:
29+
- `Delete All` is absent.
30+
- `Delete Action` and `Delete Mappings` are horizontally centered.
31+
- Selected tiles highlight associated keyboard, mouse, and game controller controls in the Capture area.
32+
- The `Disable Context` checkbox appears in the Capture accordion header.
33+
- Enabling `Disable Context` prevents `contextmenu` default behavior inside Input Mapping V2.
34+
- Enabling `Disable Context` does not prevent `contextmenu` default behavior outside the tool workspace.
35+
- Disabling `Disable Context` restores `contextmenu` default behavior inside Input Mapping V2.
36+
37+
Expected pass behavior:
38+
- The focused Input Mapping V2 Playwright assertions pass.
39+
- `npm run test:workspace-v2` passes.
40+
- Right-click suppression is opt-in and scoped to the Input Mapping V2 workspace root.
41+
42+
Expected fail behavior:
43+
- Tests fail if `Delete All` remains visible, action buttons are not centered, selected input chips do not update with tile selection, or context-menu suppression leaks outside the tool workspace.
44+
45+
## Validation
46+
- Targeted syntax/import validation for changed Input Mapping V2 files: PASS.
47+
- Targeted syntax validation for carried engine/input files and InputService test module: PASS.
48+
- Targeted InputService import/unit run: PASS.
49+
- Focused Input Mapping V2 Playwright run: PASS, 2 tests.
50+
- `npm run test:workspace-v2`: PASS, 61 tests.
51+
- Playwright V8 coverage report for changed runtime JS: PASS, advisory only.
52+
- `git diff --check`: PASS, line-ending warnings only.
53+
- HTML inline script/style/handler scan: PASS, no matches.
54+
- Sample/JSON diff scan: PASS, no changed sample or JSON files.
55+
- Full samples smoke test: not run, per request and project instructions.
56+
57+
## Manual Validation
58+
1. Open Workspace Manager V2 and launch Input Mapping V2.
59+
2. Confirm the Captured Mappings action area shows only `Delete Action` and `Delete Mappings`, centered below the separator.
60+
3. Create or select a tile with keyboard, mouse, and game controller mappings.
61+
4. Confirm the selected tile visibly highlights the associated used controls in the Capture area.
62+
5. Enable `Disable Context`, right-click inside the Input Mapping V2 workspace, and confirm the browser context menu is suppressed.
63+
6. Right-click outside the Input Mapping V2 workspace and confirm normal context-menu behavior is not suppressed.
64+
7. Disable `Disable Context`, right-click inside Input Mapping V2, and confirm normal context-menu behavior is restored.
65+
66+
## Out Of Scope
67+
- Schema changes.
68+
- Sample JSON alignment.
69+
- Full samples smoke test.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,9 +1583,31 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15831583
expect(actionOptions).toEqual([...actionOptions].sort((left, right) => left.localeCompare(right)));
15841584
expect(actionOptions).toEqual(expect.arrayContaining(["Move Left", "Confirm", "Cancel", "Fire", "Thrust", "Rotate Left", "Rotate Right", "Pause", "Select", "Start"]));
15851585
await expect(page.locator("#inputMappingV2ResetActionsButton")).toHaveCount(0);
1586-
await expect(page.locator("#inputMappingV2ClearActionButton")).toHaveText("Delete All");
1586+
await expect(page.locator("#inputMappingV2ClearActionButton")).toHaveCount(0);
15871587
await expect(page.locator("#actionSetupContent .input-mapping-v2__button-row button")).toHaveText(["Add", "Delete"]);
1588-
await expect(page.locator("#mappingPanelContent #inputMappingV2ClearActionButton")).toBeVisible();
1588+
await expect(page.locator(".input-mapping-v2__capture-header #inputMappingV2DisableContextCheckbox")).toBeVisible();
1589+
expect(await page.locator(".input-mapping-v2").evaluate((root) => {
1590+
const event = new MouseEvent("contextmenu", { bubbles: true, button: 2, cancelable: true });
1591+
const dispatchResult = root.dispatchEvent(event);
1592+
return { defaultPrevented: event.defaultPrevented, dispatchResult };
1593+
})).toEqual({ defaultPrevented: false, dispatchResult: true });
1594+
await page.locator("#inputMappingV2DisableContextCheckbox").check();
1595+
expect(await page.locator(".input-mapping-v2").evaluate((root) => {
1596+
const event = new MouseEvent("contextmenu", { bubbles: true, button: 2, cancelable: true });
1597+
const dispatchResult = root.dispatchEvent(event);
1598+
return { defaultPrevented: event.defaultPrevented, dispatchResult };
1599+
})).toEqual({ defaultPrevented: true, dispatchResult: false });
1600+
expect(await page.evaluate(() => {
1601+
const event = new MouseEvent("contextmenu", { bubbles: true, button: 2, cancelable: true });
1602+
const dispatchResult = document.body.dispatchEvent(event);
1603+
return { defaultPrevented: event.defaultPrevented, dispatchResult };
1604+
})).toEqual({ defaultPrevented: false, dispatchResult: true });
1605+
await page.locator("#inputMappingV2DisableContextCheckbox").uncheck();
1606+
expect(await page.locator(".input-mapping-v2").evaluate((root) => {
1607+
const event = new MouseEvent("contextmenu", { bubbles: true, button: 2, cancelable: true });
1608+
const dispatchResult = root.dispatchEvent(event);
1609+
return { defaultPrevented: event.defaultPrevented, dispatchResult };
1610+
})).toEqual({ defaultPrevented: false, dispatchResult: true });
15891611
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='gameController'] #inputMappingV2RumbleFeedbackCheckbox")).toBeVisible();
15901612
await page.locator("#inputMappingV2RumbleFeedbackCheckbox").check();
15911613
await expect(page.locator("#statusLog")).toHaveValue(/WARN Gamepad rumble unavailable:/);
@@ -1594,17 +1616,19 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15941616
expect(await page.locator("#previewOutput").evaluate((node) => getComputedStyle(node).overflowY)).toBe("auto");
15951617
const capturedMappingsActionPlacement = await page.locator("#mappingPanelContent").evaluate((content) => ({
15961618
actionButtonTexts: Array.from(content.querySelectorAll(".input-mapping-v2__mapping-actions button")).map((button) => button.textContent),
1619+
justifyContent: getComputedStyle(content.querySelector(".input-mapping-v2__mapping-actions")).justifyContent,
15971620
lastElementClass: content.lastElementChild?.className,
15981621
previousElementClass: content.lastElementChild?.previousElementSibling?.className,
15991622
previousElementTag: content.lastElementChild?.previousElementSibling?.tagName,
16001623
radii: Array.from(content.querySelectorAll(".input-mapping-v2__mapping-actions button")).map((button) => getComputedStyle(button).borderRadius)
16011624
}));
16021625
expect(capturedMappingsActionPlacement).toEqual({
1603-
actionButtonTexts: ["Delete Action", "Delete Mappings", "Delete All"],
1626+
actionButtonTexts: ["Delete Action", "Delete Mappings"],
1627+
justifyContent: "center",
16041628
lastElementClass: "input-mapping-v2__mapping-actions",
16051629
previousElementClass: "input-mapping-v2__mapping-actions-separator",
16061630
previousElementTag: "HR",
1607-
radii: ["6px", "6px", "6px"]
1631+
radii: ["6px", "6px"]
16081632
});
16091633
const captureFlowLayout = await page.locator("#captureInputContent").evaluate((content) => {
16101634
const rectFor = (selector) => {
@@ -1815,6 +1839,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18151839
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Keyboard KeyA Press");
18161840
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Mouse Middle Button Click");
18171841
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Mouse Left Button Drag Release");
1842+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='keyboard'] .input-mapping-v2__used-control", { hasText: "Keyboard KeyA Press" })).toHaveCount(1);
1843+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='mouse'] .input-mapping-v2__used-control", { hasText: "Mouse Left Button Drag Release" })).toHaveCount(1);
18181844
await expect(page.locator("#inspectorOutput")).toContainText('"action": "moveLeft"');
18191845
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyA"');
18201846
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyD"');
@@ -1838,6 +1864,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18381864
await page.keyboard.press("KeyB");
18391865
await expect(page.locator("#statusLog")).toHaveValue(/OK Keyboard KeyB mapped to Confirm\./);
18401866
await expect(page.locator("#captureInputContent")).not.toContainText("Keyboard KeyB mapped to Confirm.");
1867+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='keyboard'] .input-mapping-v2__used-control", { hasText: "Keyboard KeyB Press" })).toHaveCount(1);
1868+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='keyboard'] .input-mapping-v2__used-control", { hasText: "Keyboard KeyA Press" })).toHaveCount(0);
18411869
await page.locator("#inputMappingV2DeleteMappingsButton").click();
18421870
await expect(page.locator(".input-mapping-v2__mapping-card", { hasText: "Confirm" })).toHaveCount(1);
18431871
await expect(page.locator(".input-mapping-v2__mapping-card", { hasText: "Confirm" })).toContainText("No inputs captured.");
@@ -1958,6 +1986,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
19581986
await expect(page.locator("#previewOutput")).not.toContainText("(Gamepad 1)");
19591987
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).toHaveClass(/has-used-input/);
19601988
await expect(page.locator("#inputMappingV2UsedInputHighlights")).toContainText("Game Controller X Button");
1989+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='gameController'] .input-mapping-v2__used-control", { hasText: "Game Controller X Button" })).toHaveCount(1);
19611990
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Game Controller" }).filter({ hasText: "X" })).toHaveAttribute("title", "Logitech RumblePad 2 USB\nSTANDARD GAMEPAD\nVendor: 046d Product: c218\nX");
19621991
await expect(page.locator("#inspectorOutput")).toContainText('"source": "gamepad"');
19631992
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "Pad1:Button2"');
@@ -2013,10 +2042,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
20132042
await page.locator("#toolCopyJsonButton").click();
20142043
expect(await page.evaluate(() => window.__inputMappingV2Clipboard)).toContain('"toolId": "input-mapping-v2"');
20152044
await expect(page.locator("#statusLog")).toHaveValue(/OK Mapping JSON copied\./);
2016-
await page.locator("#inputMappingV2ClearActionButton").click();
2017-
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(0);
2018-
await expect(page.locator("#previewOutput")).toContainText("No inputs captured yet.");
2019-
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted \d+ captured mappings\./);
2045+
await expect(page.locator("#inputMappingV2ClearActionButton")).toHaveCount(0);
20202046
expect(pageErrors).toEqual([]);
20212047
} finally {
20222048
await workspaceV2CoverageReporter.stop(page);

tools/input-mapping-v2/index.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,16 @@ <h2 class="tools-platform-frame__eyebrow">Input Sources and Action Mapping</h2>
102102
</section>
103103

104104
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true">
105-
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="captureInputContent">
105+
<div class="accordion-v2__header input-mapping-v2__capture-header" aria-expanded="true" aria-controls="captureInputContent">
106106
<span>Capture</span>
107-
<span class="accordion-v2__icon" aria-hidden="true">+</span>
108-
</button>
107+
<div class="input-mapping-v2__capture-header-actions">
108+
<label class="input-mapping-v2__context-toggle" for="inputMappingV2DisableContextCheckbox">
109+
<input id="inputMappingV2DisableContextCheckbox" type="checkbox">
110+
<span>Disable Context</span>
111+
</label>
112+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
113+
</div>
114+
</div>
109115
<div id="captureInputContent" class="accordion-v2__content">
110116
<p id="inputMappingV2SelectedActionLabel" class="tool-starter__hint">Selected action: Move Up</p>
111117
<div class="input-mapping-v2__button-row">
@@ -133,7 +139,6 @@ <h2 class="tools-platform-frame__eyebrow">Input Sources and Action Mapping</h2>
133139
<div class="input-mapping-v2__mapping-actions">
134140
<button id="inputMappingV2MappingDeleteActionButton" type="button">Delete Action</button>
135141
<button id="inputMappingV2DeleteMappingsButton" type="button">Delete Mappings</button>
136-
<button id="inputMappingV2ClearActionButton" type="button">Delete All</button>
137142
</div>
138143
</div>
139144
</section>

tools/input-mapping-v2/js/ToolStarterApp.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class ToolStarterApp {
1414
shell,
1515
state,
1616
statusLog,
17+
workspaceRoot,
1718
windowRef = window
1819
}) {
1920
this.accordions = accordions;
@@ -30,20 +31,23 @@ export class ToolStarterApp {
3031
this.shell = shell;
3132
this.state = state;
3233
this.statusLog = statusLog;
34+
this.workspaceRoot = workspaceRoot;
3335
this.window = windowRef;
3436
this.captureMode = "";
3537
this.activeGamepadIndex = null;
3638
this.comboCaptureInputs = [];
3739
this.rumbleFeedbackEnabled = false;
3840
this.captureTimeoutMs = 8000;
3941
this.captureTimeoutTimer = null;
42+
this.contextMenuDisabled = false;
4043
this.gamepadPollIntervalMs = 750;
4144
this.gamepadPollTimer = null;
4245
this.lastGamepadStatusSignature = "";
4346
this.enabledDeviceIds = new Set();
4447
this.enabledDevicesInitialized = false;
4548
this.handleGamepadConnectionChange = this.handleGamepadConnectionChange.bind(this);
4649
this.handleKeyDown = this.handleKeyDown.bind(this);
50+
this.handleContextMenu = this.handleContextMenu.bind(this);
4751
this.handleMouseDown = this.handleMouseDown.bind(this);
4852
this.handleWheel = this.handleWheel.bind(this);
4953
this.pollGamepadDevices = this.pollGamepadDevices.bind(this);
@@ -73,6 +77,7 @@ export class ToolStarterApp {
7377
onCaptureGamepad: (gamepadIndex) => this.startGamepadCapture(gamepadIndex),
7478
onCaptureKeyboard: () => this.startKeyboardCapture(),
7579
onCaptureMouse: () => this.startMouseCapture(),
80+
onDisableContextChanged: (isDisabled) => this.setContextMenuDisabled(isDisabled),
7681
onRefreshGamepads: () => this.refreshGamepads()
7782
});
7883
this.deviceList.mount({
@@ -90,10 +95,10 @@ export class ToolStarterApp {
9095
this.window.addEventListener("keydown", this.handleKeyDown, true);
9196
this.window.addEventListener("mousedown", this.handleMouseDown, true);
9297
this.window.addEventListener("wheel", this.handleWheel, true);
98+
this.workspaceRoot.addEventListener("contextmenu", this.handleContextMenu);
9399
this.statusLog.mount();
94100
this.preview.mount({
95101
onDeleteAction: () => this.deleteSelectedAction(),
96-
onDeleteAllMappings: () => this.deleteAllMappings(),
97102
onDeleteMappings: () => this.deleteSelectedMappings(),
98103
onSelectAction: (actionId) => this.selectAction(actionId)
99104
});
@@ -122,12 +127,6 @@ export class ToolStarterApp {
122127
this.refreshActions();
123128
}
124129

125-
deleteAllMappings() {
126-
const result = this.state.deleteAllMappings();
127-
this.statusLog[result.ok ? "ok" : "warn"](result.message);
128-
this.refreshActions();
129-
}
130-
131130
deleteSelectedMappings() {
132131
const result = this.state.deleteSelectedMappings();
133132
this.statusLog[result.ok ? "ok" : "warn"](result.message);
@@ -238,6 +237,12 @@ export class ToolStarterApp {
238237
this.tryCaptureComboGamepad();
239238
}
240239

240+
handleContextMenu(event) {
241+
if (this.contextMenuDisabled) {
242+
event.preventDefault();
243+
}
244+
}
245+
241246
tryCaptureActiveGamepad() {
242247
if (this.captureMode !== "gamepad" || !Number.isInteger(this.activeGamepadIndex)) {
243248
return false;
@@ -339,6 +344,11 @@ export class ToolStarterApp {
339344
}
340345
}
341346

347+
setContextMenuDisabled(isDisabled) {
348+
this.contextMenuDisabled = isDisabled;
349+
this.statusLog.ok(`Browser context menu ${isDisabled ? "disabled" : "enabled"} within Input Mapping V2 workspace.`);
350+
}
351+
342352
setDeviceEnabled(deviceId, isEnabled) {
343353
if (isEnabled) {
344354
this.enabledDeviceIds.add(deviceId);

tools/input-mapping-v2/js/bootstrap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ window.addEventListener("DOMContentLoaded", () => {
4949
captureKeyboardButton: requireElement("#inputMappingV2CaptureKeyboardButton"),
5050
captureMessage: requireElement("#inputMappingV2CaptureMessage"),
5151
captureMouseButton: requireElement("#inputMappingV2CaptureMouseButton"),
52+
disableContextCheckbox: requireElement("#inputMappingV2DisableContextCheckbox"),
5253
refreshGamepadsButton: requireElement("#inputMappingV2RefreshGamepadsButton"),
5354
selectedActionLabel: requireElement("#inputMappingV2SelectedActionLabel"),
5455
usedInputHighlights: requireElement("#inputMappingV2UsedInputHighlights")
@@ -69,13 +70,13 @@ window.addEventListener("DOMContentLoaded", () => {
6970
inspector: new InspectorControl(requireElement("#inspectorOutput")),
7071
preview: new PreviewPanelControl({
7172
deleteActionButton: requireElement("#inputMappingV2MappingDeleteActionButton"),
72-
deleteAllButton: requireElement("#inputMappingV2ClearActionButton"),
7373
deleteMappingsButton: requireElement("#inputMappingV2DeleteMappingsButton"),
7474
output: requireElement("#previewOutput")
7575
}),
7676
shell: new ToolStarterShellControl(),
7777
state: new InputMappingState(),
7878
statusLog,
79+
workspaceRoot: requireElement(".input-mapping-v2"),
7980
windowRef: window
8081
});
8182

0 commit comments

Comments
 (0)