@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
259 lines • 11.7 kB
JavaScript
import { Shade, createComponent } from '@furystack/shades';
import { buildTransition, cssVariableTheme } from '../../services/css-variable-theme.js';
import { ThemeProviderService } from '../../services/theme-provider-service.js';
import { FormContextToken } from '../form.js';
const clampValue = (value, min, max) => {
if (min !== undefined && value < min)
return min;
if (max !== undefined && value > max)
return max;
return value;
};
const roundToPrecision = (value, precision) => {
if (precision === undefined)
return value;
const factor = Math.pow(10, precision);
return Math.round(value * factor) / factor;
};
const formatValue = (value, precision, formatter) => {
if (value === undefined)
return '';
if (formatter)
return formatter(value);
if (precision !== undefined)
return value.toFixed(precision);
return String(value);
};
const parseValue = (text, parser) => {
if (parser)
return parser(text);
if (text === '' || text === '-')
return undefined;
const num = Number(text);
return isNaN(num) ? undefined : num;
};
export const InputNumber = Shade({
customElementName: 'shade-input-number',
css: {
display: 'block',
fontFamily: cssVariableTheme.typography.fontFamily,
marginBottom: '1.25em',
'& label': {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'space-between',
fontSize: cssVariableTheme.typography.fontSize.xs,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
letterSpacing: '0.01em',
padding: '12px 14px',
borderRadius: cssVariableTheme.shape.borderRadius.md,
transition: `all ${cssVariableTheme.transitions.duration.normal} ${cssVariableTheme.transitions.easing.default}`,
cursor: 'text',
color: cssVariableTheme.text.secondary,
background: 'transparent',
border: '2px solid transparent',
boxShadow: 'none',
},
'&[data-variant="outlined"] label': {
borderColor: cssVariableTheme.action.subtleBorder,
},
'&[data-variant="contained"] label': {
borderColor: cssVariableTheme.action.subtleBorder,
background: 'color-mix(in srgb, var(--input-number-color) 8%, transparent)',
},
'&:focus-within label': {
color: 'var(--input-number-color)',
},
'&[data-variant="outlined"]:focus-within label, &[data-variant="contained"]:focus-within label': {
borderColor: 'var(--input-number-color)',
boxShadow: cssVariableTheme.action.focusRing,
},
'&[data-variant="contained"]:focus-within label': {
background: 'color-mix(in srgb, var(--input-number-color) 12%, transparent)',
},
'&[data-disabled] label': {
color: cssVariableTheme.text.disabled,
filter: 'grayscale(100%)',
opacity: cssVariableTheme.action.disabledOpacity,
cursor: 'not-allowed',
},
'&[data-disabled]:focus-within label': {
boxShadow: 'none',
},
'& .input-number-row': {
display: 'flex',
alignItems: 'center',
width: '100%',
gap: cssVariableTheme.spacing.xs,
},
'& input': {
color: 'inherit',
border: 'none',
backgroundColor: 'transparent',
outline: 'none',
fontSize: cssVariableTheme.typography.fontSize.sm,
fontWeight: cssVariableTheme.typography.fontWeight.normal,
width: '100%',
textOverflow: 'ellipsis',
padding: '0',
marginTop: cssVariableTheme.spacing.sm,
marginBottom: '2px',
flexGrow: '1',
lineHeight: '1.5',
textAlign: 'center',
appearance: 'textfield',
},
'& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
webkitAppearance: 'none',
margin: '0',
},
'& .step-button': {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '28px',
height: '28px',
border: 'none',
borderRadius: cssVariableTheme.shape.borderRadius.sm,
background: 'color-mix(in srgb, var(--input-number-color) 15%, transparent)',
color: 'var(--input-number-color)',
cursor: 'pointer',
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.bold,
lineHeight: '1',
flexShrink: '0',
userSelect: 'none',
transition: buildTransition(['background', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['transform', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.easeOut]),
},
'& .step-button:hover:not(:disabled)': {
background: 'color-mix(in srgb, var(--input-number-color) 25%, transparent)',
},
'& .step-button:active:not(:disabled)': {
transform: 'scale(0.92)',
},
'& .step-button:disabled': {
cursor: 'not-allowed',
opacity: '0.4',
},
'& .helperText': {
fontSize: cssVariableTheme.typography.fontSize.xs,
marginTop: '6px',
opacity: '0.85',
lineHeight: '1.4',
},
// Size: small
'&[data-size="small"] label': {
padding: `${cssVariableTheme.spacing.xs} ${cssVariableTheme.spacing.sm}`,
},
'&[data-size="small"] input': {
fontSize: cssVariableTheme.typography.fontSize.xs,
},
'&[data-size="small"] .step-button': {
width: '22px',
height: '22px',
fontSize: cssVariableTheme.typography.fontSize.sm,
},
// Size: large
'&[data-size="large"] label': {
padding: `${cssVariableTheme.spacing.md} ${cssVariableTheme.spacing.lg}`,
fontSize: cssVariableTheme.typography.fontSize.sm,
},
'&[data-size="large"] input': {
fontSize: cssVariableTheme.typography.fontSize.md,
},
'&[data-size="large"] .step-button': {
width: '34px',
height: '34px',
fontSize: cssVariableTheme.typography.fontSize.lg,
},
},
render: ({ props, injector, useState, useDisposable, useHostProps, useRef }) => {
const inputRef = useRef('formInput');
useDisposable('form-registration', () => {
const formService = injector.get(FormContextToken);
if (formService) {
queueMicrotask(() => {
if (inputRef.current)
formService.inputs.add(inputRef.current);
});
}
return {
[Symbol.dispose]: () => {
if (inputRef.current && formService)
formService.inputs.delete(inputRef.current);
},
};
});
const themeProvider = injector.get(ThemeProviderService);
const primaryColor = themeProvider.theme.palette[props.color || 'primary'].main;
useHostProps({
'data-variant': props.variant || undefined,
'data-size': props.size && props.size !== 'medium' ? props.size : undefined,
'data-disabled': props.disabled ? '' : undefined,
style: { '--input-number-color': primaryColor },
});
const step = props.step ?? 1;
const [state, setState] = useState('inputNumberState', {
value: props.value,
displayValue: formatValue(props.value, props.precision, props.formatter),
});
const updateValue = (newValue) => {
if (newValue !== undefined) {
newValue = roundToPrecision(newValue, props.precision);
newValue = clampValue(newValue, props.min, props.max);
}
const displayValue = formatValue(newValue, props.precision, props.formatter);
setState({ value: newValue, displayValue });
props.onValueChange?.(newValue);
};
const handleIncrement = () => {
if (props.disabled || props.readOnly)
return;
const current = state.value ?? props.min ?? 0;
updateValue(current + step);
};
const handleDecrement = () => {
if (props.disabled || props.readOnly)
return;
const current = state.value ?? props.min ?? 0;
updateValue(current - step);
};
const isDecrementDisabled = props.disabled ||
props.readOnly ||
(props.min !== undefined && state.value !== undefined && state.value <= props.min);
const isIncrementDisabled = props.disabled ||
props.readOnly ||
(props.max !== undefined && state.value !== undefined && state.value >= props.max);
return (createComponent("label", { ...props.labelProps },
props.labelTitle,
createComponent("div", { className: "input-number-row" },
createComponent("button", { type: "button", className: "step-button", "aria-label": "Decrease value", disabled: isDecrementDisabled, onclick: handleDecrement, tabIndex: -1 }, "\u2212"),
createComponent("input", { ref: inputRef, type: "text", inputMode: "decimal", role: "spinbutton", "aria-valuemin": props.min !== undefined ? String(props.min) : undefined, "aria-valuemax": props.max !== undefined ? String(props.max) : undefined, "aria-valuenow": state.value !== undefined ? String(state.value) : undefined, name: props.name, value: state.displayValue, placeholder: props.placeholder, disabled: props.disabled, readOnly: props.readOnly, onkeydown: (ev) => {
if (props.disabled || props.readOnly)
return;
if (ev.key === 'ArrowUp') {
ev.preventDefault();
handleIncrement();
}
else if (ev.key === 'ArrowDown') {
ev.preventDefault();
handleDecrement();
}
}, oninput: (ev) => {
const el = ev.target;
setState({ ...state, displayValue: el.value });
}, onblur: (ev) => {
const el = ev.target;
const parsed = parseValue(el.value, props.parser);
updateValue(parsed);
}, onchange: (ev) => {
const el = ev.target;
const parsed = parseValue(el.value, props.parser);
updateValue(parsed);
} }),
createComponent("button", { type: "button", className: "step-button", "aria-label": "Increase value", disabled: isIncrementDisabled, onclick: handleIncrement, tabIndex: -1 }, "+")),
props.helperText ? createComponent("span", { className: "helperText" }, props.helperText) : null));
},
});
//# sourceMappingURL=input-number.js.map