UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

259 lines 11.7 kB
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