feat: add NumericStepper component and integrate into index page
This commit is contained in:
@@ -11,4 +11,5 @@ export { default as Link } from './Link/link.astro';
|
||||
export { default as ListItem } from './ListItem/listItem.astro';
|
||||
export { default as ListItemTitle } from './ListItem/listItemTitle.astro';
|
||||
export { default as ListItemSubtitle } from './ListItem/listItemSubtitle.astro';
|
||||
export { default as LoadingBar } from './LoadingBar/loadingBar.astro';
|
||||
export { default as LoadingBar } from './LoadingBar/loadingBar.astro';
|
||||
export { default as NumericStepper } from './numericStepper/numericStepper.astro';
|
||||
@@ -0,0 +1,14 @@
|
||||
@use '../../styles/tokens/typography' as *;
|
||||
|
||||
.numeric-stepper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--nds-spacing-xs);
|
||||
width: fit-content;
|
||||
& p {
|
||||
@include text-xl;
|
||||
margin: 0;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
import Button from '../Button/button.astro';
|
||||
import { Arrow2Icon } 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="secondary" icon={true}>
|
||||
<Arrow2Icon size={24} orientation="up" />
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
<p class="numeric-stepper__value">{value}</p>
|
||||
|
||||
<span class="numeric-stepper__control" data-action="decrement">
|
||||
<Button type="secondary" icon={true}>
|
||||
<Arrow2Icon size={24} orientation="down" />
|
||||
</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>
|
||||
Reference in New Issue
Block a user