Skip to content

Commit 21a28b5

Browse files
committed
Fix Audio SFX slider focus and centralize control limits - PR_26145_008-audio-sfx-slider-focus-and-control-limits
1 parent 7ef5d50 commit 21a28b5

3 files changed

Lines changed: 165 additions & 30 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Audio / SFX Playground V2 Slider Focus and Control Limits
2+
3+
PR: `PR_26145_008-audio-sfx-slider-focus-and-control-limits`
4+
5+
## Scope
6+
7+
- Audited `tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js`.
8+
- Moved Audio / SFX slider `min`, `max`, `step`, and default values into the centralized `SLIDER_LIMITS` table in `SfxControlPanel.js`.
9+
- Removed scattered slider limit attributes from the Audio / SFX HTML; the control now applies them at mount.
10+
- Kept Sound Style as defaults/guidance only:
11+
- no slider range mutation
12+
- no waveform option disabling or hiding
13+
- Added range input focus retention after pointer, input, and change events so keyboard arrows continue adjusting the active slider.
14+
- Preserved current slider steps, including Duration, Release, and Sweep step `5`.
15+
16+
## Targeted Static Validation
17+
18+
- `node --check` over `tools/audio-sfx-playground-v2/js/**/*.js`: PASS
19+
- `JSON.parse` for `tools/schemas/tools/audio-sfx-playground-v2.schema.json`: PASS
20+
- HTML/CSS static guard:
21+
- no inline event handlers: PASS
22+
- no `<style>` blocks: PASS
23+
- no inline `<script>` blocks: PASS
24+
- CSS non-empty: PASS
25+
- `git diff --check -- tools/audio-sfx-playground-v2`: PASS
26+
- Git reported LF-to-CRLF working-copy warnings only.
27+
28+
## Targeted Node Validation
29+
30+
- `SfxControlPanel.js` includes centralized `SLIDER_LIMITS`: PASS
31+
- Duration limit remains `min=60`, `max=2000`, `step=5`, `defaultValue=180`: PASS
32+
- Release limit remains `min=20`, `max=700`, `step=5`, `defaultValue=90`: PASS
33+
- Sweep limit remains `min=-1200`, `max=1200`, `step=5`, `defaultValue=700`: PASS
34+
- Mount applies centralized slider limits to the DOM inputs: PASS
35+
- Sound Style selection does not change slider limits: PASS
36+
- Sound Style selection does not disable waveform select: PASS
37+
- Slider pointer handler calls focus on the active range input: PASS
38+
39+
## Focused Playwright Validation
40+
41+
Ran a focused Playwright validation through a temporary local HTTP server using local browser binaries from `.ms-playwright`.
42+
43+
- Tool launches without console errors: PASS
44+
- Runtime-applied slider limits are present on all range inputs: PASS
45+
- Duration, Release, and Sweep keep step `5`: PASS
46+
- Clicking Duration keeps focus on `#durationInput`: PASS
47+
- Pressing ArrowRight after clicking Duration adjusts by `5`: PASS
48+
- Clicking Sweep keeps focus on `#pitchSweepInput`: PASS
49+
- Pressing ArrowRight after clicking Sweep adjusts by `5`: PASS
50+
- Dragging Release keeps focus on `#releaseInput`: PASS
51+
- Pressing ArrowRight after dragging Release adjusts by `5`: PASS
52+
- Sound Style selection does not mutate slider limits: PASS
53+
- Sound Style selection does not disable or hide waveform options: PASS
54+
- All waveforms remain selectable after style selection: PASS
55+
56+
## V8 Coverage
57+
58+
- `(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - covered by focused slider focus Playwright validation`
59+
60+
Coverage is advisory per project instructions.
61+
62+
## Workspace V2
63+
64+
Command:
65+
66+
```powershell
67+
$env:PLAYWRIGHT_BROWSERS_PATH='.ms-playwright'; npm.cmd run test:workspace-v2
68+
```
69+
70+
Result: FAIL.
71+
72+
Observed result:
73+
74+
- 72 tests discovered.
75+
- 68 passed.
76+
- 4 failed:
77+
- Input Mapping V2 mouse drag visual capture state test.
78+
- Object Vector Studio V2 layout shell/schema-only palette gate test.
79+
- Object Vector Studio V2 compact geometry layout/selected palette state test.
80+
- Asset Manager V2 temporary UAT manifest/session context test timed out.
81+
82+
The observed failures are outside the Audio / SFX Playground V2 scope.
83+
84+
## Full Samples Smoke
85+
86+
Skipped per request because this PR only impacts Audio / SFX Playground V2 controls.

tools/audio-sfx-playground-v2/index.html

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,32 +95,32 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
9595
</label>
9696
<label class="tool-starter__field" for="frequencyInput" data-density-control="frequency">
9797
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Sets the starting pitch before any sweep is applied.">Frequency</span>
98-
<input id="frequencyInput" type="range" min="80" max="1800" step="1">
98+
<input id="frequencyInput" type="range">
9999
<output id="frequencyValue" for="frequencyInput"></output>
100100
</label>
101101
<label class="tool-starter__field" for="durationInput" data-density-control="duration">
102102
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls the total playback length of the effect.">Duration</span>
103-
<input id="durationInput" type="range" min="60" max="2000" step="5">
103+
<input id="durationInput" type="range">
104104
<output id="durationValue" for="durationInput"></output>
105105
</label>
106106
<label class="tool-starter__field" for="attackInput" data-density-control="attack">
107107
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls how quickly the sound reaches full volume.">Attack</span>
108-
<input id="attackInput" type="range" min="0" max="250" step="5">
108+
<input id="attackInput" type="range">
109109
<output id="attackValue" for="attackInput"></output>
110110
</label>
111111
<label class="tool-starter__field" for="releaseInput" data-density-control="release">
112112
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls how long the sound fades out at the end.">Release</span>
113-
<input id="releaseInput" type="range" min="20" max="700" step="5">
113+
<input id="releaseInput" type="range">
114114
<output id="releaseValue" for="releaseInput"></output>
115115
</label>
116116
<label class="tool-starter__field" for="volumeInput" data-density-control="volume">
117117
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Sets the playback loudness for audition and export.">Volume</span>
118-
<input id="volumeInput" type="range" min="0" max="1" step="0.01">
118+
<input id="volumeInput" type="range">
119119
<output id="volumeValue" for="volumeInput"></output>
120120
</label>
121121
<label class="tool-starter__field" for="pitchSweepInput" data-density-control="sweep">
122122
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Bends pitch over time in cents, positive upward and negative downward.">Sweep</span>
123-
<input id="pitchSweepInput" type="range" min="-1200" max="1200" step="5">
123+
<input id="pitchSweepInput" type="range">
124124
<output id="pitchSweepValue" for="pitchSweepInput"></output>
125125
</label>
126126
<label class="tool-starter__toggle" for="noiseInput">
@@ -129,17 +129,17 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
129129
</label>
130130
<label class="tool-starter__field" for="noiseAmountInput" data-density-control="noiseAmount">
131131
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Sets how loud the arcade transient is when the noise source or layer is enabled.">Amount</span>
132-
<input id="noiseAmountInput" type="range" min="0" max="1" step="0.01">
132+
<input id="noiseAmountInput" type="range">
133133
<output id="noiseAmountValue" for="noiseAmountInput"></output>
134134
</label>
135135
<label class="tool-starter__field" for="noiseDecayInput" data-density-control="noiseDecay">
136136
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls how quickly the burst fades out.">Decay</span>
137-
<input id="noiseDecayInput" type="range" min="20" max="600" step="5">
137+
<input id="noiseDecayInput" type="range">
138138
<output id="noiseDecayValue" for="noiseDecayInput"></output>
139139
</label>
140140
<label class="tool-starter__field" for="noiseFilterInput" data-density-control="noiseFilter">
141141
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Raises or lowers the transient brightness filter.">Brightness</span>
142-
<input id="noiseFilterInput" type="range" min="400" max="9000" step="50">
142+
<input id="noiseFilterInput" type="range">
143143
<output id="noiseFilterValue" for="noiseFilterInput"></output>
144144
</label>
145145
<p id="sfxValidationMessage" class="tool-starter__hint">Ready to audition.</p>

tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
1+
const SLIDER_LIMITS = Object.freeze({
2+
attackMs: Object.freeze({ min: 0, max: 250, step: 5, defaultValue: 5 }),
3+
durationMs: Object.freeze({ min: 60, max: 2000, step: 5, defaultValue: 180 }),
4+
frequencyHz: Object.freeze({ min: 80, max: 1800, step: 1, defaultValue: 880 }),
5+
noiseAmount: Object.freeze({ min: 0, max: 1, step: 0.01, defaultValue: 0.65 }),
6+
noiseDecayMs: Object.freeze({ min: 20, max: 600, step: 5, defaultValue: 95 }),
7+
noiseFilterHz: Object.freeze({ min: 400, max: 9000, step: 50, defaultValue: 5200 }),
8+
pitchSweepCents: Object.freeze({ min: -1200, max: 1200, step: 5, defaultValue: 700 }),
9+
releaseMs: Object.freeze({ min: 20, max: 700, step: 5, defaultValue: 90 }),
10+
volume: Object.freeze({ min: 0, max: 1, step: 0.01, defaultValue: 0.42 })
11+
});
12+
13+
const SLIDER_INPUTS = Object.freeze([
14+
Object.freeze({ soundKey: "attackMs", inputProperty: "attackInput" }),
15+
Object.freeze({ soundKey: "durationMs", inputProperty: "durationInput" }),
16+
Object.freeze({ soundKey: "frequencyHz", inputProperty: "frequencyInput" }),
17+
Object.freeze({ soundKey: "noiseAmount", inputProperty: "noiseAmountInput" }),
18+
Object.freeze({ soundKey: "noiseDecayMs", inputProperty: "noiseDecayInput" }),
19+
Object.freeze({ soundKey: "noiseFilterHz", inputProperty: "noiseFilterInput" }),
20+
Object.freeze({ soundKey: "pitchSweepCents", inputProperty: "pitchSweepInput" }),
21+
Object.freeze({ soundKey: "releaseMs", inputProperty: "releaseInput" }),
22+
Object.freeze({ soundKey: "volume", inputProperty: "volumeInput" })
23+
]);
24+
125
const DEFAULT_SOUND = Object.freeze({
2-
attackMs: 5,
3-
durationMs: 180,
4-
frequencyHz: 880,
26+
attackMs: SLIDER_LIMITS.attackMs.defaultValue,
27+
durationMs: SLIDER_LIMITS.durationMs.defaultValue,
28+
frequencyHz: SLIDER_LIMITS.frequencyHz.defaultValue,
529
name: "Coin",
630
noise: false,
7-
noiseAmount: 0.65,
8-
noiseDecayMs: 95,
9-
noiseFilterHz: 5200,
10-
pitchSweepCents: 700,
11-
releaseMs: 90,
12-
volume: 0.42,
31+
noiseAmount: SLIDER_LIMITS.noiseAmount.defaultValue,
32+
noiseDecayMs: SLIDER_LIMITS.noiseDecayMs.defaultValue,
33+
noiseFilterHz: SLIDER_LIMITS.noiseFilterHz.defaultValue,
34+
pitchSweepCents: SLIDER_LIMITS.pitchSweepCents.defaultValue,
35+
releaseMs: SLIDER_LIMITS.releaseMs.defaultValue,
36+
volume: SLIDER_LIMITS.volume.defaultValue,
1337
waveform: "square"
1438
});
1539

@@ -138,10 +162,9 @@ function toNumber(input) {
138162
return Number.parseFloat(input.value);
139163
}
140164

141-
function readRange(input) {
165+
function readRange(input, limits) {
142166
const value = toNumber(input);
143-
const min = Number.parseFloat(input.min);
144-
const max = Number.parseFloat(input.max);
167+
const { min, max } = limits;
145168
if (!Number.isFinite(value) || value < min || value > max) {
146169
return { ok: false, message: `${input.id} must be between ${min} and ${max}.` };
147170
}
@@ -204,6 +227,7 @@ export class SfxControlPanel {
204227
}
205228

206229
mount({ onAdd, onChange, onDelete }) {
230+
this.applySliderLimits();
207231
this.loadSound(DEFAULT_SOUND);
208232
this.addButton.addEventListener("click", onAdd);
209233
this.deleteButton.addEventListener("click", onDelete);
@@ -236,6 +260,31 @@ export class SfxControlPanel {
236260
onChange();
237261
});
238262
});
263+
this.sliderInputs().forEach((input) => {
264+
input.addEventListener("pointerdown", () => this.focusSlider(input));
265+
input.addEventListener("input", () => this.focusSlider(input));
266+
input.addEventListener("change", () => this.focusSlider(input));
267+
});
268+
}
269+
270+
sliderInputs() {
271+
return SLIDER_INPUTS.map((item) => this[item.inputProperty]);
272+
}
273+
274+
applySliderLimits() {
275+
SLIDER_INPUTS.forEach((item) => {
276+
const input = this[item.inputProperty];
277+
const limits = SLIDER_LIMITS[item.soundKey];
278+
input.min = String(limits.min);
279+
input.max = String(limits.max);
280+
input.step = String(limits.step);
281+
});
282+
}
283+
284+
focusSlider(input) {
285+
if (typeof input.focus === "function") {
286+
input.focus({ preventScroll: true });
287+
}
239288
}
240289

241290
loadSound(sound) {
@@ -297,15 +346,15 @@ export class SfxControlPanel {
297346
if (!ALLOWED_WAVEFORMS.has(this.waveformSelect.value)) {
298347
return { valid: false, message: `Unsupported waveform: ${this.waveformSelect.value}.` };
299348
}
300-
const frequency = readRange(this.frequencyInput);
301-
const duration = readRange(this.durationInput);
302-
const attack = readRange(this.attackInput);
303-
const release = readRange(this.releaseInput);
304-
const volume = readRange(this.volumeInput);
305-
const pitchSweep = readRange(this.pitchSweepInput);
306-
const noiseAmount = readRange(this.noiseAmountInput);
307-
const noiseDecay = readRange(this.noiseDecayInput);
308-
const noiseFilter = readRange(this.noiseFilterInput);
349+
const frequency = readRange(this.frequencyInput, SLIDER_LIMITS.frequencyHz);
350+
const duration = readRange(this.durationInput, SLIDER_LIMITS.durationMs);
351+
const attack = readRange(this.attackInput, SLIDER_LIMITS.attackMs);
352+
const release = readRange(this.releaseInput, SLIDER_LIMITS.releaseMs);
353+
const volume = readRange(this.volumeInput, SLIDER_LIMITS.volume);
354+
const pitchSweep = readRange(this.pitchSweepInput, SLIDER_LIMITS.pitchSweepCents);
355+
const noiseAmount = readRange(this.noiseAmountInput, SLIDER_LIMITS.noiseAmount);
356+
const noiseDecay = readRange(this.noiseDecayInput, SLIDER_LIMITS.noiseDecayMs);
357+
const noiseFilter = readRange(this.noiseFilterInput, SLIDER_LIMITS.noiseFilterHz);
309358
const failed = [frequency, duration, attack, release, volume, pitchSweep, noiseAmount, noiseDecay, noiseFilter].find((result) => !result.ok);
310359
if (failed) {
311360
return { valid: false, message: failed.message };

0 commit comments

Comments
 (0)