From a09c4cecef87bc25650d5e026b9baf5ef876d511 Mon Sep 17 00:00:00 2001 From: Brian Genisio Date: Fri, 29 May 2026 14:26:25 -0400 Subject: [PATCH] fix(input): stop mouse wheel from changing number input values Add optional input.js helper that blurs a focused .input[type="number"] on wheel so scrolling no longer mutates the value while the page still scrolls. Uses delegated, auto-initializing listener and documents usage. Co-authored-by: Cursor --- agents.md | 33 ++++++++++++----- components/input/README.md | 31 ++++++++++++++++ components/input/input.js | 72 ++++++++++++++++++++++++++++++++++++++ components/input/test.html | 3 ++ llms.txt | 3 +- 5 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 components/input/input.js diff --git a/agents.md b/agents.md index 8d6bda4..168119b 100644 --- a/agents.md +++ b/agents.md @@ -22,7 +22,7 @@ The CodeSignal Design System is a CSS-based design system organized into **Found - **Semantic over Primitive**: Always prefer semantic tokens (e.g., `--Colors-Text-Body-Default`) over base scale tokens - **Dark Mode Support**: All components automatically adapt to dark mode via `@media (prefers-color-scheme: dark)` -- **CSS-First**: Components are primarily CSS-based with minimal JavaScript (Dropdown, Numeric Slider, and Modal use JS) +- **CSS-First**: Components are primarily CSS-based with minimal JavaScript (Dropdown, Numeric Slider, Modal, Split Panel, and Horizontal Cards use JS; Input has an optional JS helper) - **Accessibility**: Components follow WCAG guidelines and support keyboard navigation --- @@ -69,11 +69,12 @@ The CodeSignal Design System is a CSS-based design system organized into **Found ```html ``` @@ -345,7 +346,22 @@ height: var(--UI-Input-md); ``` -**Dependencies:** colors.css, spacing.css, typography.css +**Optional Scroll Fix (JavaScript):** +By default, scrolling the mouse wheel over a focused `` changes its value. Load `input/input.js` to disable this. It auto-initializes and uses event delegation, covering existing and dynamically added number inputs: + +```html + +``` + +Or import and scope it manually: + +```javascript +import preventNumberInputScroll from '/design-system/components/input/input.js'; +preventNumberInputScroll(); // whole document (default) +const cleanup = preventNumberInputScroll(containerEl); // scoped; returns cleanup fn +``` + +**Dependencies:** colors.css, spacing.css, typography.css (input.js has no dependencies) --- @@ -1195,6 +1211,7 @@ design-system/ │ │ └── test.html │ ├── input/ │ │ ├── input.css +│ │ ├── input.js │ │ ├── README.md │ │ └── test.html │ ├── modal/ @@ -1249,7 +1266,7 @@ This provides: - **Design System Version**: Current - **Browser Support**: Modern browsers (Chrome, Firefox, Safari, Edge) - **CSS Features Used**: CSS Custom Properties, CSS Grid, Flexbox, CSS Masks -- **JavaScript**: ES6 Modules (Dropdown, Numeric Slider, and Modal components) +- **JavaScript**: ES6 Modules (Dropdown, Numeric Slider, Modal, Split Panel, and Horizontal Cards components; optional Input scroll-fix helper) --- diff --git a/components/input/README.md b/components/input/README.md index 30c0d5f..5d223ed 100644 --- a/components/input/README.md +++ b/components/input/README.md @@ -272,6 +272,34 @@ Number inputs (`type="number"`) include styled spinner buttons that: ### Focus Ring When focused, inputs display a subtle focus ring using the primary color with reduced opacity for better accessibility. +### Scroll-to-Change Fix (JavaScript) +By default, browsers change the value of a focused `` when the mouse wheel is scrolled over it. This is rarely the intended behavior and can silently corrupt user input. The optional `input.js` helper fixes this by blurring the number input on wheel, so the value stays put and the page keeps scrolling normally. + +#### Usage + +Load the module once anywhere on the page. It auto-initializes and uses event delegation, so it covers both existing and dynamically added `.input[type="number"]` fields: + +```html + +``` + +You can also import it and control scope manually: + +```javascript +import preventNumberInputScroll from '/design-system/components/input/input.js'; + +// Apply to the whole document (default; safe to call once). +preventNumberInputScroll(); + +// Or scope it to a specific container. +const cleanup = preventNumberInputScroll(document.querySelector('#my-form')); + +// Call the returned function to remove the listener. +cleanup(); +``` + +> **Note:** The fix only targets elements matching `.input[type="number"]`, so other inputs and page scrolling are unaffected. + ## Dark Mode The component automatically adapts to dark mode via the `@media (prefers-color-scheme: dark)` query. All states are optimized for both light and dark themes. @@ -283,3 +311,6 @@ This component relies on variables from: - `design-system/spacing/spacing.css` - `design-system/typography/typography.css` +The optional scroll-to-change fix is provided by: +- `design-system/components/input/input.js` (no CSS dependencies) + diff --git a/components/input/input.js b/components/input/input.js new file mode 100644 index 0000000..dbdc82d --- /dev/null +++ b/components/input/input.js @@ -0,0 +1,72 @@ +/** + * Input Component Helpers + * Matches CodeSignal Design System + * + * Fixes the default browser behavior where scrolling the mouse wheel over a + * focused `` increments/decrements its value. We blur the + * input on wheel so the value stays put and the page keeps scrolling normally. + * + * The handler is attached once via event delegation, so it covers both existing + * and dynamically added number inputs without re-initialization. + */ + +const NUMBER_INPUT_SELECTOR = '.input[type="number"]'; + +function handleWheel(event) { + const el = event.target; + if ( + el instanceof HTMLInputElement && + el.matches(NUMBER_INPUT_SELECTOR) && + document.activeElement === el + ) { + // Blurring during the wheel event (before the default action runs) cancels + // the value change because the input is no longer focused. + el.blur(); + } +} + +let documentDelegationActive = false; + +/** + * Prevent mouse-wheel scrolling from changing `.input[type="number"]` values. + * + * @param {Document|HTMLElement} [root=document] Scope for the listener. When + * `document` (the default), a single delegated listener covers every current + * and future number input. Pass a specific element to scope it instead. + * @returns {() => void} A cleanup function that removes the listener. + */ +export function preventNumberInputScroll(root = document) { + if (root === document) { + if (!documentDelegationActive) { + document.addEventListener('wheel', handleWheel, { passive: true }); + documentDelegationActive = true; + } + return () => { + document.removeEventListener('wheel', handleWheel, { passive: true }); + documentDelegationActive = false; + }; + } + + root.addEventListener('wheel', handleWheel, { passive: true }); + return () => root.removeEventListener('wheel', handleWheel, { passive: true }); +} + +function autoInit() { + preventNumberInputScroll(document); +} + +// Auto-initialize as soon as the module loads. +if (typeof document !== 'undefined') { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', autoInit); + } else { + autoInit(); + } +} + +export default preventNumberInputScroll; + +// Also make available globally for non-module usage. +if (typeof window !== 'undefined') { + window.preventNumberInputScroll = preventNumberInputScroll; +} diff --git a/components/input/test.html b/components/input/test.html index 43a2eec..6c33568 100644 --- a/components/input/test.html +++ b/components/input/test.html @@ -130,6 +130,7 @@

Text Input States

+
Try scrolling the mouse wheel over this field while it's focused. With input.js loaded, the value no longer changes and the page scrolls normally.
@@ -392,6 +393,8 @@

Radio in table cell (matrix)

+ + diff --git a/llms.txt b/llms.txt index 37bb687..13fe104 100644 --- a/llms.txt +++ b/llms.txt @@ -45,6 +45,7 @@ Also include Work Sans font from Google Fonts. - Types: `type="text"` or `type="number"` - States: `.hover`, `.focus`, `:disabled` - Example: `` +- Optional JS helper: load `input/input.js` (or `import preventNumberInputScroll`) to stop the mouse wheel from changing `type="number"` values when focused; auto-inits + uses delegation ### Checkbox - Base: `.input-checkbox` wrapper with `input[type="checkbox"]` @@ -136,7 +137,7 @@ design-system/ ├── dropdown/dropdown.css + dropdown.js ├── horizontal-cards/horizontal-cards.css + horizontal-cards.js ├── icons/icons.css - ├── input/input.css + ├── input/input.css + input.js (optional scroll-fix helper) ├── modal/modal.css + modal.js ├── numeric-slider/numeric-slider.css + numeric-slider.js ├── split-panel/split-panel.css + split-panel.js