Skip to content

Commit

Permalink
feat(timepicker): timepicker sync from vue-next, support onPick and p…
Browse files Browse the repository at this point in the history
…resets features
  • Loading branch information
ZWkang committed May 15, 2024
1 parent ea49ef6 commit d2a9f18
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 35 deletions.
27 changes: 19 additions & 8 deletions src/time-picker/TimePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, Ref, useEffect } from 'react';
import React, { useState, Ref, useCallback } from 'react';
import classNames from 'classnames';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
Expand All @@ -12,7 +12,7 @@ import noop from '../_util/noop';

import SelectInput, { SelectInputProps, SelectInputValueChangeContext } from '../select-input';
import TimeRangePicker from './TimeRangePicker';
import TimePickerPanel from './panel/TimePickerPanel';
import TimePickerPanel, { TimePickerPanelProps } from './panel/TimePickerPanel';

import { useTimePickerTextConfig } from './hooks/useTimePickerTextConfig';
import { formatInputValue, validateInputValue } from '../_common/js/time-picker/utils';
Expand Down Expand Up @@ -49,6 +49,7 @@ const TimePicker = forwardRefWithStatics(
onFocus = noop,
onOpen = noop,
onInput = noop,
onPick = noop,
} = props;

const [value, onChange] = useControlled(props, 'value', props.onChange);
Expand All @@ -65,8 +66,16 @@ const TimePicker = forwardRefWithStatics(
[`${classPrefix}-is-focused`]: isPanelShowed,
});

const effectVisibleCurrentValue = useCallback(
(visible: boolean) => {
setPanelShow(visible);
setCurrentValue(visible ? value ?? '' : '');
},
[setPanelShow, setCurrentValue, value],
);

const handleShowPopup = (visible: boolean, context: { e: React.MouseEvent<HTMLDivElement, MouseEvent> }) => {
setPanelShow(visible);
effectVisibleCurrentValue(visible);
visible ? onOpen(context) : onClose(context); // trigger on-open and on-close
};

Expand Down Expand Up @@ -97,12 +106,13 @@ const TimePicker = forwardRefWithStatics(
const handleClickConfirm = () => {
const isValidTime = validateInputValue(currentValue, format);
if (isValidTime) onChange(currentValue);
setPanelShow(false);
effectVisibleCurrentValue(false);
};

useEffect(() => {
setCurrentValue(isPanelShowed ? value ?? '' : '');
}, [isPanelShowed, value]);
const handlePanelChange: TimePickerPanelProps['onChange'] = (v, ctx) => {
setCurrentValue(v);
onPick?.(v, ctx);
};

return (
<div className={classNames(name, className)} ref={ref} style={style}>
Expand Down Expand Up @@ -134,10 +144,11 @@ const TimePicker = forwardRefWithStatics(
isFooterDisplay={true}
isShowPanel={isPanelShowed}
disableTime={disableTime}
onChange={setCurrentValue}
onChange={handlePanelChange}
onPick={props.onPick}
hideDisabledTime={hideDisabledTime}
handleConfirmClick={handleClickConfirm}
presets={props.presets}
/>
}
/>
Expand Down
55 changes: 42 additions & 13 deletions src/time-picker/TimeRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React, { FC, useState, useEffect } from 'react';
import classNames from 'classnames';

import { TimeIcon as TdTimeIcon } from 'tdesign-icons-react';
import isArray from 'lodash/isArray';
import noop from '../_util/noop';
import useControlled from '../hooks/useControlled';
import useConfig from '../hooks/useConfig';
import useGlobalIcon from '../hooks/useGlobalIcon';
import { RangeInputPopup, RangeInputPosition } from '../range-input';
import { RangeInputPopup, RangeInputPopupProps, RangeInputPosition } from '../range-input';
import TimePickerPanel from './panel/TimePickerPanel';

import { useTimePickerTextConfig } from './hooks/useTimePickerTextConfig';
Expand All @@ -20,7 +21,9 @@ import useDefaultProps from '../hooks/useDefaultProps';

export interface TimeRangePickerProps extends TdTimeRangePickerProps, StyledProps {}

const defaultArrVal = [undefined, undefined];
function handlePositionTrans(income: RangeInputPosition): TimeRangePickerPartial {
return income === 'first' ? 'start' : 'end';
}

const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
const props = useDefaultProps<TimeRangePickerProps>(originalProps, timeRangePickerDefaultProps);
Expand All @@ -41,6 +44,7 @@ const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
onInput = noop,
style,
className,
presets,
} = props;

const [value, onChange] = useControlled(props, 'value', props.onChange);
Expand All @@ -59,26 +63,48 @@ const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
[`${classPrefix}-is-focused`]: isPanelShowed,
});

const handleShowPopup = (visible: boolean) => {
const handleShowPopup: RangeInputPopupProps['onPopupVisibleChange'] = (visible: boolean, { trigger }) => {
if (trigger === 'trigger-element-click') {
setPanelShow(true);
return;
}
setPanelShow(visible);
};

function handlePickerValue(pickValue: string | string[], currentValue: string[]) {
if (Array.isArray(pickValue)) return pickValue;
return currentPanelIdx === 0
? [pickValue, currentValue[1] ?? pickValue]
: [currentValue[0] ?? pickValue, pickValue];
}

const handleOnPick = (pickValue: string[], e: { e: React.MouseEvent }) => {
let context;
if (isArray(pickValue)) {
context = { e };
} else if (currentPanelIdx.value === 0) {
context = { e, position: 'start' as TimeRangePickerPartial };
} else {
context = { e, position: 'end' as TimeRangePickerPartial };
}
props.onPick?.(pickValue, context);
};

const handleClear = (context: { e: React.MouseEvent }) => {
const { e } = context;
e.stopPropagation();
onChange(undefined);
setCurrentValue(TIME_PICKER_EMPTY);
};

const handleClick = ({ position }: { position: 'first' | 'second' }) => {
setCurrentPanelIdx(position === 'first' ? 0 : 1);
};

const handleTimeChange = (newValue: string) => {
if (currentPanelIdx === 0) {
setCurrentValue([newValue, currentValue[1] ?? newValue]);
} else {
setCurrentValue([currentValue[0] ?? newValue, newValue]);
}
const handleTimeChange = (newValue: string | string[], context: { e: React.MouseEvent }) => {
const nextCurrentValue = handlePickerValue(newValue, currentValue);
setCurrentValue(nextCurrentValue);
handleOnPick(nextCurrentValue, context);
};

const handleInputBlur = (value: TimeRangeValue, { e }: { e: React.FocusEvent<HTMLInputElement> }) => {
Expand All @@ -99,7 +125,7 @@ const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
{ e, position }: { e: React.FocusEvent<HTMLInputElement>; position: RangeInputPosition },
) => {
setCurrentValue(inputVal);
onInput({ value, e, position: position as TimeRangePickerPartial });
onInput({ value, e, position: handlePositionTrans(position) });
};

const handleClickConfirm = () => {
Expand All @@ -112,11 +138,12 @@ const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
value: TimeRangeValue,
{ e, position }: { e: React.FocusEvent<HTMLInputElement>; position: RangeInputPosition },
) => {
onFocus({ value, e, position: position as TimeRangePickerPartial });
onFocus({ value, e, position: handlePositionTrans(position) });
};

useEffect(() => {
setCurrentValue(isPanelShowed ? value ?? TIME_PICKER_EMPTY : defaultArrVal);
// to fix the effect trigger before input blur
setCurrentValue(isPanelShowed ? value ?? TIME_PICKER_EMPTY : TIME_PICKER_EMPTY);
if (!isPanelShowed) setCurrentPanelIdx(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPanelShowed]);
Expand All @@ -136,7 +163,7 @@ const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
...props.popupProps,
}}
onInputChange={handleInputChange}
inputValue={isPanelShowed ? currentValue : value ?? defaultArrVal}
inputValue={isPanelShowed ? currentValue : value ?? TIME_PICKER_EMPTY}
rangeInputProps={{
size,
clearable,
Expand Down Expand Up @@ -166,6 +193,8 @@ const TimeRangePicker: FC<TimeRangePickerProps> = (originalProps) => {
onChange={handleTimeChange}
handleConfirmClick={handleClickConfirm}
position={currentPanelIdx === 0 ? 'start' : 'end'}
activeIndex={currentPanelIdx}
presets={presets}
/>
}
/>
Expand Down
75 changes: 61 additions & 14 deletions src/time-picker/panel/TimePickerPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo, useState, MouseEvent } from 'react';
import React, { FC, useEffect, useMemo, useState, useCallback } from 'react';
import dayjs from 'dayjs';
import SinglePanel, { SinglePanelProps } from './SinglePanel';

Expand All @@ -7,13 +7,20 @@ import Button from '../../button';

import { useTimePickerTextConfig } from '../hooks/useTimePickerTextConfig';
import { DEFAULT_STEPS, DEFAULT_FORMAT } from '../../_common/js/time-picker/const';
import { TimePickerProps } from '../TimePicker';
import { TimeRangePickerProps } from '../TimeRangePicker';
import log from '../../_common/js/log';

export interface TimePickerPanelProps extends SinglePanelProps {
isShowPanel?: boolean;
isFooterDisplay?: boolean; // 是否展示footer
handleConfirmClick?: (defaultValue: dayjs.Dayjs | string) => void;
presets?: TimePickerProps['presets'] | TimeRangePickerProps['presets'];
activeIndex?: number;
}

type PresetValue = TimePickerPanelProps['presets'][keyof TimePickerPanelProps['presets']];

const TimePickerPanel: FC<TimePickerPanelProps> = (props) => {
const {
format = DEFAULT_FORMAT,
Expand All @@ -23,14 +30,15 @@ const TimePickerPanel: FC<TimePickerPanelProps> = (props) => {
onChange,
value,
isShowPanel = true,
presets = null,
} = props;
const [triggerScroll, toggleTriggerScroll] = useState(false); // 触发滚动
const { classPrefix } = useConfig();

const TEXT_CONFIG = useTimePickerTextConfig();

const panelClassName = `${classPrefix}-time-picker__panel`;
const showNowTimeBtn = !!steps.filter((v) => v > 1).length;
const showNowTimeBtn = !!steps.filter((v) => Number(v) > 1).length;

const defaultValue = useMemo(() => {
const formattedValue = dayjs(value, format);
Expand All @@ -45,43 +53,82 @@ const TimePickerPanel: FC<TimePickerPanelProps> = (props) => {
if (isShowPanel) toggleTriggerScroll(true);
}, [isShowPanel]);

const handleOnChange = (v: string, e: MouseEvent<HTMLDivElement>) => {
props.onChange(v);
props.onPick?.(v, { e });
const resetTriggerScroll = useCallback(() => {
toggleTriggerScroll(false);
}, [toggleTriggerScroll]);

const handlePresetClick = (presetValue: PresetValue) => {
const presetVal = typeof presetValue === 'function' ? presetValue() : presetValue;
if (typeof props.activeIndex === 'number') {
if (Array.isArray(presetVal)) {
props.onChange?.(presetVal[props.activeIndex]);
} else {
log.error('TimePicker', `preset: ${presets} 预设值必须是数组!`);
}
} else {
props.onChange?.(presetVal);
}
};

const renderFooter = () => {
if (presets) {
return Object.keys(presets).map((preset) => (
<Button
key={preset}
theme="primary"
size="small"
variant="text"
onClick={() => {
handlePresetClick(presets[preset]);
}}
>
{preset}
</Button>
));
}

return !showNowTimeBtn ? (
<Button
theme="primary"
variant="text"
size="small"
onClick={() => {
onChange?.(dayjs().format(format));
}}
>
{TEXT_CONFIG.nowTime}
</Button>
) : null;
};

return (
<div className={panelClassName}>
<div className={`${panelClassName}-section-body`}>
<SinglePanel
{...props}
onChange={handleOnChange}
onChange={onChange}
format={format}
steps={steps}
value={dayjs(value, format).isValid() ? value : defaultValue}
triggerScroll={triggerScroll}
isVisible={isShowPanel}
resetTriggerScroll={() => toggleTriggerScroll(false)}
resetTriggerScroll={resetTriggerScroll}
/>
</div>
{isFooterDisplay ? (
<div className={`${panelClassName}-section-footer`}>
<Button
size="small"
theme="primary"
variant="base"
disabled={!props.value}
onClick={() => {
handleConfirmClick(defaultValue);
handleConfirmClick?.(defaultValue);
}}
size="small"
>
{TEXT_CONFIG.confirm}
</Button>
{!showNowTimeBtn ? (
<Button theme="primary" variant="text" size="small" onClick={() => onChange(dayjs().format(format))}>
{TEXT_CONFIG.nowTime}
</Button>
) : null}
{renderFooter()}
</div>
) : null}
</div>
Expand Down

0 comments on commit d2a9f18

Please sign in to comment.