form-spinbutton.html html
<form-spinbutton value="0" zero-label="Add to Cart" increment-label="Increment">
<button type="button" class="decrement" aria-label="Decrement" hidden>
−
</button>
<p class="value" hidden>0</p>
<button type="button" class="increment primary">Add to Cart</button>
</form-spinbutton>
form-spinbutton.css css
form-spinbutton {
display: inline-flex;
max-width: 7rem;
.value {
box-sizing: border-box;
margin: 0;
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
height: var(--input-height);
min-width: 2rem;
max-width: 3rem;
text-align: center;
font-size: var(--font-size-s);
padding: 0 var(--space-s);
line-height: 2;
flex-grow: 1;
}
> button {
flex-grow: 0;
box-sizing: border-box;
height: var(--input-height);
min-width: var(--input-height);
border: 1px solid var(--color-border);
background-color: var(--color-secondary);
color: var(--color-text);
padding: 0 var(--space-s);
font-size: var(--font-size-s);
line-height: var(--line-height-s);
white-space: nowrap;
cursor: pointer;
transition: all var(--transition-shorter) var(--easing-inout);
&:disabled {
opacity: var(--opacity-translucent);
}
&:not(:disabled) {
cursor: pointer;
opacity: var(--opacity-solid);
&:hover {
background-color: var(--color-secondary-hover);
}
&:active {
background-color: var(--color-secondary-active);
}
}
&:first-child {
border-radius: var(--space-xs) 0 0 var(--space-xs);
}
&:last-child {
border-radius: 0 var(--space-xs) var(--space-xs) 0;
}
}
[hidden] + button {
flex-grow: 1;
border-radius: var(--space-xs);
color: var(--color-text-inverted);
background-color: var(--color-primary);
border-color: var(--color-primary-active);
&:hover {
background-color: var(--color-primary-hover);
}
&:active {
background-color: var(--color-primary-active);
}
}
}
form-spinbutton.ts ts
import {
type Component,
asInteger,
component,
fromEvents,
setProperty,
setText,
show,
} from '../../..'
export type FormSpinbuttonProps = {
value: number
}
const clickHandler = ({ target, value }) =>
value + (target.classList.contains('decrement') ? -1 : 1)
const keydownHandler = ({ event, value }) => {
const { key } = event as KeyboardEvent
if (['ArrowUp', 'ArrowDown', '-', '+'].includes(key)) {
event.stopPropagation()
event.preventDefault()
return value + (key === 'ArrowDown' || key === '-' ? -1 : 1)
}
}
export default component(
'form-spinbutton',
{
value: fromEvents(
el => asInteger()(el, el.querySelector('value')?.textContent),
'button',
{
click: clickHandler,
keydown: keydownHandler,
},
),
},
(el, { first }) => {
const zeroLabel = el.getAttribute('zero-label') || 'Add to Cart'
const incrementLabel = el.getAttribute('increment-label') || 'Increment'
const max = asInteger(9)(el, el.getAttribute('max'))
const nonZero = () => el.value !== 0
return [
first('.value', setText('value'), show(nonZero)),
first('.decrement', show(nonZero)),
first<HTMLButtonElement>(
'.increment',
setText(() => (nonZero() ? '+' : zeroLabel)),
setProperty('ariaLabel', () =>
nonZero() ? incrementLabel : zeroLabel,
),
setProperty('disabled', () => el.value >= max),
),
]
},
)
declare global {
interface HTMLElementTagNameMap {
'form-spinbutton': Component<FormSpinbuttonProps>
}
}