antd-mobile
Version:
<div align="center">
215 lines • 6.83 kB
JavaScript
import classNames from 'classnames';
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import { MinusOutline, AddOutline } from 'antd-mobile-icons';
import { useMergedState } from 'rc-util';
import getMiniDecimal, { toFixed } from '@rc-component/mini-decimal';
import { withNativeProps } from '../../utils/native-props';
import { mergeProps } from '../../utils/with-default-props';
import Input from '../input';
import Button from '../button';
import { useConfig } from '../config-provider';
const classPrefix = `adm-stepper`;
const defaultProps = {
step: 1,
disabled: false,
allowEmpty: false
};
export function InnerStepper(p, ref) {
const props = mergeProps(defaultProps, p);
const {
defaultValue = 0,
value,
onChange,
disabled,
step,
max,
min,
inputReadOnly,
digits,
stringMode,
formatter,
parser
} = props;
const {
locale
} = useConfig();
// ========================== Ref ==========================
useImperativeHandle(ref, () => ({
focus: () => {
var _a;
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
},
blur: () => {
var _a;
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
},
get nativeElement() {
var _a, _b;
return (_b = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.nativeElement) !== null && _b !== void 0 ? _b : null;
}
}));
// ========================== Parse / Format ==========================
const fixedValue = value => {
const fixedValue = digits !== undefined ? toFixed(value.toString(), '.', digits) : value;
return fixedValue.toString();
};
const getValueAsType = value => stringMode ? value.toString() : value.toNumber();
const parseValue = text => {
if (text === '') return null;
if (parser) {
return String(parser(text));
}
const decimal = getMiniDecimal(text);
return decimal.isInvalidate() ? null : decimal.toString();
};
const formatValue = value => {
if (value === null) return '';
return formatter ? formatter(value) : fixedValue(value);
};
// ======================== Value & InputValue ========================
const [mergedValue, setMergedValue] = useMergedState(defaultValue, {
value,
onChange: nextValue => {
onChange === null || onChange === void 0 ? void 0 : onChange(nextValue);
}
});
const [inputValue, setInputValue] = useState(() => formatValue(mergedValue));
// >>>>> Value
function setValueWithCheck(nextValue) {
if (nextValue.isNaN()) return;
let target = nextValue;
// Put into range
if (min !== undefined) {
const minDecimal = getMiniDecimal(min);
if (target.lessEquals(minDecimal)) {
target = minDecimal;
}
}
if (max !== undefined) {
const maxDecimal = getMiniDecimal(max);
if (maxDecimal.lessEquals(target)) {
target = maxDecimal;
}
}
// Fix digits
if (digits !== undefined) {
target = getMiniDecimal(fixedValue(getValueAsType(target)));
}
setMergedValue(getValueAsType(target));
}
// >>>>> Input
const handleInputChange = v => {
setInputValue(v);
const valueStr = parseValue(v);
if (valueStr === null) {
if (props.allowEmpty) {
setMergedValue(null);
} else {
setMergedValue(defaultValue);
}
} else {
setValueWithCheck(getMiniDecimal(valueStr));
}
};
// ============================== Focus ===============================
const [focused, setFocused] = useState(false);
const inputRef = React.useRef(null);
function triggerFocus(nextFocus) {
setFocused(nextFocus);
// We will convert value to original text when focus
if (nextFocus) {
setInputValue(mergedValue !== null && mergedValue !== undefined ? String(mergedValue) : '');
}
}
useEffect(() => {
var _a, _b, _c;
if (focused) {
(_c = (_b = (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.nativeElement) === null || _b === void 0 ? void 0 : _b.select) === null || _c === void 0 ? void 0 : _c.call(_b);
}
}, [focused]);
// Focus change to format value
useEffect(() => {
if (!focused) {
setInputValue(formatValue(mergedValue));
}
}, [focused, mergedValue, digits]);
// ============================ Operations ============================
const handleOffset = positive => {
let stepValue = getMiniDecimal(step);
if (!positive) {
stepValue = stepValue.negate();
}
setValueWithCheck(getMiniDecimal(mergedValue !== null && mergedValue !== void 0 ? mergedValue : 0).add(stepValue.toString()));
};
const handleMinus = () => {
handleOffset(false);
};
const handlePlus = () => {
handleOffset(true);
};
const minusDisabled = () => {
if (disabled) return true;
if (mergedValue === null) return false;
if (min !== undefined) {
return mergedValue <= min;
}
return false;
};
const plusDisabled = () => {
if (disabled) return true;
if (mergedValue === null) return false;
if (max !== undefined) {
return mergedValue >= max;
}
return false;
};
// ============================== Render ==============================
return withNativeProps(props, React.createElement("div", {
className: classNames(classPrefix, {
[`${classPrefix}-active`]: focused
})
}, React.createElement(Button, {
className: `${classPrefix}-minus`,
onClick: handleMinus,
disabled: minusDisabled(),
fill: 'none',
shape: 'rectangular',
color: 'primary',
"aria-label": locale.Stepper.decrease
}, React.createElement(MinusOutline, null)), React.createElement("div", {
className: `${classPrefix}-middle`
}, React.createElement(Input, {
ref: inputRef,
className: `${classPrefix}-input`,
onFocus: e => {
var _a;
triggerFocus(true);
(_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
},
value: inputValue,
onChange: val => {
disabled || handleInputChange(val);
},
disabled: disabled,
onBlur: e => {
var _a;
triggerFocus(false);
(_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
},
readOnly: inputReadOnly,
role: 'spinbutton',
"aria-valuenow": Number(inputValue),
"aria-valuemax": Number(max),
"aria-valuemin": Number(min),
inputMode: 'decimal'
})), React.createElement(Button, {
className: `${classPrefix}-plus`,
onClick: handlePlus,
disabled: plusDisabled(),
fill: 'none',
shape: 'rectangular',
color: 'primary',
"aria-label": locale.Stepper.increase
}, React.createElement(AddOutline, null))));
}
export const Stepper = forwardRef(InnerStepper);