Files
nova-design-system/src/components/numericStepper/numericStepper.astro
T

117 lines
3.1 KiB
Plaintext
Raw Normal View History

---
import Button from '../Button/button.astro';
import { MinusIcon, PlusIcon } from '../Icons';
export interface Props {
id: string;
min?: number;
max?: number;
value?: number;
step?: number;
}
const { id, min, max, value = 0, step = 1 } = Astro.props;
---
<div
class="numeric-stepper"
id={id}
data-numeric-stepper
data-min={min}
data-max={max}
data-value={value}
data-step={step}
>
<span class="numeric-stepper__control" data-action="increment">
<Button type="ghost" icon={true}>
<MinusIcon size={24} />
</Button>
</span>
<p class="numeric-stepper__value">{value}</p>
<span class="numeric-stepper__control" data-action="decrement">
<Button type="ghost" icon={true}>
<PlusIcon size={24} />
</Button>
</span>
</div>
<script>
type StepperAction = 'increment' | 'decrement';
interface StepperChangeDetail {
value: number;
}
interface StepperSetDetail {
value: number;
}
interface NumericStepperElement extends HTMLElement {
setValue: (value: number) => void;
getValue: () => number;
getStep: () => number;
}
declare global {
interface HTMLElementEventMap {
'stepper:change': CustomEvent<StepperChangeDetail>;
'stepper:set': CustomEvent<StepperSetDetail>;
}
}
document.querySelectorAll<NumericStepperElement>('[data-numeric-stepper]').forEach((stepper) => {
const { min: minAttr, max: maxAttr, step: stepAttr, value: valueAttr } = stepper.dataset;
const min = minAttr !== undefined ? Number(minAttr) : -Infinity;
const max = maxAttr !== undefined ? Number(maxAttr) : Infinity;
const step = stepAttr !== undefined ? Number(stepAttr) : 1;
let current = valueAttr !== undefined ? Number(valueAttr) : 0;
const display = stepper.querySelector<HTMLParagraphElement>('.numeric-stepper__value');
function render(): void {
if (display) display.textContent = String(current);
stepper.dataset.value = String(current);
}
function setValue(newValue: number): void {
const clamped = Math.min(max, Math.max(min, newValue));
if (clamped === current) return;
current = clamped;
render();
stepper.dispatchEvent(
new CustomEvent<StepperChangeDetail>('stepper:change', {
detail: { value: current },
bubbles: true,
}),
);
}
stepper.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
const control = target.closest<HTMLElement>('[data-action]');
if (!control || !stepper.contains(control)) return;
const action = control.dataset.action as StepperAction | undefined;
if (action === 'increment') setValue(current + step);
else if (action === 'decrement') setValue(current - step);
});
stepper.addEventListener('stepper:set', (event) => {
setValue(event.detail.value);
});
stepper.setValue = (value: number): void => setValue(value);
stepper.getValue = (): number => current;
stepper.getStep = (): number => step;
});
</script>
<style lang="scss">
@use "./numericStepper";
</style>