From 3f3aae9083305f936b9a53db6ae3a81918660574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kuna?= <1282324+codecalm@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:44:05 +0200 Subject: [PATCH] Rename the `Stepper` quantity input to `NumberInput` (#3317) --- .changeset/number-input.md | 7 + .changeset/stepper.md | 7 - core/js/src/{stepper.ts => number-input.ts} | 44 +++--- core/js/tabler.ts | 4 +- .../{stepper.spec.ts => number-input.spec.ts} | 146 +++++++++--------- core/scss/_core.scss | 2 +- core/scss/_variables.scss | 22 +-- ...m-stepper.scss => _form-number-input.scss} | 52 +++---- .../forms/{stepper.mdx => number-input.mdx} | 72 ++++----- docs/lib/redirects.ts | 2 + preview/pages/billing.astro | 4 +- preview/pages/form-elements.astro | 14 +- .../{stepper.astro => number-input.astro} | 62 ++++---- preview/pages/settings-plan.astro | 4 +- .../{stepper.astro => number-input.astro} | 16 +- shared/data/docs.json | 4 +- shared/data/menu.json | 6 +- .../ui/{Stepper.astro => NumberInput.astro} | 8 +- 18 files changed, 240 insertions(+), 236 deletions(-) create mode 100644 .changeset/number-input.md delete mode 100644 .changeset/stepper.md rename core/js/src/{stepper.ts => number-input.ts} (91%) rename core/js/tests/unit/{stepper.spec.ts => number-input.spec.ts} (77%) rename core/scss/ui/forms/{_form-stepper.scss => _form-number-input.scss} (71%) rename docs/content/ui/forms/{stepper.mdx => number-input.mdx} (68%) rename preview/pages/{stepper.astro => number-input.astro} (62%) rename screenshots/pages/{stepper.astro => number-input.astro} (73%) rename shared/ui/{Stepper.astro => NumberInput.astro} (82%) diff --git a/.changeset/number-input.md b/.changeset/number-input.md new file mode 100644 index 000000000..dd9764d56 --- /dev/null +++ b/.changeset/number-input.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/preview": minor +"@tabler/docs": minor +--- + +Added the `NumberInput` component, a `.number-input` quantity input with buttons, bounds, hold-to-repeat and keyboard support. diff --git a/.changeset/stepper.md b/.changeset/stepper.md deleted file mode 100644 index 3649ce255..000000000 --- a/.changeset/stepper.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@tabler/core": minor -"@tabler/preview": minor -"@tabler/docs": minor ---- - -Added the `Stepper` component, a `.stepper` quantity input with buttons, bounds, hold-to-repeat and keyboard support. diff --git a/core/js/src/stepper.ts b/core/js/src/number-input.ts similarity index 91% rename from core/js/src/stepper.ts rename to core/js/src/number-input.ts index 4a0b1287a..485b64a8f 100644 --- a/core/js/src/stepper.ts +++ b/core/js/src/number-input.ts @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Tabler stepper.ts + * Tabler number-input.ts * Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE) * -------------------------------------------------------------------------- */ @@ -12,7 +12,7 @@ import { initAll } from './bootstrap/util/component-functions' import { isDisabled } from './bootstrap/util/index' import type { ElementSelector } from './bootstrap/types' -type StepperAction = 'increment' | 'decrement' +type NumberInputAction = 'increment' | 'decrement' type ComponentConfig = { /** lower bound; `null` reads the input's `min` attribute, no bound when that is missing too */ @@ -38,23 +38,25 @@ type ComponentConfigInput = Partial & Record * Constants */ -const NAME = 'stepper' +const NAME = 'numberInput' +// the attribute spelling: `data-bs-toggle="number-input"`, `data-bs-number-input-action` +const DATA_NAME = 'number-input' const DATA_KEY = `bs.${NAME}` const EVENT_KEY = `.${DATA_KEY}` const EVENT_CHANGE = `change${EVENT_KEY}` -const CLASS_NAME_STEPPER = 'stepper' +const CLASS_NAME_NUMBER_INPUT = 'number-input' const CLASS_NAME_BUTTON = 'btn btn-icon' -const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${NAME}"]` +const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${DATA_NAME}"], [data-tblr-toggle="${DATA_NAME}"]` const SELECTOR_INPUT = 'input' -const SELECTOR_ACTION = `[data-bs-${NAME}-action], [data-tblr-${NAME}-action]` +const SELECTOR_ACTION = `[data-bs-${DATA_NAME}-action], [data-tblr-${DATA_NAME}-action]` -const ATTRIBUTES_ACTION = [`data-bs-${NAME}-action`, `data-tblr-${NAME}-action`] +const ATTRIBUTES_ACTION = [`data-bs-${DATA_NAME}-action`, `data-tblr-${DATA_NAME}-action`] const ATTRIBUTE_ACTION = ATTRIBUTES_ACTION[0] -// `.form-control-sm` on a bare input becomes `.stepper-sm` on the rendered box +// `.form-control-sm` on a bare input becomes `.number-input-sm` on the rendered box const SIZES = ['sm', 'lg'] const KEY_UP = 'ArrowUp' @@ -96,12 +98,12 @@ const DefaultType: Record = { * all write to it, clamp to `min` / `max` and round to the precision of * `step`, so a form sees nothing but an ordinary field. * - * Initialised on a bare ``, the plugin wraps it in `.stepper` and - * renders the two buttons itself; initialised on a `.stepper` wrapper, it + * Initialised on a bare ``, the plugin wraps it in `.number-input` and + * renders the two buttons itself; initialised on a `.number-input` wrapper, it * uses the buttons found in the markup. */ -class Stepper extends BaseComponent { +class NumberInput extends BaseComponent { declare _element: HTMLElement declare _config: ComponentConfig _input: HTMLInputElement | null = null @@ -217,20 +219,20 @@ class Stepper extends BaseComponent { } // Private - // Puts a bare input into a `.stepper` box with a button on each side. The + // Puts a bare input into a `.number-input` box with a button on each side. The // buttons are empty: the stylesheet paints their icons. _renderWrapper(): void { const input = this._input! const wrapper = document.createElement('div') - wrapper.className = CLASS_NAME_STEPPER + wrapper.className = CLASS_NAME_NUMBER_INPUT for (const size of SIZES) { if (input.classList.contains(`form-control-${size}`)) { - wrapper.classList.add(`${CLASS_NAME_STEPPER}-${size}`) + wrapper.classList.add(`${CLASS_NAME_NUMBER_INPUT}-${size}`) } } - const button = (action: StepperAction, label: string): HTMLButtonElement => { + const button = (action: NumberInputAction, label: string): HTMLButtonElement => { const element = document.createElement('button') element.type = 'button' element.className = CLASS_NAME_BUTTON @@ -311,7 +313,7 @@ class Stepper extends BaseComponent { } } - _actionOf(button: HTMLElement): StepperAction | null { + _actionOf(button: HTMLElement): NumberInputAction | null { for (const name of ATTRIBUTES_ACTION) { const action = button.getAttribute(name) if (action === 'increment' || action === 'decrement') { @@ -370,7 +372,7 @@ class Stepper extends BaseComponent { } } - _runAction(action: StepperAction | null): void { + _runAction(action: NumberInputAction | null): void { if (action === 'increment') { this.increment() } else if (action === 'decrement') { @@ -554,8 +556,8 @@ class Stepper extends BaseComponent { * Data API implementation */ -// js-docs-start stepper-init -initAll(SELECTOR_DATA_TOGGLE, Stepper) -// js-docs-end stepper-init +// js-docs-start number-input-init +initAll(SELECTOR_DATA_TOGGLE, NumberInput) +// js-docs-end number-input-init -export default Stepper +export default NumberInput diff --git a/core/js/tabler.ts b/core/js/tabler.ts index b9ae8013c..3304d4f52 100644 --- a/core/js/tabler.ts +++ b/core/js/tabler.ts @@ -15,7 +15,7 @@ import './src/sortable' import './src/otp-input' import './src/sparkline' import './src/strength' -import './src/stepper' +import './src/number-input' // Re-export everything from bootstrap.ts (single source of truth) export * from './src/bootstrap' @@ -30,7 +30,7 @@ export { default as InputMask } from './src/input-mask' export { default as OtpInput } from './src/otp-input' export { default as Sortable } from './src/sortable' export { default as Sparkline } from './src/sparkline' -export { default as Stepper } from './src/stepper' +export { default as NumberInput } from './src/number-input' export { default as Strength } from './src/strength' export { default as SwitchIcon } from './src/switch-icon' diff --git a/core/js/tests/unit/stepper.spec.ts b/core/js/tests/unit/number-input.spec.ts similarity index 77% rename from core/js/tests/unit/stepper.spec.ts rename to core/js/tests/unit/number-input.spec.ts index 595116d03..05f8ce013 100644 --- a/core/js/tests/unit/stepper.spec.ts +++ b/core/js/tests/unit/number-input.spec.ts @@ -1,17 +1,17 @@ import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest' import { clearFixture, getFixture } from '../helpers/fixture' -import Stepper from '../../src/stepper' +import NumberInput from '../../src/number-input' const markup = (inputAttributes = 'value="1" min="0" max="5"', wrapperAttributes = ''): string => [ - `
`, - ' ', + `
`, + ' ', ` `, - ' ', + ' ', '
', ].join('') -describe('Stepper', () => { +describe('NumberInput', () => { let fixtureEl: HTMLElement beforeAll(() => { @@ -27,10 +27,10 @@ describe('Stepper', () => { vi.useRealTimers() }) - const stepper = (): HTMLElement => fixtureEl.querySelector('.stepper')! + const numberInput = (): HTMLElement => fixtureEl.querySelector('.number-input')! const input = (): HTMLInputElement => fixtureEl.querySelector('input')! - const decrementButton = (): HTMLButtonElement => fixtureEl.querySelector('[data-bs-stepper-action="decrement"]')! - const incrementButton = (): HTMLButtonElement => fixtureEl.querySelector('[data-bs-stepper-action="increment"]')! + const decrementButton = (): HTMLButtonElement => fixtureEl.querySelector('[data-bs-number-input-action="decrement"]')! + const incrementButton = (): HTMLButtonElement => fixtureEl.querySelector('[data-bs-number-input-action="increment"]')! const change = (value: string): void => { input().value = value @@ -45,28 +45,28 @@ describe('Stepper', () => { describe('NAME', () => { it('should return the plugin name', () => { - expect(Stepper.NAME).toBe('stepper') + expect(NumberInput.NAME).toBe('numberInput') }) }) describe('Default', () => { it('should expose the default config', () => { - expect(Stepper.Default.min).toBeNull() - expect(Stepper.Default.repeat).toBe(true) - expect(typeof Stepper.DefaultType.step).toBe('string') + expect(NumberInput.Default.min).toBeNull() + expect(NumberInput.Default.repeat).toBe(true) + expect(typeof NumberInput.DefaultType.step).toBe('string') }) }) describe('constructor', () => { it('should read the bounds from the input attributes', () => { - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) expect(instance._min).toBe(0) expect(instance._max).toBe(5) expect(instance._step).toBe(1) }) it('should let the config override the input attributes', () => { - const instance = new Stepper(stepper(), { min: 2, max: 10, step: 2 }) + const instance = new NumberInput(numberInput(), { min: 2, max: 10, step: 2 }) expect(instance._min).toBe(2) expect(instance._max).toBe(10) expect(instance._step).toBe(2) @@ -75,7 +75,7 @@ describe('Stepper', () => { it('should read the config from data attributes', () => { fixtureEl.innerHTML = markup('value="1"', 'data-bs-min="1" data-bs-max="3" data-bs-step="0.5"') - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) expect(instance._min).toBe(1) expect(instance._max).toBe(3) expect(instance._step).toBe(0.5) @@ -83,24 +83,24 @@ describe('Stepper', () => { it('should have no bounds when neither attributes nor config set them', () => { fixtureEl.innerHTML = markup('value="1"') - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) expect(instance._min).toBe(Number.NEGATIVE_INFINITY) expect(instance._max).toBe(Number.POSITIVE_INFINITY) }) it('should fall back to a step of 1 for a zero or negative step', () => { fixtureEl.innerHTML = markup('value="1" step="0"') - expect(new Stepper(stepper())._step).toBe(1) + expect(new NumberInput(numberInput())._step).toBe(1) }) it('should clamp the initial value', () => { fixtureEl.innerHTML = markup('value="9" min="0" max="5"') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(input().value).toBe('5') }) it('should give a text input the spinbutton role and a numeric inputmode', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) expect(input().getAttribute('role')).toBe('spinbutton') expect(input().getAttribute('inputmode')).toBe('numeric') expect(input().getAttribute('aria-valuemin')).toBe('0') @@ -110,27 +110,27 @@ describe('Stepper', () => { it('should use the decimal inputmode for a fractional step', () => { fixtureEl.innerHTML = markup('value="1" step="0.25"') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(input().getAttribute('inputmode')).toBe('decimal') }) it('should leave a number input alone', () => { fixtureEl.innerHTML = markup('type="number" value="1" min="0" max="5"').replace('type="text"', '') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(input().hasAttribute('role')).toBe(false) expect(input().hasAttribute('aria-valuenow')).toBe(false) }) it('should do nothing without an input', () => { - fixtureEl.innerHTML = '
' - const instance = new Stepper(stepper()) + fixtureEl.innerHTML = '
' + const instance = new NumberInput(numberInput()) expect(instance._input).toBeNull() expect(instance.getValue()).toBeNull() }) it('should disable the buttons of a disabled input', () => { fixtureEl.innerHTML = markup('value="1" disabled') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(decrementButton().disabled).toBe(true) expect(incrementButton().disabled).toBe(true) }) @@ -138,7 +138,7 @@ describe('Stepper', () => { describe('buttons', () => { it('should step the value on click', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) incrementButton().click() expect(input().value).toBe('2') decrementButton().click() @@ -148,7 +148,7 @@ describe('Stepper', () => { it('should disable the button at a bound', () => { fixtureEl.innerHTML = markup('value="4" min="0" max="5"') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(incrementButton().disabled).toBe(false) incrementButton().click() expect(input().value).toBe('5') @@ -157,7 +157,7 @@ describe('Stepper', () => { }) it('should never leave the bounds', () => { - const instance = new Stepper(stepper(), { max: 2 }) + const instance = new NumberInput(numberInput(), { max: 2 }) instance.increment() instance.increment() instance.increment() @@ -166,7 +166,7 @@ describe('Stepper', () => { it('should keep stepping while a button is held', () => { vi.useFakeTimers() - new Stepper(stepper(), { max: 100 }) + new NumberInput(numberInput(), { max: 100 }) incrementButton().dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerType: 'mouse' })) vi.advanceTimersByTime(400) @@ -185,7 +185,7 @@ describe('Stepper', () => { it('should stop a hold at a bound', () => { vi.useFakeTimers() - const instance = new Stepper(stepper(), { max: 3 }) + const instance = new NumberInput(numberInput(), { max: 3 }) incrementButton().dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerType: 'mouse' })) vi.advanceTimersByTime(400 + 80 * 5) @@ -195,7 +195,7 @@ describe('Stepper', () => { it('should not repeat when the option is off', () => { vi.useFakeTimers() - new Stepper(stepper(), { repeat: false, max: 100 }) + new NumberInput(numberInput(), { repeat: false, max: 100 }) incrementButton().dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerType: 'mouse' })) vi.advanceTimersByTime(2000) @@ -205,7 +205,7 @@ describe('Stepper', () => { describe('keyboard', () => { it('should step with the arrow keys and jump with Home and End', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) expect(keydown('ArrowUp').defaultPrevented).toBe(true) expect(input().value).toBe('2') keydown('ArrowDown') @@ -218,34 +218,34 @@ describe('Stepper', () => { it('should ignore Home and End without a bound', () => { fixtureEl.innerHTML = markup('value="1"') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(keydown('End').defaultPrevented).toBe(false) expect(input().value).toBe('1') }) it('should leave other keys alone', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) expect(keydown('a').defaultPrevented).toBe(false) }) }) describe('typed value', () => { it('should clamp a typed value on change', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) change('42') expect(input().value).toBe('5') expect(incrementButton().disabled).toBe(true) }) it('should clamp on blur as well', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) input().value = '125' input().dispatchEvent(new Event('blur')) expect(input().value).toBe('5') }) it('should fire nothing when an untouched field is left', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) const spy = vi.fn() input().addEventListener('change', spy) input().dispatchEvent(new Event('blur')) @@ -253,7 +253,7 @@ describe('Stepper', () => { }) it('should restore the previous value after a non-numeric edit', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) change('3') change('abc') expect(input().value).toBe('3') @@ -261,7 +261,7 @@ describe('Stepper', () => { it('should accept a decimal comma', () => { fixtureEl.innerHTML = markup('value="1" step="0.5"') - new Stepper(stepper()) + new NumberInput(numberInput()) change('2,5') expect(input().value).toBe('2.5') }) @@ -275,7 +275,7 @@ describe('Stepper', () => { } it('should drop a typed letter and keep a digit', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) expect(beforeInput('a').defaultPrevented).toBe(true) expect(beforeInput('7').defaultPrevented).toBe(false) expect(beforeInput('.').defaultPrevented).toBe(true) @@ -284,7 +284,7 @@ describe('Stepper', () => { it('should allow the sign and the separator when the bounds and step need them', () => { fixtureEl.innerHTML = markup('value="0" min="-5" step="0.5"') - new Stepper(stepper()) + new NumberInput(numberInput()) expect(beforeInput('-').defaultPrevented).toBe(false) expect(beforeInput('.').defaultPrevented).toBe(false) expect(beforeInput(',').defaultPrevented).toBe(false) @@ -292,7 +292,7 @@ describe('Stepper', () => { }) it('should keep only the digits of a pasted text', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) input().value = '' input().setSelectionRange(0, 0) expect(beforeInput('4 pcs', 'insertFromPaste').defaultPrevented).toBe(true) @@ -300,7 +300,7 @@ describe('Stepper', () => { }) it('should leave deletions alone', () => { - new Stepper(stepper()) + new NumberInput(numberInput()) expect(beforeInput('', 'deleteContentBackward').defaultPrevented).toBe(false) }) }) @@ -308,7 +308,7 @@ describe('Stepper', () => { describe('precision', () => { it('should round to the precision of the step', () => { fixtureEl.innerHTML = markup('value="0.1" step="0.1"') - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) instance.increment() instance.increment() expect(input().value).toBe('0.3') @@ -317,7 +317,7 @@ describe('Stepper', () => { it('should step an empty field from the lower bound', () => { fixtureEl.innerHTML = markup('value="" min="2" max="9"') - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) expect(instance.getValue()).toBeNull() expect(input().hasAttribute('aria-valuenow')).toBe(false) instance.increment() @@ -326,10 +326,10 @@ describe('Stepper', () => { }) describe('events', () => { - it('should fire change.bs.stepper with the values', () => { - const instance = new Stepper(stepper()) + it('should fire change.bs.numberInput with the values', () => { + const instance = new NumberInput(numberInput()) const spy = vi.fn() - stepper().addEventListener('change.bs.stepper', spy) + numberInput().addEventListener('change.bs.numberInput', spy) instance.increment() expect(spy).toHaveBeenCalledTimes(1) @@ -339,7 +339,7 @@ describe('Stepper', () => { }) it('should fire input and change on the field', () => { - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) const onInput = vi.fn() const onChange = vi.fn() input().addEventListener('input', onInput) @@ -351,9 +351,9 @@ describe('Stepper', () => { }) it('should not fire when the value does not change', () => { - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) const spy = vi.fn() - stepper().addEventListener('change.bs.stepper', spy) + numberInput().addEventListener('change.bs.numberInput', spy) instance.setValue(1) instance.decrement() @@ -364,7 +364,7 @@ describe('Stepper', () => { describe('setValue', () => { it('should set and clamp the value', () => { - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) instance.setValue(3) expect(input().value).toBe('3') instance.setValue('99') @@ -377,7 +377,7 @@ describe('Stepper', () => { describe('disabled', () => { it('should not step a disabled or readonly input', () => { fixtureEl.innerHTML = markup('value="1" readonly') - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) instance.increment() expect(input().value).toBe('1') expect(incrementButton().disabled).toBe(true) @@ -386,26 +386,26 @@ describe('Stepper', () => { describe('dispose', () => { it('should remove the listeners and the instance', () => { - const instance = new Stepper(stepper()) + const instance = new NumberInput(numberInput()) instance.dispose() incrementButton().click() expect(input().value).toBe('1') - expect(Stepper.getInstance(stepper())).toBeNull() + expect(NumberInput.getInstance(numberInput())).toBeNull() }) }) describe('bare input', () => { const bare = (attributes = 'value="1" min="0" max="5"'): HTMLInputElement => { - fixtureEl.innerHTML = `
` + fixtureEl.innerHTML = `
` return fixtureEl.querySelector('input')! } it('should wrap the input and render the two buttons', () => { const el = bare() - new Stepper(el) + new NumberInput(el) const wrapper = el.parentElement! - expect(wrapper.classList.contains('stepper')).toBe(true) + expect(wrapper.classList.contains('number-input')).toBe(true) expect(wrapper.parentElement!.tagName).toBe('FORM') expect([...wrapper.children].map((child) => child.tagName)).toEqual(['BUTTON', 'INPUT', 'BUTTON']) expect(decrementButton().getAttribute('aria-label')).toBe('Decrease') @@ -416,14 +416,14 @@ describe('Stepper', () => { it('should step through the rendered buttons', () => { const el = bare() - new Stepper(el) + new NumberInput(el) incrementButton().click() expect(el.value).toBe('2') }) it('should read the config from the input and label the buttons', () => { const el = bare('value="1" data-bs-max="2" data-bs-increment-label="More" data-bs-decrement-label="Fewer"') - new Stepper(el) + new NumberInput(el) expect(incrementButton().getAttribute('aria-label')).toBe('More') expect(decrementButton().getAttribute('aria-label')).toBe('Fewer') incrementButton().click() @@ -432,41 +432,41 @@ describe('Stepper', () => { }) it('should map the size of the input to the box', () => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const el = fixtureEl.querySelector('input')! - new Stepper(el) - expect(el.parentElement!.classList.contains('stepper-sm')).toBe(true) + new NumberInput(el) + expect(el.parentElement!.classList.contains('number-input-sm')).toBe(true) }) - it('should fire change.bs.stepper on the input', () => { + it('should fire change.bs.numberInput on the input', () => { const el = bare() - const instance = new Stepper(el) + const instance = new NumberInput(el) const spy = vi.fn() - el.addEventListener('change.bs.stepper', spy) + el.addEventListener('change.bs.numberInput', spy) instance.increment() expect(spy).toHaveBeenCalledTimes(1) }) it('should unwrap the input on dispose', () => { const el = bare() - new Stepper(el).dispose() + new NumberInput(el).dispose() expect(el.parentElement!.tagName).toBe('FORM') - expect(fixtureEl.querySelector('.stepper')).toBeNull() + expect(fixtureEl.querySelector('.number-input')).toBeNull() expect(fixtureEl.querySelectorAll('button')).toHaveLength(0) }) }) describe('data-api', () => { it('should initialise on getOrCreateInstance', () => { - const instance = Stepper.getOrCreateInstance(stepper()) - expect(instance).toBeInstanceOf(Stepper) - expect(Stepper.getOrCreateInstance(stepper())).toBe(instance) + const instance = NumberInput.getOrCreateInstance(numberInput()) + expect(instance).toBeInstanceOf(NumberInput) + expect(NumberInput.getOrCreateInstance(numberInput())).toBe(instance) }) it('should accept the data-tblr-* action attributes', () => { - fixtureEl.innerHTML = markup().replaceAll('data-bs-stepper-action', 'data-tblr-stepper-action') - new Stepper(stepper()) - fixtureEl.querySelector('[data-tblr-stepper-action="increment"]')!.click() + fixtureEl.innerHTML = markup().replaceAll('data-bs-number-input-action', 'data-tblr-number-input-action') + new NumberInput(numberInput()) + fixtureEl.querySelector('[data-tblr-number-input-action="increment"]')!.click() expect(input().value).toBe('2') }) }) diff --git a/core/scss/_core.scss b/core/scss/_core.scss index e00cf61aa..54ae80f4e 100644 --- a/core/scss/_core.scss +++ b/core/scss/_core.scss @@ -69,7 +69,7 @@ @forward 'ui/forms/form-custom'; @forward 'ui/forms/form-check'; @forward 'ui/forms/form-otp'; -@forward 'ui/forms/form-stepper'; +@forward 'ui/forms/form-number-input'; @forward 'ui/forms/validation'; @forward 'ui/forms/strength'; @forward 'ui/legend'; diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index d308f1d29..2a7534364 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -1226,24 +1226,24 @@ $otp-slot-box-shadow: var(--shadow-input) !default; $otp-separator-color: var(--secondary) !default; // scss-docs-end otp-variables -// Stepper -// scss-docs-start stepper-variables -$stepper-input-width: 3.5rem !default; -$stepper-input-widths: ( +// NumberInput +// scss-docs-start number-input-variables +$number-input-input-width: 3.5rem !default; +$number-input-input-widths: ( 'sm': 3rem, 'lg': 4rem, ) !default; -$stepper-icon-sizes: ( +$number-input-icon-sizes: ( 'sm': $input-btn-icon-size-sm, 'lg': $input-btn-icon-size-lg, ) !default; -$stepper-bg: $input-bg !default; -$stepper-border-color: var(--border-color) !default; -$stepper-border-radius: $input-border-radius !default; +$number-input-bg: $input-bg !default; +$number-input-border-color: var(--border-color) !default; +$number-input-border-radius: $input-border-radius !default; // Monochrome icons, painted with `currentColor` through `mask-icon()` on an empty button. -$stepper-icon-decrement: url("data:image/svg+xml,") !default; -$stepper-icon-increment: url("data:image/svg+xml,") !default; -// scss-docs-end stepper-variables +$number-input-icon-decrement: url("data:image/svg+xml,") !default; +$number-input-icon-increment: url("data:image/svg+xml,") !default; +// scss-docs-end number-input-variables // Cards // scss-docs-start card-variables diff --git a/core/scss/ui/forms/_form-stepper.scss b/core/scss/ui/forms/_form-number-input.scss similarity index 71% rename from core/scss/ui/forms/_form-stepper.scss rename to core/scss/ui/forms/_form-number-input.scss index a4c28b17e..145cb65d6 100644 --- a/core/scss/ui/forms/_form-stepper.scss +++ b/core/scss/ui/forms/_form-number-input.scss @@ -1,28 +1,28 @@ /* -Stepper (number input with buttons) +Number input (a field with a decrease and an increase button) */ @use 'sass:map'; @use '../../config' as *; // One bordered box, like a `.form-control`: the field in the middle and the // two buttons inside it have no border of their own. -.stepper { - // scss-docs-start stepper-css-vars - --stepper-input-width: #{$stepper-input-width}; - --stepper-bg: #{$stepper-bg}; - --stepper-border-color: #{$stepper-border-color}; - --stepper-border-radius: #{$stepper-border-radius}; - // scss-docs-end stepper-css-vars +.number-input { + // scss-docs-start number-input-css-vars + --number-input-input-width: #{$number-input-input-width}; + --number-input-bg: #{$number-input-bg}; + --number-input-border-color: #{$number-input-border-color}; + --number-input-border-radius: #{$number-input-border-radius}; + // scss-docs-end number-input-css-vars position: relative; display: inline-flex; align-items: stretch; // Never stretched by a flex column parent: the box is as wide as its parts width: fit-content; vertical-align: middle; - background-color: var(--stepper-bg); - border: $input-border-width solid var(--stepper-border-color); + background-color: var(--number-input-bg); + border: $input-border-width solid var(--number-input-border-color); box-shadow: $input-box-shadow; - @include border-radius(var(--stepper-border-radius)); + @include border-radius(var(--number-input-border-radius)); @include focus-ring-transition(border-color); // The box carries the focus ring for the field inside it. @@ -40,7 +40,7 @@ Stepper (number input with buttons) // stylelint-disable-next-line length-zero-no-unit --control-border-width: 0px; flex: 0 0 auto; - width: var(--stepper-input-width); + width: var(--number-input-input-width); min-width: 0; padding-inline: 0.25rem; text-align: center; @@ -51,7 +51,7 @@ Stepper (number input with buttons) box-shadow: none; // A `type="number"` field keeps its keyboard behaviour but loses the - // native spin buttons, which the stepper's own buttons replace. + // native spin buttons, which the number input's own buttons replace. &::-webkit-inner-spin-button, &::-webkit-outer-spin-button { margin: 0; @@ -95,14 +95,14 @@ Stepper (number input with buttons) // An empty button paints the default icon through a CSS mask, so the // markup needs no svg; any content put inside replaces it. - &[data-bs-stepper-action='decrement'], - &[data-tblr-stepper-action='decrement'] { - --stepper-icon: #{escape-svg($stepper-icon-decrement)}; + &[data-bs-number-input-action='decrement'], + &[data-tblr-number-input-action='decrement'] { + --number-input-icon: #{escape-svg($number-input-icon-decrement)}; } - &[data-bs-stepper-action='increment'], - &[data-tblr-stepper-action='increment'] { - --stepper-icon: #{escape-svg($stepper-icon-increment)}; + &[data-bs-number-input-action='increment'], + &[data-tblr-number-input-action='increment'] { + --number-input-icon: #{escape-svg($number-input-icon-increment)}; } &:not(:has(> *))::before { @@ -110,15 +110,15 @@ Stepper (number input with buttons) height: var(--btn-icon-size); content: ''; background-color: currentColor; - @include mask-icon(var(--stepper-icon), $size: 100%); + @include mask-icon(var(--number-input-icon), $size: 100%); } &:first-child { - @include border-start-radius(calc(var(--stepper-border-radius) - #{$input-border-width})); + @include border-start-radius(calc(var(--number-input-border-radius) - #{$input-border-width})); } &:last-child { - @include border-end-radius(calc(var(--stepper-border-radius) - #{$input-border-width})); + @include border-end-radius(calc(var(--number-input-border-radius) - #{$input-border-width})); } } @@ -144,9 +144,9 @@ Stepper (number input with buttons) // Sizes move the same tokens `.form-control-sm` / `.btn-sm` and their `-lg` // twins read, so the field and the buttons keep one height. @each $size in (sm, lg) { - .stepper-#{$size} { - --stepper-input-width: #{map.get($stepper-input-widths, $size)}; - --stepper-border-radius: var(--btn-input-#{$size}-border-radius); + .number-input-#{$size} { + --number-input-input-width: #{map.get($number-input-input-widths, $size)}; + --number-input-border-radius: var(--btn-input-#{$size}-border-radius); > .form-control { --control-padding-y: var(--btn-input-#{$size}-padding-y); @@ -158,7 +158,7 @@ Stepper (number input with buttons) --btn-padding-y: var(--btn-input-#{$size}-padding-y); --btn-font-size: var(--btn-input-#{$size}-font-size); --btn-line-height: var(--btn-input-#{$size}-line-height); - --btn-icon-size: #{map.get($stepper-icon-sizes, $size)}; + --btn-icon-size: #{map.get($number-input-icon-sizes, $size)}; } } } diff --git a/docs/content/ui/forms/stepper.mdx b/docs/content/ui/forms/number-input.mdx similarity index 68% rename from docs/content/ui/forms/stepper.mdx rename to docs/content/ui/forms/number-input.mdx index ac74cedb5..9f106333f 100644 --- a/docs/content/ui/forms/stepper.mdx +++ b/docs/content/ui/forms/number-input.mdx @@ -1,20 +1,20 @@ --- -title: Stepper -seoTitle: Stepper (quantity input) +title: Number input +seoTitle: Number input (quantity stepper) order: 14 summary: A number field with a decrease and an increase button, for a quantity in a cart, a number of seats or any value changed in fixed steps. -description: Add a stepper input with min, max and step, hold-to-repeat buttons, arrow key support and change events. +description: Add a number input with min, max and step, hold-to-repeat buttons, arrow key support and change events. related: [/ui/forms/elements, /ui/forms/validation] -source: core/scss/ui/forms/_form-stepper.scss -css-vars: stepper +source: core/scss/ui/forms/_form-number-input.scss +css-vars: number-input classnames: component: - - class: stepper + - class: number-input desc: The box around the field and the two buttons; rendered by the plugin, or written by hand for custom buttons size: - - class: stepper-sm + - class: number-input-sm desc: Smaller field and buttons - - class: stepper-lg + - class: number-input-lg desc: Larger field and buttons --- @@ -24,12 +24,12 @@ import Icon from '@ui/Icon.astro' ## Overview -A stepper is an ordinary `` between two buttons. The input stays the only field a form sees: the buttons, the arrow keys and the JavaScript API all write to it, keep the value between `min` and `max` and round it to the precision of `step`. +A number input is an ordinary `` between two buttons. The input stays the only field a form sees: the buttons, the arrow keys and the JavaScript API all write to it, keep the value between `min` and `max` and round it to the precision of `step`. -Add `data-bs-toggle="stepper"` to the input and Tabler does the rest: it wraps the field in a `.stepper` box and renders the two buttons. The bounds come from the input's own `min`, `max` and `step` attributes. +Add `data-bs-toggle="number-input"` to the input and Tabler does the rest: it wraps the field in a `.number-input` box and renders the two buttons. The bounds come from the input's own `min`, `max` and `step` attributes. - + ## Usage @@ -40,17 +40,17 @@ The button at a bound goes disabled, so the user sees when the value cannot go f once the field is left. A fractional `step` sets the precision: with `step="0.25"` the value is always shown with two decimals. - + Without `min` and `max` the value has no bounds. An empty field steps from `min` when that is positive, otherwise from zero. ### Text or number input -A `type="text"` input gets `role="spinbutton"`, the matching `aria-value*` attributes and a numeric `inputmode`, so a phone shows the number keyboard and the native spin buttons never appear. A `type="number"` input keeps the browser's own validation and keyboard behaviour; the stepper only hides its spin buttons. +A `type="text"` input gets `role="spinbutton"`, the matching `aria-value*` attributes and a numeric `inputmode`, so a phone shows the number keyboard and the native spin buttons never appear. A `type="number"` input keeps the browser's own validation and keyboard behaviour; the number input only hides its spin buttons. - + ### Keyboard @@ -59,24 +59,24 @@ With the focus in the field, ↑ and ↓ step the value, < ### Sizes -Add `.form-control-sm` or `.form-control-lg` to the input; the rendered box gets `.stepper-sm` or `.stepper-lg`, so the field and the buttons keep one height. +Add `.form-control-sm` or `.form-control-lg` to the input; the rendered box gets `.number-input-sm` or `.number-input-lg`, so the field and the buttons keep one height. - -
- + +
+ - +
- + ### Wider input -The input is `3.5rem` wide by default, enough for a quantity. Set `--tblr-stepper-input-width` on the input for longer values. +The input is `3.5rem` wide by default, enough for a quantity. Set `--tblr-number-input-input-width` on the input for longer values. - + ### Disabled @@ -84,7 +84,7 @@ The input is `3.5rem` wide by default, enough for a quantity. Set `--tblr-steppe Add `disabled` or `readonly` to the input; both buttons follow. - + ### Validation @@ -93,11 +93,11 @@ Add `.is-valid` or `.is-invalid` to the input, the same way you would on any `.f
- +
In stock, ships today.
- +
Only 5 pieces left.
@@ -107,10 +107,10 @@ With a `type="number"` input the browser validates `required` itself (the plugin
-
@@ -118,15 +118,15 @@ With a `type="number"` input the browser validates `required` itself (the plugin ### Custom buttons -To change the icons, the labels or the classes of the buttons, write the markup yourself: put `data-bs-toggle="stepper"` on a `.stepper` wrapper and mark the buttons with `data-bs-stepper-action="decrement"` and `"increment"`. An empty button still gets the default icon from CSS; any content inside replaces it. +To change the icons, the labels or the classes of the buttons, write the markup yourself: put `data-bs-toggle="number-input"` on a `.number-input` wrapper and mark the buttons with `data-bs-number-input-action="decrement"` and `"increment"`. An empty button still gets the default icon from CSS; any content inside replaces it. -
- -
@@ -149,21 +149,21 @@ Options are read from `data-bs-*` attributes on the element that carries `data-b ## JavaScript -Tabler creates a `Stepper` for every element matching `[data-bs-toggle="stepper"]` (or `[data-tblr-toggle="stepper"]`), an input or a `.stepper` wrapper: +Tabler creates a `NumberInput` for every element matching `[data-bs-toggle="number-input"]` (or `[data-tblr-toggle="number-input"]`), an input or a `.number-input` wrapper: - + Create one manually, or get an existing instance: ```javascript const el = document.querySelector('#quantity') -const stepper = tabler.Stepper.getOrCreateInstance(el, { min: 1, max: 10 }) +const numberInput = tabler.NumberInput.getOrCreateInstance(el, { min: 1, max: 10 }) ``` -The input itself fires the usual `input` and `change` events whenever a button or the API changes it, so a form, a framework binding or validation sees a stepped value like a typed one. On top of that, `change.bs.stepper` fires on the element the plugin was created on with both values: +The input itself fires the usual `input` and `change` events whenever a button or the API changes it, so a form, a framework binding or validation sees a stepped value like a typed one. On top of that, `change.bs.numberInput` fires on the element the plugin was created on with both values: ```javascript -el.addEventListener('change.bs.stepper', (event) => { +el.addEventListener('change.bs.numberInput', (event) => { console.log(event.value, event.previousValue) }) ``` diff --git a/docs/lib/redirects.ts b/docs/lib/redirects.ts index 509801cca..61d9887e1 100644 --- a/docs/lib/redirects.ts +++ b/docs/lib/redirects.ts @@ -43,6 +43,8 @@ export const redirects: Record = { '/ui/forms/form-fieldset': { status: 301, destination: '/ui/forms/fieldset' }, '/ui/forms/form-floating': { status: 301, destination: '/ui/forms/floating-labels' }, '/ui/forms/form-helpers': { status: 301, destination: '/ui/forms/helpers' }, + // The quantity stepper was renamed before 1.7 shipped; `stepper` is the v6 wizard component. + '/ui/forms/stepper': { status: 301, destination: '/ui/forms/number-input' }, '/ui/forms/form-selectboxes': { status: 301, destination: '/ui/forms/select-group' }, '/ui/forms/form-image-check': { status: 301, destination: '/ui/forms/image-check' }, '/ui/forms/form-color-check': { status: 301, destination: '/ui/forms/color-check' }, diff --git a/preview/pages/billing.astro b/preview/pages/billing.astro index cef5671e4..de2fc2530 100644 --- a/preview/pages/billing.astro +++ b/preview/pages/billing.astro @@ -16,7 +16,7 @@ import ListGroupItem from '@ui/ListGroupItem.astro' import Icon from '@ui/Icon.astro' import Payment from '@ui/Payment.astro' import Progress from '@ui/Progress.astro' -import Stepper from '@ui/Stepper.astro' +import NumberInput from '@ui/NumberInput.astro' import type { PaymentProvider } from '@shared/lib/tokens' const usage = { used: 7420, limit: 10000 } @@ -82,7 +82,7 @@ const invoiceStatus = { September 18, 2026
- + 8 used
diff --git a/preview/pages/form-elements.astro b/preview/pages/form-elements.astro index f5884c7d1..33164c77b 100644 --- a/preview/pages/form-elements.astro +++ b/preview/pages/form-elements.astro @@ -11,7 +11,7 @@ import Layout from '@shared/components/cards/form/Layout.astro' import InputIcon from '@ui/form/InputIcon.astro' import InputMask from '@ui/form/InputMask.astro' import OtpInput from '@ui/OtpInput.astro' -import Stepper from '@ui/Stepper.astro' +import NumberInput from '@ui/NumberInput.astro' import Button from '@ui/Button.astro' import Avatar from '@ui/Avatar.astro' import Card from '@ui/Card.astro' @@ -161,13 +161,13 @@ const rows = [
- + - - - - - + + + + +
diff --git a/preview/pages/stepper.astro b/preview/pages/number-input.astro similarity index 62% rename from preview/pages/stepper.astro rename to preview/pages/number-input.astro index 696aec3a9..a99407436 100644 --- a/preview/pages/stepper.astro +++ b/preview/pages/number-input.astro @@ -6,7 +6,7 @@ import CardTitle from '@ui/CardTitle.astro' import CardSubtitle from '@ui/CardSubtitle.astro' import DocsLink from '@ui/DocsLink.astro' import FormGroup from '@ui/FormGroup.astro' -import Stepper from '@ui/Stepper.astro' +import NumberInput from '@ui/NumberInput.astro' import Icon from '@ui/Icon.astro' const cart = [ @@ -16,16 +16,16 @@ const cart = [ ] --- - - + +
Basic - Add data-bs-toggle="stepper" to the input; the plugin renders the box and the buttons, min and max set the bounds. - + Add data-bs-toggle="number-input" to the input; the plugin renders the box and the buttons, min and max set the bounds. +
@@ -35,8 +35,8 @@ const cart = [ At a bound The button at min or max goes disabled, so the user sees the value cannot go further.
- - + +
@@ -47,8 +47,8 @@ const cart = [ Step and precision A fractional step sets how many decimals the value keeps.
- - + +
@@ -57,11 +57,11 @@ const cart = [ Sizes - Add .form-control-sm or .form-control-lg to the input; the box gets .stepper-sm or -lg. + Add .form-control-sm or .form-control-lg to the input; the box gets .number-input-sm or -lg.
- - - + + +
@@ -71,7 +71,7 @@ const cart = [ Without bounds Without min and max the value goes both ways, so a minus sign can be typed too. - +
@@ -79,8 +79,8 @@ const cart = [ Native number input - Use type="number" to keep the browser's own validation; the stepper only hides its spin buttons. - + Use type="number" to keep the browser's own validation; the number input only hides its spin buttons. +
@@ -89,7 +89,7 @@ const cart = [ No repeat Add data-bs-repeat="false" to step once per click instead of repeating while the button is held. - + @@ -97,8 +97,8 @@ const cart = [ Wider input - Set --tblr-stepper-input-width on the input for longer values. - + Set --tblr-number-input-input-width on the input for longer values. + @@ -108,8 +108,8 @@ const cart = [ Disabled and readonly Add disabled or readonly to the input; both buttons follow.
- - + +
@@ -121,11 +121,11 @@ const cart = [ Add .is-valid or .is-invalid to the input, like on any .form-control; the feedback text after it still shows.
- +
In stock, ships today.
- +
Only 5 pieces left.
@@ -136,11 +136,11 @@ const cart = [ Custom buttons - Write the .stepper markup yourself to change the icons or labels; data-bs-stepper-action marks the buttons. -
- + Write the .number-input markup yourself to change the icons or labels; data-bs-number-input-action marks the buttons. +
+ - +
@@ -149,8 +149,8 @@ const cart = [ With a label and hint - A stepper sits in a FormGroup like any other control. - + A number input sits in a FormGroup like any other control. +
@@ -158,7 +158,7 @@ const cart = [ In a cart - The usual place for a stepper: a quantity per line, with max set to the stock. + The usual place for a number input: a quantity per line, with max set to the stock.
@@ -179,7 +179,7 @@ const cart = [
${item.price} - +