Skip to content

Commit

Permalink
feat(stepper): 对齐 mobile-vue (#525)
Browse files Browse the repository at this point in the history
* feat(stepper): 对齐 mobile-vue

* fix(stepper): 修改site文件

* fix: fix cr

---------

Co-authored-by: taninsist <[email protected]>
Co-authored-by: anlyyao <[email protected]>
  • Loading branch information
3 people authored Sep 19, 2024
1 parent db4e311 commit 804b5d6
Show file tree
Hide file tree
Showing 23 changed files with 1,679 additions and 199 deletions.
2 changes: 1 addition & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default {
{
title: 'Stepper 步进器',
name: 'Stepper',
component: () => import('tdesign-mobile-react/stepper/_example/index.jsx'),
component: () => import('tdesign-mobile-react/stepper/_example/index.tsx'),
},
{
title: 'PullDownRefresh 下拉刷新',
Expand Down
30 changes: 30 additions & 0 deletions src/_util/formatNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* 格式化数字
* @param value 传入的数字字符串
* @param allowDecimal 是否允许小数,默认为 true
* @param allowNegative 是否允许负数,默认为 true
* @returns 返回格式化后的数字字符串
*/
export const formatNumber = (value: string, allowDecimal = true, allowNegative = true) => {
let resultValue = value;
if (allowDecimal) {
const index = value.indexOf('.');
if (index !== -1) {
resultValue = `${value.slice(0, index + 1)}${value.slice(index).replace(/\./g, '')}`;
}
} else {
const [splitValue = ''] = value.split('.');
resultValue = splitValue;
}

if (allowNegative) {
const index = value.indexOf('-');
if (index !== -1) {
resultValue = `${value.slice(0, index + 1)}${value.slice(index).replace(/-/g, '')}`;
}
} else {
resultValue = value.replace(/-/g, '');
}

return resultValue.replace(/[^\d.-]/g, '');
};
179 changes: 104 additions & 75 deletions src/stepper/Stepper.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,164 @@
import React, { FC } from 'react';
import React, { FormEvent, forwardRef, memo } from 'react';
import classNames from 'classnames';
import identity from 'lodash/identity';
import { AddIcon, RemoveIcon } from 'tdesign-icons-react';
import useConfig from '../_util/useConfig';
import { usePrefixClass } from '../hooks/useClass';
import useDefaultProps from '../hooks/useDefaultProps';
import useDefault from '../_util/useDefault';
import { formatNumber } from '../_util/formatNumber';
import { StyledProps } from '../common';
import { TdStepperProps } from './type';
import withNativeProps, { NativeProps } from '../_util/withNativeProps';

export interface StepperProps extends TdStepperProps, NativeProps {}

const defaultProps = {
disabled: false,
disableInput: false,
max: 100,
min: 0,
step: 1,
theme: 'normal',
defaultValue: 0,
onBlur: identity,
onChange: identity,
onOverlimit: identity,
};

const Stepper: FC<StepperProps> = (props) => {
import { stepperDefaultProps } from './defaultProps';

export interface StepperProps extends TdStepperProps, StyledProps {}

const Stepper = forwardRef<HTMLDivElement, StepperProps>((originProps, ref) => {
const props = useDefaultProps(originProps, stepperDefaultProps);
const {
disabled,
className,
style,
disableInput,
disabled,
inputWidth,
integer,
max,
size,
min,
step,
theme,
value,
defaultValue,
onBlur,
onChange,
onFocus,
onOverlimit,
} = props;

const [currentValue, setCurrentValue] = useDefault(value, defaultValue, onChange);
const { classPrefix } = useConfig();
const name = `${classPrefix}-stepper`;

const stepperClass = usePrefixClass('stepper');

const inputStyle = inputWidth ? { width: `${inputWidth}px` } : {};

const isDisabled = (type: 'minus' | 'plus') => {
if (disabled) return true;
if (type === 'minus' && currentValue <= min) {
if (type === 'minus' && Number(currentValue) <= min) {
return true;
}
if (type === 'plus' && currentValue >= max) {
if (type === 'plus' && Number(currentValue) >= max) {
return true;
}
return false;
};

const getLen = (num: number) => {
const numStr = num.toString();
return numStr.indexOf('.') === -1 ? 0 : numStr.split('.')[1].length;
};

/**
* 精确加法
*/
const add = (a: number, b: number) => {
const maxLen = Math.max(getLen(a), getLen(b));
const base = 10 ** maxLen;
return Math.round(a * base + b * base) / base;
};

const formatValue = (value: number) =>
Math.max(Math.min(max, value, Number.MAX_SAFE_INTEGER), min, Number.MIN_SAFE_INTEGER);
Math.max(Math.min(max, value, Number.MAX_SAFE_INTEGER), min, Number.MIN_SAFE_INTEGER).toFixed(
Math.max(getLen(step), getLen(value)),
);

const updateValue = (value: number) => {
setCurrentValue(formatValue(value));
onChange(value);
const updateValue = (value: TdStepperProps['value']) => {
setCurrentValue(formatNumber(`${value}`, !integer));
};

const minusValue = () => {
if (isDisabled('minus')) {
onOverlimit('minus');
const plusValue = () => {
if (isDisabled('plus')) {
onOverlimit?.('plus');
return;
}
updateValue(Number(currentValue) - step);
updateValue(formatValue(add(Number(currentValue), step)));
};

const plusValue = () => {
if (isDisabled('plus')) {
onOverlimit('plus');
const minusValue = () => {
if (isDisabled('minus')) {
onOverlimit?.('minus');
return;
}
updateValue(Number(currentValue) + step);
updateValue(formatValue(add(Number(currentValue), -step)));
};

const handleChange = (e: React.FocusEvent<HTMLInputElement, Element>) => {
const { value } = e.currentTarget;
if (isNaN(Number(value))) return;
const formattedValue = formatValue(Number(value));
setCurrentValue(formattedValue);
onChange(formattedValue);
const handleInput = (e: FormEvent<HTMLInputElement>) => {
const value = formatNumber((e.target as HTMLInputElement).value, !integer);
setCurrentValue(value);
};

const handleBlur = (e: React.FocusEvent<HTMLInputElement, Element>) => {
const { value } = e.currentTarget;
if (isNaN(Number(value))) return;
const formattedValue = formatValue(Number(value));
setCurrentValue(formattedValue);
onBlur(formattedValue);
const handleChange = () => {
const formattedValue = formatValue(Number(currentValue));
updateValue(formattedValue);
};

return withNativeProps(
props,
<div
className={classNames(name, {
[`${classPrefix}-is-disabled`]: disabled,
[`${name}__pure`]: theme === 'grey',
[`${name}__normal`]: theme !== 'grey',
})}
>
const handleFocus = () => {
onFocus(Number(currentValue));
};

const handleBlur = () => {
onBlur(Number(currentValue));
};

return (
<div className={classNames(stepperClass, `${stepperClass}--${size}`, className)} style={style} ref={ref}>
<div
className={classNames(`${name}__minus`, {
[`${classPrefix}-is-disabled`]: currentValue <= min,
})}
className={classNames(
`${stepperClass}__minus`,
`${stepperClass}__minus--${theme}`,
`${stepperClass}__icon--${size}`,
{
[`${stepperClass}--${theme}-disabled`]: disabled || Number(currentValue) <= props.min,
},
)}
onClick={minusValue}
>
<RemoveIcon className={`${name}__icon`} />
<RemoveIcon className={`${stepperClass}__minus-icon`} />
</div>
<input
className={`${name}__input`}
style={{ width: inputWidth || 50 }}
value={currentValue}
onChange={handleChange}
className={classNames(
`${stepperClass}__input`,
`${stepperClass}__input--${theme}`,
`${stepperClass}__input--${size}`,
{
[`${stepperClass}--${theme}-disabled`]: disabled,
},
)}
type={integer ? 'tel' : 'text'}
inputMode={integer ? 'numeric' : 'decimal'}
style={inputStyle}
disabled={disableInput || disabled}
readOnly={disableInput}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled || disableInput}
onInput={handleInput}
onChange={handleChange}
/>
<div
className={classNames(`${name}__plus`, {
[`${classPrefix}-is-disabled`]: currentValue >= max,
})}
className={classNames(
`${stepperClass}__plus`,
`${stepperClass}__plus--${theme}`,
`${stepperClass}__icon--${size}`,
{
[`${stepperClass}--${theme}-disabled`]: disabled || Number(currentValue) >= max,
},
)}
onClick={plusValue}
>
<AddIcon className={`${name}__icon`} />
<AddIcon className={`${stepperClass}__plus-icon`} />
</div>
</div>,
</div>
);
};
});

Stepper.defaultProps = defaultProps as StepperProps;
Stepper.displayName = 'Stepper';

export default Stepper;
export default memo(Stepper);
Loading

0 comments on commit 804b5d6

Please sign in to comment.