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 @@
input.js loaded, the value no longer changes and the page scrolls normally.