From 989e2078fba5e8dab32a812db46701793f0abfeb Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 11:26:30 +0800 Subject: [PATCH 01/25] input move into hooks --- .eslintrc.js | 1 + src/Picker.tsx | 155 +++--------- src/RangePicker.tsx | 473 +++++------------------------------- src/hooks/useMergeState.ts | 35 +++ src/hooks/usePickerInput.ts | 125 ++++++++++ 5 files changed, 260 insertions(+), 529 deletions(-) create mode 100644 src/hooks/useMergeState.ts create mode 100644 src/hooks/usePickerInput.ts diff --git a/.eslintrc.js b/.eslintrc.js index f1b8be4f8..9b2ea05f3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,7 @@ module.exports = { 'react/sort-comp': 0, '@typescript-eslint/no-explicit-any': 0, 'default-case': 0, + 'no-confusing-arrow': 0, 'jsx-a11y/no-autofocus': 0, 'import/no-extraneous-dependencies': [ 'error', diff --git a/src/Picker.tsx b/src/Picker.tsx index a5f49cfcd..1fd572e2a 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -12,7 +12,6 @@ */ import * as React from 'react'; -import KeyCode from 'rc-util/lib/KeyCode'; import classNames from 'classnames'; import { AlignType } from 'rc-trigger/lib/interface'; import PickerPanel, { @@ -25,11 +24,8 @@ import { isEqual } from './utils/dateUtil'; import getDataOrAriaProps, { toArray } from './utils/miscUtil'; import PanelContext, { ContextOperationRefProps } from './PanelContext'; import { PickerMode } from './interface'; -import { - getDefaultFormat, - getInputSize, - addGlobalMouseDownEvent, -} from './utils/uiUtil'; +import { getDefaultFormat, getInputSize } from './utils/uiUtil'; +import usePickerInput from './hooks/usePickerInput'; export interface PickerRefConfig { focus: () => void; @@ -47,6 +43,7 @@ export interface PickerSharedProps extends React.AriaAttributes { autoFocus?: boolean; disabled?: boolean; open?: boolean; + defaultOpen?: boolean; /** Make input readOnly to avoid popup keyboard in mobile */ inputReadOnly?: boolean; @@ -134,6 +131,7 @@ function InnerPicker(props: PickerProps) { value, defaultValue, open, + defaultOpen, suffixIcon, clearIcon, disabled, @@ -192,7 +190,6 @@ function InnerPicker(props: PickerProps) { ) : '', ); - const [typing, setTyping] = React.useState(false); /** Similar as `setTextValue` but accept `DateType` and convert into string */ const setDateText = (date: DateType | null) => { @@ -209,7 +206,12 @@ function InnerPicker(props: PickerProps) { >(null); // Trigger - const [innerOpen, setInnerOpen] = React.useState(false); + const [innerOpen, setInnerOpen] = React.useState(() => { + if (defaultOpen !== undefined) { + return defaultOpen; + } + return false; + }); let mergedOpen: boolean; if (disabled) { mergedOpen = false; @@ -230,9 +232,6 @@ function InnerPicker(props: PickerProps) { } }; - // Focus - const [focused, setFocused] = React.useState(false); - // ============================= Value ============================= const isSameTextDate = (text: string, date: DateType | null) => { if (date === null) { @@ -255,11 +254,6 @@ function InnerPicker(props: PickerProps) { setInternalSelectedValue(newDate); }; - const onInputMouseDown: React.MouseEventHandler = () => { - triggerOpen(true); - setTyping(true); - }; - const onInputChange: React.ChangeEventHandler = e => { const text = e.target.value; setTextValue(text); @@ -290,95 +284,42 @@ function InnerPicker(props: PickerProps) { }; const forwardKeyDown = (e: React.KeyboardEvent) => { - if ( - !typing && - mergedOpen && - operationRef.current && - operationRef.current.onKeyDown - ) { + if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { // Let popup panel handle keyboard return operationRef.current.onKeyDown(e); } return false; }; - const onInputKeyDown: React.KeyboardEventHandler = e => { - switch (e.which) { - case KeyCode.ENTER: { - if (!mergedOpen) { - triggerOpen(true); - } else { - triggerChange(selectedValue); - triggerOpen(false); - setTyping(true); - } - return; - } - - case KeyCode.TAB: { - if (typing && mergedOpen && !e.shiftKey) { - setTyping(false); - e.preventDefault(); - } else if (!typing && mergedOpen) { - if (!forwardKeyDown(e) && e.shiftKey) { - setTyping(true); - e.preventDefault(); - } - } - return; - } - - case KeyCode.ESC: { - triggerChange(mergedValue); - setSelectedValue(mergedValue); - triggerOpen(false); - setTyping(true); - return; - } - } - - if (!mergedOpen && ![KeyCode.SHIFT].includes(e.which)) { - triggerOpen(true); - } else { - // Let popup panel handle keyboard - forwardKeyDown(e); - } - }; - - const onInputFocus: React.FocusEventHandler = e => { - setTyping(true); - setFocused(true); - - if (onFocus) { - onFocus(e); - } - }; - - /** - * We will prevent blur to handle open event when user click outside, - * since this will repeat trigger `onOpenChange` event. - */ - const preventBlurRef = React.useRef(false); - const triggerClose = () => { triggerOpen(false); setInnerValue(selectedValue); triggerChange(selectedValue); }; - const onInputBlur: React.FocusEventHandler = e => { - if (preventBlurRef.current) { - preventBlurRef.current = false; - return; - } - - triggerClose(); - setFocused(false); - - if (onBlur) { - onBlur(e); - } - }; + const [inputProps, { focused, typing }] = usePickerInput({ + open: mergedOpen, + triggerOpen, + triggerClose, + forwardKeyDown, + isClickOutside: target => + !!( + panelDivRef.current && + !panelDivRef.current.contains(target as Node) && + inputDivRef.current && + !inputDivRef.current.contains(target as Node) && + onOpenChange + ), + onSubmit: () => { + triggerChange(selectedValue); + }, + onCancel: () => { + triggerChange(mergedValue); + setSelectedValue(mergedValue); + }, + onFocus, + onBlur, + }); // ============================= Sync ============================== // Close should sync back with text value @@ -402,28 +343,6 @@ function InnerPicker(props: PickerProps) { } }, [mergedValue]); - // Global click handler - React.useEffect(() => - addGlobalMouseDownEvent(({ target }: MouseEvent) => { - if ( - mergedOpen && - panelDivRef.current && - !panelDivRef.current.contains(target as Node) && - inputDivRef.current && - !inputDivRef.current.contains(target as Node) && - onOpenChange - ) { - preventBlurRef.current = true; - triggerClose(); - - // Always set back in case `onBlur` prevented by user - window.setTimeout(() => { - preventBlurRef.current = false; - }, 0); - } - }), - ); - // ============================ Private ============================ if (pickerRef) { pickerRef.current = { @@ -523,15 +442,11 @@ function InnerPicker(props: PickerProps) { diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 8ed74f1b2..6f6c1d3d4 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -1,42 +1,16 @@ -/** - * TODO: - * - Highlight range when hover the ranges value - * - Click ranges value will go to the related panel - */ - import * as React from 'react'; import classNames from 'classnames'; -import Picker, { - PickerBaseProps, - PickerDateProps, - PickerTimeProps, - PickerRefConfig, -} from './Picker'; -import { - NullableDateType, - DisabledTimes, - DisabledTime, - PickerMode, - PanelMode, - OnPanelChange, -} from './interface'; -import { toArray } from './utils/miscUtil'; -import RangeContext from './RangeContext'; -import { isSameDate } from './utils/dateUtil'; -import { getDefaultFormat } from './utils/uiUtil'; +import { DisabledTimes, PanelMode } from './interface'; +import { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker'; import { SharedTimeProps } from './panels/TimePanel'; +import useMergedState from './hooks/useMergeState'; +import PickerTrigger from './PickerTrigger'; +import PickerPanel from './PickerPanel'; type EventValue = DateType | null; -type RangeValue = [EventValue, EventValue] | null; - -function canTriggerChange( - dates: [EventValue, EventValue], - allowEmpty?: [boolean, boolean], -): boolean { - const passStart = dates[0] || (allowEmpty && allowEmpty[0]); - const passEnd = dates[1] || (allowEmpty && allowEmpty[1]); - return !!(passStart && passEnd); -} +type RangeValue = + | [EventValue | null, EventValue | null] + | null; export interface RangePickerSharedProps { value?: RangeValue; @@ -110,404 +84,85 @@ export type RangePickerProps = | RangePickerDateProps | RangePickerTimeProps; -interface MergedRangePickerProps - extends Omit< - RangePickerBaseProps & - RangePickerDateProps & - RangePickerTimeProps, - 'picker' - > { - picker?: PickerMode; -} - -function InternalRangePicker( - props: RangePickerProps & { - pickerRef: React.Ref; - }, -) { +function RangePicker(props: RangePickerProps) { const { prefixCls = 'rc-picker', - className, style, + className, + popupStyle, + dropdownClassName, + transitionName, + dropdownAlign, + getPopupContainer, + generateConfig, + locale, + separator = '~', value, defaultValue, - defaultPickerValue, - separator = '~', - mode, - picker, - pickerRef, - locale, - generateConfig, - placeholder, - showTime, - use12Hours, - disabledTime, - ranges, - format, - allowEmpty, - selectable, - disabled, + open, + defaultOpen, onChange, - onCalendarChange, - onPanelChange, - onFocus, - onBlur, - } = props as MergedRangePickerProps & { - pickerRef: React.MutableRefObject; - }; - - const formatList = toArray( - getDefaultFormat(format, picker, showTime, use12Hours), - ); - - const [startShowTime, endShowTime] = React.useMemo(() => { - if (showTime && typeof showTime === 'object' && showTime.defaultValue) { - return [ - { ...showTime, defaultValue: showTime.defaultValue[0] }, - { ...showTime, defaultValue: showTime.defaultValue[1] }, - ]; - } - return [showTime, showTime]; - }, [showTime]); + } = props; - const mergedSelectable = React.useMemo< - [boolean | undefined, boolean | undefined] - >(() => [selectable && selectable[0], selectable && selectable[1]], [ - selectable, - ]); - - // ============================= Values ============================= - const [innerValue, setInnerValue] = React.useState>( - () => { - if (value !== undefined) { - return value; - } - if (defaultValue !== undefined) { - return defaultValue; - } - return null; + // ======================== States ======================== + // Value + const [mergedValue, triggerValueChange] = useMergedState< + RangeValue + >({ + value, + defaultValue, + defaultStateValue: null, + onChange: (nextValue, prevValue) => { + // TODO: handle this + console.log('Value Changed!', nextValue, prevValue); }, - ); - - const mergedValue = value !== undefined ? value : innerValue; - - // Get picker value, should order this internally - const [value1, value2] = React.useMemo(() => { - let val1 = mergedValue ? mergedValue[0] : null; - let val2 = mergedValue ? mergedValue[1] : null; - - // Exchange - if (val1 && val2 && generateConfig.isAfter(val1, val2)) { - const tmp = val1; - val1 = val2; - val2 = tmp; - } - - return [val1, val2]; - }, [mergedValue]); - - // Select value: used for click to update ranged value. Must set in pair - const [selectedValues, setSelectedValues] = React.useState< - [DateType | null, DateType | null] | undefined - >(undefined); - - React.useEffect(() => { - setSelectedValues([value1, value2]); - }, [value1, value2]); - - const onStartSelect = (date: DateType) => { - setSelectedValues([date, value2]); - }; - - const onEndSelect = (date: DateType) => { - setSelectedValues([value1, date]); - }; - - // ============================= Change ============================= - const formatDate = (date: NullableDateType) => { - if (date) { - return generateConfig.locale.format(locale.locale, date, formatList[0]); - } - return ''; - }; - - const onInternalChange = ( - values: NullableDateType[], - changedByStartTime: boolean, - ) => { - const startDate: DateType | null = values[0] || null; - let endDate: DateType | null = values[1] || null; - - // If user change start time is after end time, should reset end time to null - if ( - startDate && - endDate && - !isSameDate(generateConfig, startDate, endDate) && - generateConfig.isAfter(startDate, endDate) && - changedByStartTime - ) { - endDate = null; - } - - setInnerValue([startDate, endDate]); - - const startStr = formatDate(startDate); - const endStr = formatDate(endDate); - - if (onChange && canTriggerChange([startDate, endDate], allowEmpty)) { - onChange([startDate, endDate], [startStr, endStr]); - } - - if (onCalendarChange) { - onCalendarChange([startDate, endDate], [startStr, endStr]); - } - }; - - // ============================== Open ============================== - const startPickerRef = React.useRef>(null); - const endPickerRef = React.useRef>(null); - const lastOpenIdRef = React.useRef(); - - const onStartOpenChange = (open: boolean) => { - if (!open && selectedValues && selectedValues[0]) { - lastOpenIdRef.current = window.setTimeout(() => { - if (endPickerRef.current) { - endPickerRef.current!.focus(); - endPickerRef.current!.open(); - } - }, 100); - } - - if (props.onOpenChange) { - props.onOpenChange(open); - } - }; + }); - React.useEffect( - () => () => { - window.clearTimeout(lastOpenIdRef.current); + // Open + const [mergedOpen, triggerOpenChange] = useMergedState({ + value: open, + defaultValue: defaultOpen, + defaultStateValue: false, + onChange: nextOpen => { + console.log('Open Changed!', nextOpen); }, - [], - ); - - if (pickerRef) { - pickerRef.current = startPickerRef.current as any; - } - - // ============================== Mode ============================== - /** - * [Legacy] handle internal `onPanelChange` - */ - const [innerModes, setInnerModes] = React.useState((): [ - PanelMode, - PanelMode, - ] => { - if (mode) { - return mode; - } - if (picker) { - return [picker, picker]; - } - return showTime ? ['datetime', 'datetime'] : ['date', 'date']; }); - const [onStartPanelChange, onEndPanelChange] = React.useMemo< - [OnPanelChange | undefined, OnPanelChange | undefined] - >(() => { - const onInternalPanelChange = ( - newValue: DateType, - newMode: PanelMode, - source: 'start' | 'end', - ) => { - const values: [EventValue, EventValue] = [ - ...(mergedValue || []), - ] as [EventValue, EventValue]; - const modes: [PanelMode, PanelMode] = [...innerModes] as [ - PanelMode, - PanelMode, - ]; - - if (source === 'start') { - values[0] = newValue; - modes[0] = newMode; - } else { - values[1] = newValue; - modes[1] = newMode; - } - setInnerModes(modes); - - if (onPanelChange) { - onPanelChange(values, modes); - } - }; - - return [ - (newVal: DateType, newMode: PanelMode) => { - onInternalPanelChange(newVal, newMode, 'start'); - }, - (newVal: DateType, newMode: PanelMode) => { - onInternalPanelChange(newVal, newMode, 'end'); - }, - ]; - }, [onPanelChange, mode, picker]); - - React.useEffect(() => { - if (mode) { - setInnerModes(mode); - } - }, [mode]); - - // ============================= Render ============================= - const pickerProps = { - ...props, - defaultValue: undefined, - defaultPickerValue: undefined, - className: undefined, - style: undefined, - placeholder: undefined, - disabledTime: undefined, - onPanelChange: undefined, - }; - - // Time - let disabledStartTime: DisabledTime | undefined; - let disabledEndTime: DisabledTime | undefined; - if (disabledTime) { - disabledStartTime = (date: DateType | null) => disabledTime(date, 'start'); - disabledEndTime = (date: DateType | null) => disabledTime(date, 'end'); - } + // ======================== Typing ======================== - // Ranges - let extraFooterSelections: - | { - label: string; - onClick: React.MouseEventHandler; - }[] - | undefined; - if (ranges) { - extraFooterSelections = Object.keys(ranges).map(label => ({ - label, - onClick: () => { - const rangedValue = ranges[label]; - onInternalChange( - typeof rangedValue === 'function' ? rangedValue() : rangedValue, - false, - ); - }, - })); - } - - // End date should disabled before start date - const { disabledDate } = pickerProps; - - const disabledStartDate = (date: DateType) => { - let mergedDisabled = disabledDate ? disabledDate(date) : false; - - if (mergedSelectable[1] === false && value2) { - mergedDisabled = - !isSameDate(generateConfig, date, value2) && - generateConfig.isAfter(date, value2); - } - - return mergedDisabled; - }; - - const disabledEndDate = (date: DateType) => { - let mergedDisabled = disabledDate ? disabledDate(date) : false; - - if (!mergedDisabled && value1) { - // Can be the same date - mergedDisabled = - !isSameDate(generateConfig, value1, date) && - generateConfig.isAfter(value1, date); - } - - return mergedDisabled; - }; + // ======================== Panels ======================== + const panel = ( + + generateConfig={generateConfig} + locale={locale} + tabIndex={-1} + onMouseDown={e => { + e.preventDefault(); + }} + /> + ); return ( -
- - {...pickerProps} - ref={startPickerRef} - prefixCls={prefixCls} - value={value1} - placeholder={placeholder && placeholder[0]} - defaultPickerValue={defaultPickerValue && defaultPickerValue[0]} - {...{ disabledTime: disabledStartTime, showTime: startShowTime }} // Fix ts define - mode={mode && mode[0]} - disabled={disabled || mergedSelectable[0] === false} - disabledDate={disabledStartDate} - onChange={date => { - onInternalChange([date, value2], true); - }} - onSelect={onStartSelect} - onFocus={onFocus} - onBlur={onBlur} - onPanelChange={onStartPanelChange} - onOpenChange={onStartOpenChange} - /> + {separator} - - {...pickerProps} - ref={endPickerRef} - prefixCls={prefixCls} - value={value2} - placeholder={placeholder && placeholder[1]} - defaultPickerValue={defaultPickerValue && defaultPickerValue[1]} - {...{ disabledTime: disabledEndTime, showTime: endShowTime }} // Fix ts define - mode={mode && mode[1]} - disabled={disabled || mergedSelectable[1] === false} - disabledDate={disabledEndDate} - onChange={date => { - onInternalChange([value1, date], false); - }} - onSelect={onEndSelect} - onFocus={onFocus} - onBlur={onBlur} - onPanelChange={onEndPanelChange} - /> +
-
+ ); } -// Wrap with class component to enable pass generic with instance method -class RangePicker extends React.Component< - RangePickerProps -> { - pickerRef = React.createRef(); - - focus = () => { - if (this.pickerRef.current) { - this.pickerRef.current.focus(); - } - }; - - blur = () => { - if (this.pickerRef.current) { - this.pickerRef.current.blur(); - } - }; - - render() { - return ( - - {...this.props} - pickerRef={this.pickerRef as React.MutableRefObject} - /> - ); - } -} - export default RangePicker; diff --git a/src/hooks/useMergeState.ts b/src/hooks/useMergeState.ts new file mode 100644 index 000000000..1f35f2e69 --- /dev/null +++ b/src/hooks/useMergeState.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; + +export default function useMergedState({ + value, + defaultValue, + defaultStateValue, + onChange, +}: { + value?: T; + defaultValue?: T; + defaultStateValue: T; + onChange?: (value: T, prevValue: T) => void; +}): [T, (value: T) => void] { + const [innerValue, setInnerValue] = React.useState(() => { + if (value !== undefined) { + return value; + } + if (defaultValue !== undefined) { + return defaultValue; + } + return defaultStateValue; + }); + + const mergedValue = value !== undefined ? value : innerValue; + + function triggerChange(newValue: T) { + setInnerValue(newValue); + + if (onChange) { + onChange(newValue, mergedValue); + } + } + + return [mergedValue, triggerChange]; +} diff --git a/src/hooks/usePickerInput.ts b/src/hooks/usePickerInput.ts new file mode 100644 index 000000000..b2f13e877 --- /dev/null +++ b/src/hooks/usePickerInput.ts @@ -0,0 +1,125 @@ +import * as React from 'react'; +import KeyCode from 'rc-util/lib/KeyCode'; +import { addGlobalMouseDownEvent } from '../utils/uiUtil'; + +export default function usePickerInput({ + open, + isClickOutside, + triggerOpen, + triggerClose, + forwardKeyDown, + onSubmit, + onCancel, + onFocus, + onBlur, +}: { + open: boolean; + isClickOutside: (clickElement: EventTarget | null) => boolean; + triggerOpen: (open: boolean) => void; + triggerClose: () => void; + forwardKeyDown: (e: React.KeyboardEvent) => boolean; + onSubmit: () => void; + onCancel: () => void; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; +}): [ + React.DOMAttributes, + { focused: boolean; typing: boolean }, +] { + const [typing, setTyping] = React.useState(false); + const [focused, setFocused] = React.useState(false); + + /** + * We will prevent blur to handle open event when user click outside, + * since this will repeat trigger `onOpenChange` event. + */ + const preventBlurRef = React.useRef(false); + + const inputProps: React.DOMAttributes = { + onMouseDown: () => { + setTyping(true); + triggerOpen(true); + }, + onKeyDown: e => { + switch (e.which) { + case KeyCode.ENTER: { + if (!open) { + triggerOpen(true); + } else { + onSubmit(); + triggerOpen(false); + setTyping(true); + } + return; + } + + case KeyCode.TAB: { + if (typing && open && !e.shiftKey) { + setTyping(false); + e.preventDefault(); + } else if (!typing && open) { + if (!forwardKeyDown(e) && e.shiftKey) { + setTyping(true); + e.preventDefault(); + } + } + return; + } + + case KeyCode.ESC: { + triggerOpen(false); + setTyping(true); + onCancel(); + return; + } + } + + if (!open && ![KeyCode.SHIFT].includes(e.which)) { + triggerOpen(true); + } else if (!typing) { + // Let popup panel handle keyboard + forwardKeyDown(e); + } + }, + + onFocus: e => { + setTyping(true); + setFocused(true); + + if (onFocus) { + onFocus(e); + } + }, + + onBlur: e => { + if (preventBlurRef.current) { + preventBlurRef.current = false; + return; + } + + triggerClose(); + setFocused(false); + + if (onBlur) { + onBlur(e); + } + }, + }; + + // Global click handler + React.useEffect(() => + addGlobalMouseDownEvent(({ target }: MouseEvent) => { + if (open && isClickOutside(target)) { + preventBlurRef.current = true; + triggerClose(); + + // Always set back in case `onBlur` prevented by user + window.setTimeout(() => { + preventBlurRef.current = false; + }, 0); + } + }), + ); + + return [inputProps, { focused, typing }]; +} From 738777c7f5ada5e47560a7ee12b39caae7898021 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 12:56:06 +0800 Subject: [PATCH 02/25] text sync with hooks --- src/Picker.tsx | 91 +++++++++++--------------------- src/hooks/useTextValueMapping.ts | 29 ++++++++++ 2 files changed, 61 insertions(+), 59 deletions(-) create mode 100644 src/hooks/useTextValueMapping.ts diff --git a/src/Picker.tsx b/src/Picker.tsx index 1fd572e2a..0058d1e31 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -26,6 +26,7 @@ import PanelContext, { ContextOperationRefProps } from './PanelContext'; import { PickerMode } from './interface'; import { getDefaultFormat, getInputSize } from './utils/uiUtil'; import usePickerInput from './hooks/usePickerInput'; +import useTextValueMapping from './hooks/useTextValueMapping'; export interface PickerRefConfig { focus: () => void; @@ -180,26 +181,6 @@ function InnerPicker(props: PickerProps) { setInternalSelectedValue, ] = React.useState(mergedValue); - // Text - const [textValue, setTextValue] = React.useState( - selectedValue - ? generateConfig.locale.format( - locale.locale, - selectedValue, - formatList[0], - ) - : '', - ); - - /** Similar as `setTextValue` but accept `DateType` and convert into string */ - const setDateText = (date: DateType | null) => { - setTextValue( - date === null - ? '' - : generateConfig.locale.format(locale.locale, date, formatList[0]), - ); - }; - // Operation ref const operationRef: React.MutableRefObject = React.useRef< ContextOperationRefProps @@ -232,42 +213,11 @@ function InnerPicker(props: PickerProps) { } }; - // ============================= Value ============================= - const isSameTextDate = (text: string, date: DateType | null) => { - if (date === null) { - return !text; - } - - const inputDate = generateConfig.locale.parse( - locale.locale, - text, - formatList, - ); - return isEqual(generateConfig, inputDate, date); - }; - // =========================== Formatter =========================== const setSelectedValue = (newDate: DateType | null) => { - if (!isSameTextDate(textValue, newDate)) { - setDateText(newDate); - } setInternalSelectedValue(newDate); }; - const onInputChange: React.ChangeEventHandler = e => { - const text = e.target.value; - setTextValue(text); - - const inputDate = generateConfig.locale.parse( - locale.locale, - text, - formatList, - ); - if (inputDate && (!disabledDate || !disabledDate(inputDate))) { - setSelectedValue(inputDate); - } - }; - // ============================ Trigger ============================ const triggerChange = (newValue: DateType | null) => { setSelectedValue(newValue); @@ -297,6 +247,31 @@ function InnerPicker(props: PickerProps) { triggerChange(selectedValue); }; + // ============================= Text ============================== + const valueTexts = React.useMemo(() => { + if (!selectedValue) { + return ['']; + } + return formatList.map(subFormat => + generateConfig.locale.format(locale.locale, selectedValue, subFormat), + ); + }, [selectedValue]); + + const [text, setText, resetText] = useTextValueMapping({ + valueTexts, + onTextChange: newText => { + const inputDate = generateConfig.locale.parse( + locale.locale, + newText, + formatList, + ); + if (inputDate && (!disabledDate || !disabledDate(inputDate))) { + setSelectedValue(inputDate); + } + }, + }); + + // ============================= Input ============================= const [inputProps, { focused, typing }] = usePickerInput({ open: mergedOpen, triggerOpen, @@ -324,8 +299,8 @@ function InnerPicker(props: PickerProps) { // ============================= Sync ============================== // Close should sync back with text value React.useEffect(() => { - if (!mergedOpen && !isSameTextDate(textValue, mergedValue)) { - setDateText(mergedValue); + if (!mergedOpen) { + resetText(); } }, [mergedOpen]); @@ -336,11 +311,6 @@ function InnerPicker(props: PickerProps) { setInnerValue(mergedValue); setSelectedValue(mergedValue); } - - // Sync text - if (!isSameTextDate(textValue, mergedValue)) { - setDateText(mergedValue); - } }, [mergedValue]); // ============================ Private ============================ @@ -442,7 +412,10 @@ function InnerPicker(props: PickerProps) { { + setText(e.target.value); + }} autoFocus={autoFocus} placeholder={placeholder} ref={inputRef} diff --git a/src/hooks/useTextValueMapping.ts b/src/hooks/useTextValueMapping.ts new file mode 100644 index 000000000..c8294287e --- /dev/null +++ b/src/hooks/useTextValueMapping.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; + +export default function useTextValueMapping({ + valueTexts, + onTextChange, +}: { + /** Must useMemo, to assume that `valueTexts` only match on the first change */ + valueTexts: string[]; + onTextChange: (text: string) => void; +}): [string, (text: string) => void, () => void] { + const [text, setInnerText] = React.useState(''); + + function setText(newText: string) { + setInnerText(newText); + onTextChange(newText); + } + + function resetText() { + setInnerText(valueTexts[0]); + } + + React.useEffect(() => { + if (valueTexts.every(valText => valText !== text)) { + setInnerText(valueTexts[0]); + } + }, [valueTexts.join('||')]); + + return [text, setText, resetText]; +} From d9e3f2411de12f102d242d085f9b95395cdb5fa9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 14:49:17 +0800 Subject: [PATCH 03/25] merged range picker --- src/Picker.tsx | 84 +++---- src/RangePicker.tsx | 372 +++++++++++++++++++++++++++++-- src/hooks/useMergeState.ts | 9 +- src/hooks/useTextValueMapping.ts | 16 +- src/hooks/useValueTexts.ts | 25 +++ 5 files changed, 417 insertions(+), 89 deletions(-) create mode 100644 src/hooks/useValueTexts.ts diff --git a/src/Picker.tsx b/src/Picker.tsx index 0058d1e31..e08f50784 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -27,6 +27,8 @@ import { PickerMode } from './interface'; import { getDefaultFormat, getInputSize } from './utils/uiUtil'; import usePickerInput from './hooks/usePickerInput'; import useTextValueMapping from './hooks/useTextValueMapping'; +import useMergedState from './hooks/useMergeState'; +import useValueTexts from './hooks/useValueTexts'; export interface PickerRefConfig { focus: () => void; @@ -164,45 +166,29 @@ function InnerPicker(props: PickerProps) { const inputDivRef = React.useRef(null); // Real value - const [innerValue, setInnerValue] = React.useState(() => { - if (value !== undefined) { - return value; - } - if (defaultValue !== undefined) { - return defaultValue; - } - return null; + const [mergedValue, setInnerValue] = useMergedState({ + value, + defaultValue, + defaultStateValue: null, }); - const mergedValue = value !== undefined ? value : innerValue; // Selected value - const [ - selectedValue, - setInternalSelectedValue, - ] = React.useState(mergedValue); + const [selectedValue, setSelectedValue] = React.useState( + mergedValue, + ); // Operation ref const operationRef: React.MutableRefObject = React.useRef< ContextOperationRefProps >(null); - // Trigger - const [innerOpen, setInnerOpen] = React.useState(() => { - if (defaultOpen !== undefined) { - return defaultOpen; - } - return false; - }); - let mergedOpen: boolean; - if (disabled) { - mergedOpen = false; - } else { - mergedOpen = typeof open === 'boolean' ? open : innerOpen; - } - - const triggerOpen = (newOpen: boolean) => { - if (mergedOpen !== newOpen) { - setInnerOpen(newOpen); + // Open + const [mergedOpen, triggerOpen] = useMergedState({ + value: open, + defaultValue: defaultOpen, + defaultStateValue: false, + postState: postOpen => (disabled ? false : postOpen), + onChange: newOpen => { if (onOpenChange) { onOpenChange(newOpen); } @@ -210,13 +196,8 @@ function InnerPicker(props: PickerProps) { if (!newOpen && operationRef.current && operationRef.current.onClose) { operationRef.current.onClose(); } - } - }; - - // =========================== Formatter =========================== - const setSelectedValue = (newDate: DateType | null) => { - setInternalSelectedValue(newDate); - }; + }, + }); // ============================ Trigger ============================ const triggerChange = (newValue: DateType | null) => { @@ -243,21 +224,17 @@ function InnerPicker(props: PickerProps) { const triggerClose = () => { triggerOpen(false); - setInnerValue(selectedValue); triggerChange(selectedValue); }; // ============================= Text ============================== - const valueTexts = React.useMemo(() => { - if (!selectedValue) { - return ['']; - } - return formatList.map(subFormat => - generateConfig.locale.format(locale.locale, selectedValue, subFormat), - ); - }, [selectedValue]); + const valueTexts = useValueTexts(selectedValue, { + formatList, + generateConfig, + locale, + }); - const [text, setText, resetText] = useTextValueMapping({ + const [text, triggerTextChange] = useTextValueMapping({ valueTexts, onTextChange: newText => { const inputDate = generateConfig.locale.parse( @@ -300,17 +277,14 @@ function InnerPicker(props: PickerProps) { // Close should sync back with text value React.useEffect(() => { if (!mergedOpen) { - resetText(); + setSelectedValue(mergedValue); } }, [mergedOpen]); // Sync innerValue with control mode React.useEffect(() => { - if (!isEqual(generateConfig, mergedValue, innerValue)) { - // Sync inner & select value - setInnerValue(mergedValue); - setSelectedValue(mergedValue); - } + // Sync select value + setSelectedValue(mergedValue); }, [mergedValue]); // ============================ Private ============================ @@ -413,9 +387,7 @@ function InnerPicker(props: PickerProps) { disabled={disabled} readOnly={inputReadOnly || !typing} value={text} - onChange={e => { - setText(e.target.value); - }} + onChange={triggerTextChange} autoFocus={autoFocus} placeholder={placeholder} ref={inputRef} diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 6f6c1d3d4..521033010 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -1,16 +1,64 @@ import * as React from 'react'; import classNames from 'classnames'; -import { DisabledTimes, PanelMode } from './interface'; +import { DisabledTimes, PanelMode, PickerMode } from './interface'; import { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker'; import { SharedTimeProps } from './panels/TimePanel'; import useMergedState from './hooks/useMergeState'; import PickerTrigger from './PickerTrigger'; import PickerPanel from './PickerPanel'; +import usePickerInput from './hooks/usePickerInput'; +import getDataOrAriaProps, { toArray } from './utils/miscUtil'; +import { getDefaultFormat, getInputSize } from './utils/uiUtil'; +import { ContextOperationRefProps } from './PanelContext'; +import { isEqual } from './utils/dateUtil'; +import useValueTexts from './hooks/useValueTexts'; +import useTextValueMapping from './hooks/useTextValueMapping'; +import { GenerateConfig } from './generate'; type EventValue = DateType | null; -type RangeValue = - | [EventValue | null, EventValue | null] - | null; +type RangeValue = [EventValue, EventValue] | null; + +function getIndexValue( + values: null | undefined | [T | null, T | null], + index: number, +): T | null { + return values ? values[index] : null; +} + +function updateRangeValue( + values: RangeValue, + value: DateType, + index: number, +) { + const newValues: RangeValue = [ + getIndexValue(values, 0), + getIndexValue(values, 1), + ]; + + newValues[index] = value; + + if (!newValues[0] && !newValues[1]) { + return null; + } + + return newValues; +} + +function reorderValues( + values: RangeValue, + generateConfig: GenerateConfig, +): RangeValue { + if ( + values && + values[0] && + values[1] && + generateConfig.isAfter(values[0], values[1]) + ) { + return [values[1], values[0]]; + } + + return values; +} export interface RangePickerSharedProps { value?: RangeValue; @@ -84,6 +132,16 @@ export type RangePickerProps = | RangePickerDateProps | RangePickerTimeProps; +interface MergedRangePickerProps + extends Omit< + RangePickerBaseProps & + RangePickerDateProps & + RangePickerTimeProps, + 'picker' + > { + picker?: PickerMode; +} + function RangePicker(props: RangePickerProps) { const { prefixCls = 'rc-picker', @@ -96,52 +154,295 @@ function RangePicker(props: RangePickerProps) { getPopupContainer, generateConfig, locale, + placeholder, + autoFocus, + disabled, + format, + picker, + showTime, + use12Hours, separator = '~', value, defaultValue, open, defaultOpen, + disabledDate, + allowClear, + suffixIcon, + clearIcon, + inputReadOnly, onChange, - } = props; + onOpenChange, + onFocus, + onBlur, + } = props as MergedRangePickerProps; - // ======================== States ======================== - // Value - const [mergedValue, triggerValueChange] = useMergedState< - RangeValue - >({ + const startInputRef = React.useRef(null); + const endInputRef = React.useRef(null); + + // ======================== State ======================== + const formatList = toArray( + getDefaultFormat(format, picker, showTime, use12Hours), + ); + + // Active picker + const [activePickerIndex, setActivePickerIndex] = React.useState<0 | 1>(0); + + // Panel ref + const panelDivRef = React.useRef(null); + const startInputDivRef = React.useRef(null); + const endInputDivRef = React.useRef(null); + + // Real value + const [mergedValue, setInnerValue] = useMergedState({ value, defaultValue, defaultStateValue: null, - onChange: (nextValue, prevValue) => { - // TODO: handle this - console.log('Value Changed!', nextValue, prevValue); - }, + postState: values => reorderValues(values, generateConfig), }); + // Selected value + const [selectedValue, setSelectedValue] = React.useState< + RangeValue + >(mergedValue); + + // Operation ref + const operationRef: React.MutableRefObject = React.useRef< + ContextOperationRefProps + >(null); + // Open - const [mergedOpen, triggerOpenChange] = useMergedState({ + const [mergedOpen, triggerOpen] = useMergedState({ value: open, defaultValue: defaultOpen, defaultStateValue: false, - onChange: nextOpen => { - console.log('Open Changed!', nextOpen); + postState: postOpen => (disabled ? false : postOpen), + onChange: newOpen => { + if (onOpenChange) { + onOpenChange(newOpen); + } + + if (!newOpen && operationRef.current && operationRef.current.onClose) { + operationRef.current.onClose(); + } + }, + }); + + const startOpen = mergedOpen && activePickerIndex === 0; + const endOpen = mergedOpen && activePickerIndex === 1; + + // ============================ Trigger ============================ + const triggerChange = (newValue: RangeValue) => { + const values = reorderValues(newValue, generateConfig); + + setSelectedValue(values); + setInnerValue(values); + + const startValue = getIndexValue(values, 0); + const endValue = getIndexValue(values, 1); + + if ( + onChange && + (!isEqual(generateConfig, getIndexValue(mergedValue, 0), startValue) || + !isEqual(generateConfig, getIndexValue(mergedValue, 1), endValue)) + ) { + onChange(values, [ + startValue + ? generateConfig.locale.format( + locale.locale, + startValue, + formatList[0], + ) + : '', + endValue + ? generateConfig.locale.format(locale.locale, endValue, formatList[0]) + : '', + ]); + } + }; + + const forwardKeyDown = (e: React.KeyboardEvent) => { + if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { + // Let popup panel handle keyboard + return operationRef.current.onKeyDown(e); + } + return false; + }; + + const triggerClose = () => { + triggerOpen(false); + triggerChange(selectedValue); + }; + + // ============================= Text ============================== + const sharedTextHooksProps = { + formatList, + generateConfig, + locale, + }; + + const startValueTexts = useValueTexts( + getIndexValue(selectedValue, 0), + sharedTextHooksProps, + ); + + const endValueTexts = useValueTexts( + getIndexValue(selectedValue, 1), + sharedTextHooksProps, + ); + + const onTextChange = (newText: string, index: 0 | 1) => { + const inputDate = generateConfig.locale.parse( + locale.locale, + newText, + formatList, + ); + if (inputDate && (!disabledDate || !disabledDate(inputDate))) { + setSelectedValue(updateRangeValue(selectedValue, inputDate, index)); + } + }; + + const [startText, triggerStartTextChange] = useTextValueMapping({ + valueTexts: startValueTexts, + onTextChange: newText => onTextChange(newText, 0), + }); + + const [endText, triggerEndTextChange] = useTextValueMapping({ + valueTexts: endValueTexts, + onTextChange: newText => onTextChange(newText, 1), + }); + + // ============================= Input ============================= + const sharedInputHookProps = { + triggerOpen, + triggerClose, + forwardKeyDown, + onSubmit: () => { + triggerChange(selectedValue); + }, + onCancel: () => { + triggerChange(mergedValue); + setSelectedValue(mergedValue); + }, + onBlur, + }; + + const passOnFocus: React.FocusEventHandler = e => { + if (onFocus) { + onFocus(e); + } + }; + + const [ + startInputProps, + { focused: startFocused, typing: startTyping }, + ] = usePickerInput({ + ...sharedInputHookProps, + open: startOpen, + isClickOutside: (target: EventTarget | null) => + !!( + panelDivRef.current && + !panelDivRef.current.contains(target as Node) && + startInputDivRef.current && + !startInputDivRef.current.contains(target as Node) && + onOpenChange + ), + onFocus: e => { + setActivePickerIndex(0); + passOnFocus(e); }, }); - // ======================== Typing ======================== + const [ + endInputProps, + { focused: endFocused, typing: endTyping }, + ] = usePickerInput({ + ...sharedInputHookProps, + open: endOpen, + isClickOutside: (target: EventTarget | null) => + !!( + panelDivRef.current && + !panelDivRef.current.contains(target as Node) && + endInputDivRef.current && + !endInputDivRef.current.contains(target as Node) && + onOpenChange + ), + onFocus: e => { + setActivePickerIndex(1); + passOnFocus(e); + }, + }); + + // ============================= Sync ============================== + // Close should sync back with text value + React.useEffect(() => { + if (!mergedOpen) { + setSelectedValue(mergedValue); + } + }, [mergedOpen]); + + // Sync innerValue with control mode + React.useEffect(() => { + // Sync select value + setSelectedValue(mergedValue); + }, [mergedValue]); + + // ============================ Private ============================ + // TODO: pickerRef + + // ============================= Panel ============================= + const panelProps = { + ...(props as any), + className: undefined, + style: undefined, + }; - // ======================== Panels ======================== const panel = ( + {...panelProps} generateConfig={generateConfig} + className={classNames({ + [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, + })} + value={getIndexValue(selectedValue, activePickerIndex)} locale={locale} tabIndex={-1} onMouseDown={e => { e.preventDefault(); }} + onChange={date => { + setSelectedValue( + updateRangeValue(selectedValue, date, activePickerIndex), + ); + }} /> ); + let suffixNode: React.ReactNode; + if (suffixIcon) { + suffixNode = {suffixIcon}; + } + + let clearNode: React.ReactNode; + if (allowClear && mergedValue && !disabled) { + clearNode = ( + { + e.stopPropagation(); + triggerChange(null); + }} + className={`${prefixCls}-clear`} + > + {clearIcon || } + + ); + } + + const inputSharedProps = { + disabled, + size: getInputSize(picker, formatList[0]), + }; + return ( (props: RangePickerProps) { transitionName={transitionName} >
- +
+ +
{separator} - +
+ +
+ {suffixNode} + {clearNode}
); diff --git a/src/hooks/useMergeState.ts b/src/hooks/useMergeState.ts index 1f35f2e69..4b9c7b6fb 100644 --- a/src/hooks/useMergeState.ts +++ b/src/hooks/useMergeState.ts @@ -5,11 +5,13 @@ export default function useMergedState({ defaultValue, defaultStateValue, onChange, + postState, }: { value?: T; defaultValue?: T; defaultStateValue: T; onChange?: (value: T, prevValue: T) => void; + postState?: (value: T) => T; }): [T, (value: T) => void] { const [innerValue, setInnerValue] = React.useState(() => { if (value !== undefined) { @@ -21,12 +23,15 @@ export default function useMergedState({ return defaultStateValue; }); - const mergedValue = value !== undefined ? value : innerValue; + let mergedValue = value !== undefined ? value : innerValue; + if (postState) { + mergedValue = postState(mergedValue); + } function triggerChange(newValue: T) { setInnerValue(newValue); - if (onChange) { + if (mergedValue !== newValue && onChange) { onChange(newValue, mergedValue); } } diff --git a/src/hooks/useTextValueMapping.ts b/src/hooks/useTextValueMapping.ts index c8294287e..9dd4119cf 100644 --- a/src/hooks/useTextValueMapping.ts +++ b/src/hooks/useTextValueMapping.ts @@ -7,16 +7,14 @@ export default function useTextValueMapping({ /** Must useMemo, to assume that `valueTexts` only match on the first change */ valueTexts: string[]; onTextChange: (text: string) => void; -}): [string, (text: string) => void, () => void] { +}): [string, React.ChangeEventHandler] { const [text, setInnerText] = React.useState(''); - function setText(newText: string) { - setInnerText(newText); - onTextChange(newText); - } - - function resetText() { - setInnerText(valueTexts[0]); + function triggerTextChange({ + target: { value }, + }: React.ChangeEvent) { + setInnerText(value); + onTextChange(value); } React.useEffect(() => { @@ -25,5 +23,5 @@ export default function useTextValueMapping({ } }, [valueTexts.join('||')]); - return [text, setText, resetText]; + return [text, triggerTextChange]; } diff --git a/src/hooks/useValueTexts.ts b/src/hooks/useValueTexts.ts new file mode 100644 index 000000000..e16bd0b94 --- /dev/null +++ b/src/hooks/useValueTexts.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { GenerateConfig } from '../generate'; +import { Locale } from '../interface'; + +export default function useValueTexts( + value: DateType | null, + { + formatList, + generateConfig, + locale, + }: { + formatList: string[]; + generateConfig: GenerateConfig; + locale: Locale; + }, +) { + return React.useMemo(() => { + if (!value) { + return ['']; + } + return formatList.map(subFormat => + generateConfig.locale.format(locale.locale, value, subFormat), + ); + }, [value]); +} From 78c9bc2d950c6f7ed3a20794f5b7f017e3d1df27 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 15:07:21 +0800 Subject: [PATCH 04/25] adjust open logic --- examples/range.tsx | 8 ++++---- src/Picker.tsx | 15 ++++++++------- src/RangePicker.tsx | 21 +++++++++++++-------- src/hooks/usePickerInput.ts | 6 ++---- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/examples/range.tsx b/examples/range.tsx index 0c7d08c2f..ab0e8a549 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -49,7 +49,7 @@ export default () => {
-
+ {/*

Basic

{...sharedProps} @@ -65,13 +65,13 @@ export default () => { > Focus! -
+
*/}

Basic

{...sharedProps} locale={zhCN} picker="year" />
-
+ {/*

Allow Empty

{...sharedProps} @@ -106,7 +106,7 @@ export default () => { }} allowClear /> -
+
*/}
); diff --git a/src/Picker.tsx b/src/Picker.tsx index e08f50784..72d57d8ea 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -183,7 +183,7 @@ function InnerPicker(props: PickerProps) { >(null); // Open - const [mergedOpen, triggerOpen] = useMergedState({ + const [mergedOpen, triggerInnerOpen] = useMergedState({ value: open, defaultValue: defaultOpen, defaultStateValue: false, @@ -214,6 +214,13 @@ function InnerPicker(props: PickerProps) { } }; + const triggerOpen = (newOpen: boolean) => { + triggerInnerOpen(newOpen); + if (!newOpen) { + triggerChange(selectedValue); + } + }; + const forwardKeyDown = (e: React.KeyboardEvent) => { if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { // Let popup panel handle keyboard @@ -222,11 +229,6 @@ function InnerPicker(props: PickerProps) { return false; }; - const triggerClose = () => { - triggerOpen(false); - triggerChange(selectedValue); - }; - // ============================= Text ============================== const valueTexts = useValueTexts(selectedValue, { formatList, @@ -252,7 +254,6 @@ function InnerPicker(props: PickerProps) { const [inputProps, { focused, typing }] = usePickerInput({ open: mergedOpen, triggerOpen, - triggerClose, forwardKeyDown, isClickOutside: target => !!( diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 521033010..2e57c7b39 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -212,7 +212,7 @@ function RangePicker(props: RangePickerProps) { >(null); // Open - const [mergedOpen, triggerOpen] = useMergedState({ + const [mergedOpen, triggerInnerOpen] = useMergedState({ value: open, defaultValue: defaultOpen, defaultStateValue: false, @@ -261,6 +261,16 @@ function RangePicker(props: RangePickerProps) { } }; + const triggerOpen = (newOpen: boolean, index: 0 | 1) => { + if (newOpen) { + setActivePickerIndex(index); + triggerInnerOpen(newOpen); + } else if (activePickerIndex === index) { + triggerInnerOpen(newOpen); + triggerChange(selectedValue); + } + }; + const forwardKeyDown = (e: React.KeyboardEvent) => { if (mergedOpen && operationRef.current && operationRef.current.onKeyDown) { // Let popup panel handle keyboard @@ -269,11 +279,6 @@ function RangePicker(props: RangePickerProps) { return false; }; - const triggerClose = () => { - triggerOpen(false); - triggerChange(selectedValue); - }; - // ============================= Text ============================== const sharedTextHooksProps = { formatList, @@ -314,8 +319,6 @@ function RangePicker(props: RangePickerProps) { // ============================= Input ============================= const sharedInputHookProps = { - triggerOpen, - triggerClose, forwardKeyDown, onSubmit: () => { triggerChange(selectedValue); @@ -351,6 +354,7 @@ function RangePicker(props: RangePickerProps) { setActivePickerIndex(0); passOnFocus(e); }, + triggerOpen: newOpen => triggerOpen(newOpen, 0), }); const [ @@ -371,6 +375,7 @@ function RangePicker(props: RangePickerProps) { setActivePickerIndex(1); passOnFocus(e); }, + triggerOpen: newOpen => triggerOpen(newOpen, 1), }); // ============================= Sync ============================== diff --git a/src/hooks/usePickerInput.ts b/src/hooks/usePickerInput.ts index b2f13e877..cbcebbf63 100644 --- a/src/hooks/usePickerInput.ts +++ b/src/hooks/usePickerInput.ts @@ -6,7 +6,6 @@ export default function usePickerInput({ open, isClickOutside, triggerOpen, - triggerClose, forwardKeyDown, onSubmit, onCancel, @@ -16,7 +15,6 @@ export default function usePickerInput({ open: boolean; isClickOutside: (clickElement: EventTarget | null) => boolean; triggerOpen: (open: boolean) => void; - triggerClose: () => void; forwardKeyDown: (e: React.KeyboardEvent) => boolean; onSubmit: () => void; onCancel: () => void; @@ -97,7 +95,7 @@ export default function usePickerInput({ return; } - triggerClose(); + triggerOpen(false); setFocused(false); if (onBlur) { @@ -111,7 +109,7 @@ export default function usePickerInput({ addGlobalMouseDownEvent(({ target }: MouseEvent) => { if (open && isClickOutside(target)) { preventBlurRef.current = true; - triggerClose(); + triggerOpen(false); // Always set back in case `onBlur` prevented by user window.setTimeout(() => { From 7b187b8b926a5ccaf3b85c6626c032e3800acf45 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 15:24:09 +0800 Subject: [PATCH 05/25] clean up open --- examples/range.tsx | 4 +- src/Picker.tsx | 10 --- src/RangePicker.tsx | 153 ++++++++++++++++++++++++++++++-------------- 3 files changed, 108 insertions(+), 59 deletions(-) diff --git a/examples/range.tsx b/examples/range.tsx index ab0e8a549..71fdbba34 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -49,7 +49,7 @@ export default () => {
- {/*
+

Basic

{...sharedProps} @@ -65,7 +65,7 @@ export default () => { > Focus! -
*/} +

Basic

{...sharedProps} locale={zhCN} picker="year" /> diff --git a/src/Picker.tsx b/src/Picker.tsx index 72d57d8ea..3a4f3bb47 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -33,7 +33,6 @@ import useValueTexts from './hooks/useValueTexts'; export interface PickerRefConfig { focus: () => void; blur: () => void; - open: () => void; } export interface PickerSharedProps extends React.AriaAttributes { @@ -301,9 +300,6 @@ function InnerPicker(props: PickerProps) { inputRef.current.blur(); } }, - open: () => { - triggerOpen(true); - }, }; } @@ -421,12 +417,6 @@ class Picker extends React.Component> { } }; - open = () => { - if (this.pickerRef.current) { - this.pickerRef.current.open(); - } - }; - render() { return ( diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 2e57c7b39..4a721c85d 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -1,7 +1,12 @@ import * as React from 'react'; import classNames from 'classnames'; import { DisabledTimes, PanelMode, PickerMode } from './interface'; -import { PickerBaseProps, PickerDateProps, PickerTimeProps } from './Picker'; +import { + PickerBaseProps, + PickerDateProps, + PickerTimeProps, + PickerRefConfig, +} from './Picker'; import { SharedTimeProps } from './panels/TimePanel'; import useMergedState from './hooks/useMergeState'; import PickerTrigger from './PickerTrigger'; @@ -9,7 +14,7 @@ import PickerPanel from './PickerPanel'; import usePickerInput from './hooks/usePickerInput'; import getDataOrAriaProps, { toArray } from './utils/miscUtil'; import { getDefaultFormat, getInputSize } from './utils/uiUtil'; -import { ContextOperationRefProps } from './PanelContext'; +import PanelContext, { ContextOperationRefProps } from './PanelContext'; import { isEqual } from './utils/dateUtil'; import useValueTexts from './hooks/useValueTexts'; import useTextValueMapping from './hooks/useTextValueMapping'; @@ -142,7 +147,7 @@ interface MergedRangePickerProps picker?: PickerMode; } -function RangePicker(props: RangePickerProps) { +function InnerRangePicker(props: RangePickerProps) { const { prefixCls = 'rc-picker', style, @@ -170,6 +175,7 @@ function RangePicker(props: RangePickerProps) { allowClear, suffixIcon, clearIcon, + pickerRef, inputReadOnly, onChange, onOpenChange, @@ -246,6 +252,7 @@ function RangePicker(props: RangePickerProps) { (!isEqual(generateConfig, getIndexValue(mergedValue, 0), startValue) || !isEqual(generateConfig, getIndexValue(mergedValue, 1), endValue)) ) { + console.warn('trigger change!!!!'); onChange(values, [ startValue ? generateConfig.locale.format( @@ -393,7 +400,23 @@ function RangePicker(props: RangePickerProps) { }, [mergedValue]); // ============================ Private ============================ - // TODO: pickerRef + if (pickerRef) { + pickerRef.current = { + focus: () => { + if (startInputRef.current) { + startInputRef.current.focus(); + } + }, + blur: () => { + if (startInputRef.current) { + startInputRef.current.blur(); + } + if (endInputRef.current) { + endInputRef.current.blur(); + } + }, + }; + } // ============================= Panel ============================= const panelProps = { @@ -449,53 +472,89 @@ function RangePicker(props: RangePickerProps) { }; return ( - -
-
- +
+
+ +
+ {separator} +
+ +
+ {suffixNode} + {clearNode}
- {separator} -
- -
- {suffixNode} - {clearNode} -
- + + ); } +// Wrap with class component to enable pass generic with instance method +class RangePicker extends React.Component< + RangePickerProps +> { + pickerRef = React.createRef(); + + focus = () => { + if (this.pickerRef.current) { + this.pickerRef.current.focus(); + } + }; + + blur = () => { + if (this.pickerRef.current) { + this.pickerRef.current.blur(); + } + }; + + render() { + return ( + + {...this.props} + pickerRef={this.pickerRef as React.MutableRefObject} + /> + ); + } +} + export default RangePicker; From f90a2f92f6a7a4438b0415d88af898fa960d7688 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 15:35:16 +0800 Subject: [PATCH 06/25] Fix sync text logic --- src/Picker.tsx | 14 +++++++--- src/RangePicker.tsx | 48 ++++++++++++++++++++++++-------- src/hooks/usePickerInput.ts | 2 -- src/hooks/useTextValueMapping.ts | 10 +++++-- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/Picker.tsx b/src/Picker.tsx index 3a4f3bb47..b530deac1 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -213,9 +213,12 @@ function InnerPicker(props: PickerProps) { } }; - const triggerOpen = (newOpen: boolean) => { + const triggerOpen = ( + newOpen: boolean, + preventChangeEvent: boolean = false, + ) => { triggerInnerOpen(newOpen); - if (!newOpen) { + if (!newOpen && !preventChangeEvent) { triggerChange(selectedValue); } }; @@ -235,7 +238,7 @@ function InnerPicker(props: PickerProps) { locale, }); - const [text, triggerTextChange] = useTextValueMapping({ + const [text, triggerTextChange, resetText] = useTextValueMapping({ valueTexts, onTextChange: newText => { const inputDate = generateConfig.locale.parse( @@ -264,10 +267,13 @@ function InnerPicker(props: PickerProps) { ), onSubmit: () => { triggerChange(selectedValue); + triggerOpen(false, true); + resetText(); }, onCancel: () => { - triggerChange(mergedValue); + triggerOpen(false, true); setSelectedValue(mergedValue); + resetText(); }, onFocus, onBlur, diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 4a721c85d..cb5cc4c6d 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -252,7 +252,6 @@ function InnerRangePicker(props: RangePickerProps) { (!isEqual(generateConfig, getIndexValue(mergedValue, 0), startValue) || !isEqual(generateConfig, getIndexValue(mergedValue, 1), endValue)) ) { - console.warn('trigger change!!!!'); onChange(values, [ startValue ? generateConfig.locale.format( @@ -268,13 +267,19 @@ function InnerRangePicker(props: RangePickerProps) { } }; - const triggerOpen = (newOpen: boolean, index: 0 | 1) => { + const triggerOpen = ( + newOpen: boolean, + index: 0 | 1, + preventChangeEvent: boolean = false, + ) => { if (newOpen) { setActivePickerIndex(index); triggerInnerOpen(newOpen); } else if (activePickerIndex === index) { triggerInnerOpen(newOpen); - triggerChange(selectedValue); + if (!preventChangeEvent) { + triggerChange(selectedValue); + } } }; @@ -314,12 +319,18 @@ function InnerRangePicker(props: RangePickerProps) { } }; - const [startText, triggerStartTextChange] = useTextValueMapping({ + const [ + startText, + triggerStartTextChange, + resetStartText, + ] = useTextValueMapping({ valueTexts: startValueTexts, onTextChange: newText => onTextChange(newText, 0), }); - const [endText, triggerEndTextChange] = useTextValueMapping({ + const [endText, triggerEndTextChange, resetEndText] = useTextValueMapping< + DateType + >({ valueTexts: endValueTexts, onTextChange: newText => onTextChange(newText, 1), }); @@ -327,13 +338,6 @@ function InnerRangePicker(props: RangePickerProps) { // ============================= Input ============================= const sharedInputHookProps = { forwardKeyDown, - onSubmit: () => { - triggerChange(selectedValue); - }, - onCancel: () => { - triggerChange(mergedValue); - setSelectedValue(mergedValue); - }, onBlur, }; @@ -362,6 +366,16 @@ function InnerRangePicker(props: RangePickerProps) { passOnFocus(e); }, triggerOpen: newOpen => triggerOpen(newOpen, 0), + onSubmit: () => { + triggerChange(selectedValue); + triggerOpen(false, 0, true); + resetStartText(); + }, + onCancel: () => { + triggerOpen(false, 0, true); + setSelectedValue(mergedValue); + resetStartText(); + }, }); const [ @@ -383,6 +397,16 @@ function InnerRangePicker(props: RangePickerProps) { passOnFocus(e); }, triggerOpen: newOpen => triggerOpen(newOpen, 1), + onSubmit: () => { + triggerChange(selectedValue); + triggerOpen(false, 1, true); + resetEndText(); + }, + onCancel: () => { + triggerOpen(false, 1, true); + setSelectedValue(mergedValue); + resetEndText(); + }, }); // ============================= Sync ============================== diff --git a/src/hooks/usePickerInput.ts b/src/hooks/usePickerInput.ts index cbcebbf63..48a47b68c 100644 --- a/src/hooks/usePickerInput.ts +++ b/src/hooks/usePickerInput.ts @@ -45,7 +45,6 @@ export default function usePickerInput({ triggerOpen(true); } else { onSubmit(); - triggerOpen(false); setTyping(true); } return; @@ -65,7 +64,6 @@ export default function usePickerInput({ } case KeyCode.ESC: { - triggerOpen(false); setTyping(true); onCancel(); return; diff --git a/src/hooks/useTextValueMapping.ts b/src/hooks/useTextValueMapping.ts index 9dd4119cf..bb6f35c44 100644 --- a/src/hooks/useTextValueMapping.ts +++ b/src/hooks/useTextValueMapping.ts @@ -7,7 +7,7 @@ export default function useTextValueMapping({ /** Must useMemo, to assume that `valueTexts` only match on the first change */ valueTexts: string[]; onTextChange: (text: string) => void; -}): [string, React.ChangeEventHandler] { +}): [string, React.ChangeEventHandler, () => void] { const [text, setInnerText] = React.useState(''); function triggerTextChange({ @@ -17,11 +17,15 @@ export default function useTextValueMapping({ onTextChange(value); } + function resetText() { + setInnerText(valueTexts[0]); + } + React.useEffect(() => { if (valueTexts.every(valText => valText !== text)) { - setInnerText(valueTexts[0]); + resetText(); } }, [valueTexts.join('||')]); - return [text, triggerTextChange]; + return [text, triggerTextChange, resetText]; } From d6b3e151e0e1d71ac43a18e4ab6303c5d5189f34 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 15:52:31 +0800 Subject: [PATCH 07/25] viewDate always be a value --- assets/index.less | 1 - examples/range.tsx | 4 +-- src/PickerPanel.tsx | 10 +++++-- src/RangePicker.tsx | 56 ++++++++++++++++++++++---------------- src/hooks/useMergeState.ts | 4 +-- 5 files changed, 44 insertions(+), 31 deletions(-) diff --git a/assets/index.less b/assets/index.less index 726862f77..33988198c 100644 --- a/assets/index.less +++ b/assets/index.less @@ -245,7 +245,6 @@ // ======================= Dropdown ======================= &-dropdown { position: absolute; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); &-hidden { display: none; diff --git a/examples/range.tsx b/examples/range.tsx index 71fdbba34..0c7d08c2f 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -71,7 +71,7 @@ export default () => { {...sharedProps} locale={zhCN} picker="year" />
- {/*
+

Allow Empty

{...sharedProps} @@ -106,7 +106,7 @@ export default () => { }} allowClear /> -
*/} +
); diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index 38319dce6..0700878b9 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -26,6 +26,7 @@ import { DateRender } from './panels/DatePanel/DateBody'; import { PickerModeMap } from './utils/uiUtil'; import { MonthCellRender } from './panels/MonthPanel/MonthBody'; import RangeContext, { FooterSelection } from './RangeContext'; +import useMergedState from './hooks/useMergeState'; export interface PickerPanelSharedProps { prefixCls?: string; @@ -158,9 +159,12 @@ function PickerPanel(props: PickerPanelProps) { const mergedValue = value !== undefined ? value : innerValue; // View date control - const [viewDate, setViewDate] = React.useState( - () => defaultPickerValue || mergedValue || generateConfig.getNow(), - ); + const [viewDate, setViewDate] = useMergedState({ + value: mergedValue, + defaultValue: defaultPickerValue, + defaultStateValue: null, + postState: date => date || generateConfig.getNow(), + }); // Panel control const getInternalNextMode = (nextMode: PanelMode): PanelMode => { diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index cb5cc4c6d..0c3fc27b8 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -183,6 +183,10 @@ function InnerRangePicker(props: RangePickerProps) { onBlur, } = props as MergedRangePickerProps; + const containerRef = React.useRef(null); + const panelDivRef = React.useRef(null); + const startInputDivRef = React.useRef(null); + const endInputDivRef = React.useRef(null); const startInputRef = React.useRef(null); const endInputRef = React.useRef(null); @@ -194,11 +198,6 @@ function InnerRangePicker(props: RangePickerProps) { // Active picker const [activePickerIndex, setActivePickerIndex] = React.useState<0 | 1>(0); - // Panel ref - const panelDivRef = React.useRef(null); - const startInputDivRef = React.useRef(null); - const endInputDivRef = React.useRef(null); - // Real value const [mergedValue, setInnerValue] = useMergedState({ value, @@ -237,6 +236,14 @@ function InnerRangePicker(props: RangePickerProps) { const startOpen = mergedOpen && activePickerIndex === 0; const endOpen = mergedOpen && activePickerIndex === 1; + // Popup min width + const [popupMinWidth, setPopupMinWidth] = React.useState(0); + React.useEffect(() => { + if (!mergedOpen && containerRef.current) { + setPopupMinWidth(containerRef.current.offsetWidth); + } + }, [mergedOpen]); + // ============================ Trigger ============================ const triggerChange = (newValue: RangeValue) => { const values = reorderValues(newValue, generateConfig); @@ -450,24 +457,26 @@ function InnerRangePicker(props: RangePickerProps) { }; const panel = ( - - {...panelProps} - generateConfig={generateConfig} - className={classNames({ - [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, - })} - value={getIndexValue(selectedValue, activePickerIndex)} - locale={locale} - tabIndex={-1} - onMouseDown={e => { - e.preventDefault(); - }} - onChange={date => { - setSelectedValue( - updateRangeValue(selectedValue, date, activePickerIndex), - ); - }} - /> +
+ + {...panelProps} + generateConfig={generateConfig} + className={classNames({ + [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, + })} + value={getIndexValue(selectedValue, activePickerIndex)} + locale={locale} + tabIndex={-1} + onMouseDown={e => { + e.preventDefault(); + }} + onChange={date => { + setSelectedValue( + updateRangeValue(selectedValue, date, activePickerIndex), + ); + }} + /> +
); let suffixNode: React.ReactNode; @@ -514,6 +523,7 @@ function InnerRangePicker(props: RangePickerProps) { transitionName={transitionName} >
({ +export default function useMergedState({ value, defaultValue, defaultStateValue, @@ -12,7 +12,7 @@ export default function useMergedState({ defaultStateValue: T; onChange?: (value: T, prevValue: T) => void; postState?: (value: T) => T; -}): [T, (value: T) => void] { +}): [R, (value: T) => void] { const [innerValue, setInnerValue] = React.useState(() => { if (value !== undefined) { return value; From 57806a67edb5b75ac2e0a3a635bab2c86772368e Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 16:17:38 +0800 Subject: [PATCH 08/25] fix viewDate logic --- assets/index.less | 13 +++++++++++++ examples/common.less | 7 +++++++ examples/range.tsx | 11 +++++++---- src/PickerPanel.tsx | 3 +-- src/RangePicker.tsx | 21 +++++++++++++++++++++ 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 examples/common.less diff --git a/assets/index.less b/assets/index.less index 33988198c..47baf6559 100644 --- a/assets/index.less +++ b/assets/index.less @@ -245,10 +245,23 @@ // ======================= Dropdown ======================= &-dropdown { position: absolute; + box-shadow: 0 0 1px red; &-hidden { display: none; } + + // Panel + .@{prefix-cls}-range-arrow { + position: absolute; + width: 10px; + height: 10px; + background: blue; + } + + .@{prefix-cls}-panel { + margin: 10px 0; + } } // ======================================================== diff --git a/examples/common.less b/examples/common.less new file mode 100644 index 000000000..c84387c88 --- /dev/null +++ b/examples/common.less @@ -0,0 +1,7 @@ +h1, +h2, +h3, +h4 { + margin: 0; + line-height: 200%; +} diff --git a/examples/range.tsx b/examples/range.tsx index 0c7d08c2f..74eca929b 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -4,6 +4,7 @@ import RangePicker from '../src/RangePicker'; import momentGenerateConfig from '../src/generate/moment'; import zhCN from '../src/locale/zh_CN'; import '../assets/index.less'; +import './common.less'; const defaultStartValue = moment('2019-09-03 05:02:03'); const defaultEndValue = moment('2019-11-28 01:02:03'); @@ -43,10 +44,10 @@ export default () => { return (
-

+

Value:{' '} {value ? `${formatDate(value[0])} ~ ${formatDate(value[1])}` : 'null'} -

+
@@ -56,6 +57,8 @@ export default () => { locale={zhCN} allowClear ref={rangePickerRef} + open + // style={{ width: 500 }} />
-
+ {/*

Basic

{...sharedProps} locale={zhCN} picker="year" />
@@ -106,7 +109,7 @@ export default () => { }} allowClear /> -
+
*/}
); diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index 0700878b9..e4db5bd86 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -160,8 +160,7 @@ function PickerPanel(props: PickerPanelProps) { // View date control const [viewDate, setViewDate] = useMergedState({ - value: mergedValue, - defaultValue: defaultPickerValue, + defaultValue: defaultPickerValue || mergedValue, defaultStateValue: null, postState: date => date || generateConfig.getNow(), }); diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 0c3fc27b8..1f11d604c 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -458,6 +458,27 @@ function InnerRangePicker(props: RangePickerProps) { const panel = (
+
+ + + {...panelProps} + generateConfig={generateConfig} + className={classNames({ + [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, + })} + value={getIndexValue(selectedValue, activePickerIndex)} + locale={locale} + tabIndex={-1} + onMouseDown={e => { + e.preventDefault(); + }} + onChange={date => { + setSelectedValue( + updateRangeValue(selectedValue, date, activePickerIndex), + ); + }} + /> + {...panelProps} generateConfig={generateConfig} From c0eb74fcd10af1abd418a45e5439615bcfe0a268 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 20:28:28 +0800 Subject: [PATCH 09/25] Double panel in range picker --- assets/index.less | 38 ++++++++-- examples/range.tsx | 29 +++++++- src/PanelContext.tsx | 2 + src/Picker.tsx | 16 +++-- src/PickerPanel.tsx | 25 ++++++- src/RangeContext.tsx | 1 + src/RangePicker.tsx | 160 ++++++++++++++++++++++++++++-------------- src/panels/Header.tsx | 11 +++ src/utils/dateUtil.ts | 18 ++++- src/utils/uiUtil.ts | 2 +- 10 files changed, 236 insertions(+), 66 deletions(-) diff --git a/assets/index.less b/assets/index.less index 47baf6559..1a5a94c80 100644 --- a/assets/index.less +++ b/assets/index.less @@ -1,11 +1,13 @@ @prefix-cls: rc-picker; +@background-color: rgb(255, 240, 255); + .@{prefix-cls} { display: inline-flex; &-panel { border: 1px solid #666; - background: rgb(255, 240, 255); + background: @background-color; display: inline-block; vertical-align: top; @@ -252,11 +254,39 @@ } // Panel + @arrow-size: 10px; .@{prefix-cls}-range-arrow { position: absolute; - width: 10px; - height: 10px; - background: blue; + width: @arrow-size; + height: @arrow-size; + z-index: 1; + transform: rotate(-45deg); + top: @arrow-size / 2 + 1px; + left: @arrow-size; + + &::before, + &::after { + content: ''; + position: absolute; + box-sizing: border-box; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &::before { + width: @arrow-size; + height: @arrow-size; + border: @arrow-size / 2 solid blue; + border-color: blue blue transparent transparent; + } + &::after { + width: @arrow-size - 2px; + height: @arrow-size - 2px; + border: (@arrow-size - 2px) / 2 solid blue; + border-color: @background-color @background-color transparent + transparent; + } } .@{prefix-cls}-panel { diff --git a/examples/range.tsx b/examples/range.tsx index 74eca929b..6e6bee686 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -51,6 +51,33 @@ export default () => {
+

Basic

+ + {...sharedProps} + locale={zhCN} + allowClear + ref={rangePickerRef} + open + /> + {/* + {...sharedProps} + locale={zhCN} + allowClear + ref={rangePickerRef} + picker="month" + open + /> + + {...sharedProps} + locale={zhCN} + allowClear + ref={rangePickerRef} + picker="year" + open + /> */} +
+ + {/*

Basic

{...sharedProps} @@ -68,7 +95,7 @@ export default () => { > Focus! -
+
*/} {/*

Basic

{...sharedProps} locale={zhCN} picker="year" /> diff --git a/src/PanelContext.tsx b/src/PanelContext.tsx index 1c8e67548..b186ef244 100644 --- a/src/PanelContext.tsx +++ b/src/PanelContext.tsx @@ -10,6 +10,8 @@ export interface PanelContextProps { /** Only work with time panel */ hideHeader?: boolean; panelRef?: React.Ref; + hidePrevBtn?: boolean; + hideNextBtn?: boolean; } const PanelContext = React.createContext({}); diff --git a/src/Picker.tsx b/src/Picker.tsx index b530deac1..6ec53d179 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -82,20 +82,22 @@ export interface PickerSharedProps extends React.AriaAttributes { name?: string; } +type OmitPanelProps = Omit< + Props, + 'onChange' | 'hideHeader' | 'pickerValue' | 'onPickerValueChange' +>; + export interface PickerBaseProps extends PickerSharedProps, - Omit, 'onChange' | 'hideHeader'> {} + OmitPanelProps> {} export interface PickerDateProps extends PickerSharedProps, - Omit, 'onChange' | 'hideHeader'> {} + OmitPanelProps> {} export interface PickerTimeProps extends PickerSharedProps, - Omit< - PickerPanelTimeProps, - 'onChange' | 'format' | 'hideHeader' - > {} + Omit>, 'format'> {} export type PickerProps = | PickerBaseProps @@ -315,6 +317,8 @@ function InnerPicker(props: PickerProps) { ...(props as Omit, 'picker' | 'format'>), className: undefined, style: undefined, + pickerValue: undefined, + onPickerValueChange: undefined, }; const panel = ( diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index e4db5bd86..d2146b3c5 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -44,6 +44,8 @@ export interface PickerPanelSharedProps { value?: DateType | null; defaultValue?: DateType; /** [Legacy] Set default display picker view date */ + pickerValue?: DateType; + /** [Legacy] Set default display picker view date */ defaultPickerValue?: DateType; // Date @@ -62,6 +64,8 @@ export interface PickerPanelSharedProps { /** @private This is internal usage. Do not use in your production env */ hideHeader?: boolean; + /** @private This is internal usage. Do not use in your production env */ + onPickerValueChange?: (date: DateType) => void; } export interface PickerPanelBaseProps @@ -109,6 +113,7 @@ function PickerPanel(props: PickerPanelProps) { generateConfig, value, defaultValue, + pickerValue, defaultPickerValue, mode, picker = 'date', @@ -121,6 +126,7 @@ function PickerPanel(props: PickerPanelProps) { onChange, onPanelChange, onMouseDown, + onPickerValueChange, } = props as MergedPickerPanelProps; if (process.env.NODE_ENV !== 'production') { @@ -139,7 +145,9 @@ function PickerPanel(props: PickerPanelProps) { const panelContext = React.useContext(PanelContext); const { operationRef, panelRef: panelDivRef } = panelContext; - const { extraFooterSelections, inRange } = React.useContext(RangeContext); + const { extraFooterSelections, inRange, startPanel } = React.useContext( + RangeContext, + ); const panelRef = React.useRef({}); // Handle init logic @@ -159,12 +167,23 @@ function PickerPanel(props: PickerPanelProps) { const mergedValue = value !== undefined ? value : innerValue; // View date control - const [viewDate, setViewDate] = useMergedState({ + const [viewDate, setInnerViewDate] = useMergedState< + DateType | null, + DateType + >({ + value: pickerValue, defaultValue: defaultPickerValue || mergedValue, defaultStateValue: null, postState: date => date || generateConfig.getNow(), }); + const setViewDate = (date: DateType) => { + setInnerViewDate(date); + if (onPickerValueChange) { + onPickerValueChange(date); + } + }; + // Panel control const getInternalNextMode = (nextMode: PanelMode): PanelMode => { const getNextMode = PickerModeMap[picker!]; @@ -424,6 +443,8 @@ function PickerPanel(props: PickerPanelProps) { ...panelContext, hideHeader: 'hideHeader' in props ? hideHeader : panelContext.hideHeader, + hidePrevBtn: inRange && !startPanel, + hideNextBtn: inRange && startPanel, }} >
, NullableDateType]; inRange?: boolean; + startPanel?: boolean; } const RangeContext = React.createContext({}); diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 1f11d604c..60cf3ae03 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -15,10 +15,12 @@ import usePickerInput from './hooks/usePickerInput'; import getDataOrAriaProps, { toArray } from './utils/miscUtil'; import { getDefaultFormat, getInputSize } from './utils/uiUtil'; import PanelContext, { ContextOperationRefProps } from './PanelContext'; -import { isEqual } from './utils/dateUtil'; +import { isEqual, getClosingViewDate } from './utils/dateUtil'; import useValueTexts from './hooks/useValueTexts'; import useTextValueMapping from './hooks/useTextValueMapping'; import { GenerateConfig } from './generate'; +import { PickerPanelProps } from '.'; +import RangeContext from './RangeContext'; type EventValue = DateType | null; type RangeValue = [EventValue, EventValue] | null; @@ -30,12 +32,12 @@ function getIndexValue( return values ? values[index] : null; } -function updateRangeValue( - values: RangeValue, - value: DateType, +function updateRangeValue( + values: [T | null, T | null] | null, + value: T, index: number, -) { - const newValues: RangeValue = [ +): [T | null, T | null] | null { + const newValues: [T | null, T | null] = [ getIndexValue(values, 0), getIndexValue(values, 1), ]; @@ -112,6 +114,8 @@ type OmitPickerProps = Omit< | 'onChange' | 'onSelect' | 'onPanelChange' + | 'pickerValue' + | 'onPickerValueChange' >; export interface RangePickerBaseProps @@ -163,12 +167,13 @@ function InnerRangePicker(props: RangePickerProps) { autoFocus, disabled, format, - picker, + picker = 'date', showTime, use12Hours, separator = '~', value, defaultValue, + defaultPickerValue, open, defaultOpen, disabledDate, @@ -177,8 +182,10 @@ function InnerRangePicker(props: RangePickerProps) { clearIcon, pickerRef, inputReadOnly, + mode, onChange, onOpenChange, + onPanelChange, onFocus, onBlur, } = props as MergedRangePickerProps; @@ -449,54 +456,105 @@ function InnerRangePicker(props: RangePickerProps) { }; } + // =========================== View Date =========================== + const [viewDates, setViewDates] = useMergedState< + RangeValue, + [DateType, DateType] + >({ + defaultValue: defaultPickerValue || mergedValue, + defaultStateValue: null, + postState: postViewDates => + postViewDates || [generateConfig.getNow(), generateConfig.getNow()], + }); + // ============================= Panel ============================= - const panelProps = { - ...(props as any), - className: undefined, - style: undefined, - }; + // const [mergedMode, setMode] = useMergedState<[PanelMode, PanelMode]>({ + // value: mode, + // defaultStateValue: [picker, picker], + // }); + + // const triggerPanelChange = (date: DateType, newMode: PanelMode) => { + // setMode( + // updateRangeValue(mergedMode, newMode, 0) as [ + // PanelMode, + // PanelMode, + // ], + // ); + // }; + + function renderPanel( + startPanel?: boolean, + panelProps: Partial> = {}, + ) { + return ( + + + {...(props as any)} + {...panelProps} + generateConfig={generateConfig} + style={undefined} + className={classNames({ + [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, + })} + value={getIndexValue(selectedValue, activePickerIndex)} + locale={locale} + tabIndex={-1} + onMouseDown={e => { + e.preventDefault(); + }} + onChange={date => { + setSelectedValue( + updateRangeValue(selectedValue, date, activePickerIndex), + ); + }} + /> + + ); + } - const panel = ( + function renderPanels() { + if (picker !== 'time' && !showTime) { + const viewDate = viewDates[activePickerIndex]; + const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); + + return ( + <> + {renderPanel(true, { + pickerValue: viewDate, + onPickerValueChange: newViewDate => { + setViewDates( + updateRangeValue(viewDates, newViewDate, activePickerIndex), + ); + }, + })} + {renderPanel(false, { + pickerValue: nextViewDate, + onPickerValueChange: newViewDate => { + setViewDates( + updateRangeValue( + viewDates, + getClosingViewDate(newViewDate, picker, generateConfig, -1), + activePickerIndex, + ), + ); + }, + })} + + ); + } + return renderPanel(); + } + + const rangePanel = (
- - {...panelProps} - generateConfig={generateConfig} - className={classNames({ - [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, - })} - value={getIndexValue(selectedValue, activePickerIndex)} - locale={locale} - tabIndex={-1} - onMouseDown={e => { - e.preventDefault(); - }} - onChange={date => { - setSelectedValue( - updateRangeValue(selectedValue, date, activePickerIndex), - ); - }} - /> - - - {...panelProps} - generateConfig={generateConfig} - className={classNames({ - [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, - })} - value={getIndexValue(selectedValue, activePickerIndex)} - locale={locale} - tabIndex={-1} - onMouseDown={e => { - e.preventDefault(); - }} - onChange={date => { - setSelectedValue( - updateRangeValue(selectedValue, date, activePickerIndex), - ); - }} - /> + {renderPanels()}
); @@ -535,7 +593,7 @@ function InnerRangePicker(props: RangePickerProps) { > {onSuperPrev && ( @@ -41,6 +48,7 @@ function Header({ onClick={onSuperPrev} tabIndex={-1} className={`${prefixCls}-super-prev-btn`} + style={hidePrevBtn ? HIDDEN_STYLE : {}} > {superPrevIcon} @@ -51,6 +59,7 @@ function Header({ onClick={onPrev} tabIndex={-1} className={`${prefixCls}-prev-btn`} + style={hidePrevBtn ? HIDDEN_STYLE : {}} > {prevIcon} @@ -62,6 +71,7 @@ function Header({ onClick={onNext} tabIndex={-1} className={`${prefixCls}-next-btn`} + style={hideNextBtn ? HIDDEN_STYLE : {}} > {nextIcon} @@ -72,6 +82,7 @@ function Header({ onClick={onSuperNext} tabIndex={-1} className={`${prefixCls}-super-next-btn`} + style={hideNextBtn ? HIDDEN_STYLE : {}} > {superNextIcon} diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts index 6f5ba91c0..66e48036a 100644 --- a/src/utils/dateUtil.ts +++ b/src/utils/dateUtil.ts @@ -1,6 +1,6 @@ import { noteOnce } from 'rc-util/lib/warning'; import { GenerateConfig } from '../generate'; -import { NullableDateType } from '../interface'; +import { NullableDateType, PickerMode } from '../interface'; export const WEEK_DAY_COUNT = 7; @@ -136,3 +136,19 @@ export function getWeekStartDate( return value; } } + +export function getClosingViewDate( + viewDate: DateType, + picker: PickerMode, + generateConfig: GenerateConfig, + offset: number = 1, +): DateType { + switch (picker) { + case 'year': + return generateConfig.addYear(viewDate, offset * 10); + case 'month': + return generateConfig.addYear(viewDate, offset); + default: + return generateConfig.addMonth(viewDate, offset); + } +} diff --git a/src/utils/uiUtil.ts b/src/utils/uiUtil.ts index 2d1aa2b78..5030b5ff0 100644 --- a/src/utils/uiUtil.ts +++ b/src/utils/uiUtil.ts @@ -214,4 +214,4 @@ export const PickerModeMap: Record< week: getWeekNextMode, time: null, date: null, -}; +}; \ No newline at end of file From 6bd2fcdeeb457b42af4e7736d3a1780db5b68672 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 2 Dec 2019 21:01:52 +0800 Subject: [PATCH 10/25] Auto detect next viewDate --- src/PickerPanel.tsx | 2 +- src/RangePicker.tsx | 73 ++++++++++++++++++++++++++++++++++--------- src/utils/dateUtil.ts | 15 ++++++++- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index d2146b3c5..3245c24cd 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -277,7 +277,7 @@ function PickerPanel(props: PickerPanelProps) { // ============================ Effect ============================ React.useEffect(() => { if (value && !initRef.current) { - setViewDate(value); + setInnerViewDate(value); } }, [value]); diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 60cf3ae03..83cc0fe6c 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -15,7 +15,13 @@ import usePickerInput from './hooks/usePickerInput'; import getDataOrAriaProps, { toArray } from './utils/miscUtil'; import { getDefaultFormat, getInputSize } from './utils/uiUtil'; import PanelContext, { ContextOperationRefProps } from './PanelContext'; -import { isEqual, getClosingViewDate } from './utils/dateUtil'; +import { + isEqual, + getClosingViewDate, + isSameDate, + isSameMonth, + isSameYear, +} from './utils/dateUtil'; import useValueTexts from './hooks/useValueTexts'; import useTextValueMapping from './hooks/useTextValueMapping'; import { GenerateConfig } from './generate'; @@ -32,9 +38,11 @@ function getIndexValue( return values ? values[index] : null; } +type UpdateValue = (prev: T) => T; + function updateRangeValue( values: [T | null, T | null] | null, - value: T, + value: T | UpdateValue, index: number, ): [T | null, T | null] | null { const newValues: [T | null, T | null] = [ @@ -42,7 +50,10 @@ function updateRangeValue( getIndexValue(values, 1), ]; - newValues[index] = value; + newValues[index] = + typeof value === 'function' + ? (value as UpdateValue)(newValues[index]) + : value; if (!newValues[0] && !newValues[1]) { return null; @@ -305,6 +316,47 @@ function InnerRangePicker(props: RangePickerProps) { return false; }; + // =========================== View Date =========================== + /** + * End view date is use right panel by default. + * But when they in same month (date picker) or year (month picker), will both use left panel. + */ + function getEndViewDate(viewDate: DateType) { + let compareFunc: ( + generateConfig: GenerateConfig, + date1: DateType | null, + date2: DateType | null, + ) => boolean = isSameMonth; + + if (picker === 'month') { + compareFunc = isSameYear; + } + + if ( + compareFunc( + generateConfig, + getIndexValue(mergedValue, 0), + getIndexValue(mergedValue, 1), + ) + ) { + return viewDate; + } + return getClosingViewDate(viewDate, picker, generateConfig, -1); + } + + // Config view panel + const [viewDates, setViewDates] = useMergedState< + RangeValue, + [DateType, DateType] + >({ + defaultValue: + defaultPickerValue || + updateRangeValue(mergedValue, viewDate => getEndViewDate(viewDate), 1), + defaultStateValue: null, + postState: postViewDates => + postViewDates || [generateConfig.getNow(), generateConfig.getNow()], + }); + // ============================= Text ============================== const sharedTextHooksProps = { formatList, @@ -330,6 +382,7 @@ function InnerRangePicker(props: RangePickerProps) { ); if (inputDate && (!disabledDate || !disabledDate(inputDate))) { setSelectedValue(updateRangeValue(selectedValue, inputDate, index)); + setViewDates(updateRangeValue(viewDates, inputDate, index)); } }; @@ -456,18 +509,7 @@ function InnerRangePicker(props: RangePickerProps) { }; } - // =========================== View Date =========================== - const [viewDates, setViewDates] = useMergedState< - RangeValue, - [DateType, DateType] - >({ - defaultValue: defaultPickerValue || mergedValue, - defaultStateValue: null, - postState: postViewDates => - postViewDates || [generateConfig.getNow(), generateConfig.getNow()], - }); - - // ============================= Panel ============================= + // ============================= Modes ============================= // const [mergedMode, setMode] = useMergedState<[PanelMode, PanelMode]>({ // value: mode, // defaultStateValue: [picker, picker], @@ -481,6 +523,7 @@ function InnerRangePicker(props: RangePickerProps) { // ], // ); // }; + // ============================= Panel ============================= function renderPanel( startPanel?: boolean, diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts index 66e48036a..638450b17 100644 --- a/src/utils/dateUtil.ts +++ b/src/utils/dateUtil.ts @@ -14,6 +14,19 @@ export function isNullEqual(value1: T, value2: T): boolean | undefined { return undefined; } +export function isSameYear( + generateConfig: GenerateConfig, + year1: NullableDateType, + year2: NullableDateType, +) { + const equal = isNullEqual(year1, year2); + if (typeof equal === 'boolean') { + return equal; + } + + return generateConfig.getYear(year1!) === generateConfig.getYear(year2!); +} + export function isSameMonth( generateConfig: GenerateConfig, month1: NullableDateType, @@ -25,7 +38,7 @@ export function isSameMonth( } return ( - generateConfig.getYear(month1!) === generateConfig.getYear(month2!) && + isSameYear(generateConfig, month1, month2) && generateConfig.getMonth(month1!) === generateConfig.getMonth(month2!) ); } From f2888c3e80f3020693ab5da53e6b677c6e09713d Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 09:43:59 +0800 Subject: [PATCH 11/25] Control modes --- src/RangePicker.tsx | 175 ++++++++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 79 deletions(-) diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 83cc0fe6c..3b3aaf0c2 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -40,11 +40,11 @@ function getIndexValue( type UpdateValue = (prev: T) => T; -function updateRangeValue( +function updateRangeValue( values: [T | null, T | null] | null, value: T | UpdateValue, index: number, -): [T | null, T | null] | null { +): R { const newValues: [T | null, T | null] = [ getIndexValue(values, 0), getIndexValue(values, 1), @@ -56,10 +56,10 @@ function updateRangeValue( : value; if (!newValues[0] && !newValues[1]) { - return null; + return (null as unknown) as R; } - return newValues; + return (newValues as unknown) as R; } function reorderValues( @@ -208,7 +208,7 @@ function InnerRangePicker(props: RangePickerProps) { const startInputRef = React.useRef(null); const endInputRef = React.useRef(null); - // ======================== State ======================== + // ============================= Misc ============================== const formatList = toArray( getDefaultFormat(format, picker, showTime, use12Hours), ); @@ -216,7 +216,12 @@ function InnerRangePicker(props: RangePickerProps) { // Active picker const [activePickerIndex, setActivePickerIndex] = React.useState<0 | 1>(0); - // Real value + // Operation ref + const operationRef: React.MutableRefObject = React.useRef< + ContextOperationRefProps + >(null); + + // ============================= Value ============================= const [mergedValue, setInnerValue] = useMergedState({ value, defaultValue, @@ -224,17 +229,74 @@ function InnerRangePicker(props: RangePickerProps) { postState: values => reorderValues(values, generateConfig), }); - // Selected value + // =========================== View Date =========================== + /** + * End view date is use right panel by default. + * But when they in same month (date picker) or year (month picker), will both use left panel. + */ + function getEndViewDate(viewDate: DateType, values: RangeValue) { + let compareFunc: ( + generateConfig: GenerateConfig, + date1: DateType | null, + date2: DateType | null, + ) => boolean = isSameMonth; + + if (picker === 'month') { + compareFunc = isSameYear; + } + + if ( + compareFunc( + generateConfig, + getIndexValue(values, 0), + getIndexValue(values, 1), + ) + ) { + return viewDate; + } + return getClosingViewDate(viewDate, picker, generateConfig, -1); + } + + // Config view panel + const [viewDates, setViewDates] = useMergedState< + RangeValue, + [DateType, DateType] + >({ + defaultValue: + defaultPickerValue || + updateRangeValue( + mergedValue, + (viewDate: DateType) => getEndViewDate(viewDate, mergedValue), + 1, + ), + defaultStateValue: null, + postState: postViewDates => + postViewDates || [generateConfig.getNow(), generateConfig.getNow()], + }); + + // ========================= Select Values ========================= const [selectedValue, setSelectedValue] = React.useState< RangeValue >(mergedValue); - // Operation ref - const operationRef: React.MutableRefObject = React.useRef< - ContextOperationRefProps - >(null); + // ============================= Modes ============================= + const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>({ + value: mode, + defaultStateValue: [picker, picker], + }); + + const triggerModesChange = ( + modes: [PanelMode, PanelMode], + values: RangeValue, + ) => { + setInnerModes(modes); - // Open + if (onPanelChange) { + onPanelChange(values, modes); + } + }; + + // ============================= Open ============================== const [mergedOpen, triggerInnerOpen] = useMergedState({ value: open, defaultValue: defaultOpen, @@ -254,6 +316,7 @@ function InnerRangePicker(props: RangePickerProps) { const startOpen = mergedOpen && activePickerIndex === 0; const endOpen = mergedOpen && activePickerIndex === 1; + // ============================= Popup ============================= // Popup min width const [popupMinWidth, setPopupMinWidth] = React.useState(0); React.useEffect(() => { @@ -316,47 +379,6 @@ function InnerRangePicker(props: RangePickerProps) { return false; }; - // =========================== View Date =========================== - /** - * End view date is use right panel by default. - * But when they in same month (date picker) or year (month picker), will both use left panel. - */ - function getEndViewDate(viewDate: DateType) { - let compareFunc: ( - generateConfig: GenerateConfig, - date1: DateType | null, - date2: DateType | null, - ) => boolean = isSameMonth; - - if (picker === 'month') { - compareFunc = isSameYear; - } - - if ( - compareFunc( - generateConfig, - getIndexValue(mergedValue, 0), - getIndexValue(mergedValue, 1), - ) - ) { - return viewDate; - } - return getClosingViewDate(viewDate, picker, generateConfig, -1); - } - - // Config view panel - const [viewDates, setViewDates] = useMergedState< - RangeValue, - [DateType, DateType] - >({ - defaultValue: - defaultPickerValue || - updateRangeValue(mergedValue, viewDate => getEndViewDate(viewDate), 1), - defaultStateValue: null, - postState: postViewDates => - postViewDates || [generateConfig.getNow(), generateConfig.getNow()], - }); - // ============================= Text ============================== const sharedTextHooksProps = { formatList, @@ -509,20 +531,6 @@ function InnerRangePicker(props: RangePickerProps) { }; } - // ============================= Modes ============================= - // const [mergedMode, setMode] = useMergedState<[PanelMode, PanelMode]>({ - // value: mode, - // defaultStateValue: [picker, picker], - // }); - - // const triggerPanelChange = (date: DateType, newMode: PanelMode) => { - // setMode( - // updateRangeValue(mergedMode, newMode, 0) as [ - // PanelMode, - // PanelMode, - // ], - // ); - // }; // ============================= Panel ============================= function renderPanel( @@ -539,6 +547,7 @@ function InnerRangePicker(props: RangePickerProps) { {...(props as any)} {...panelProps} + mode={mergedModes[activePickerIndex]} generateConfig={generateConfig} style={undefined} className={classNames({ @@ -555,6 +564,12 @@ function InnerRangePicker(props: RangePickerProps) { updateRangeValue(selectedValue, date, activePickerIndex), ); }} + onPanelChange={(date, newMode) => { + triggerModesChange( + updateRangeValue(mergedModes, newMode, activePickerIndex), + updateRangeValue(selectedValue, date, activePickerIndex), + ); + }} /> ); @@ -564,6 +579,7 @@ function InnerRangePicker(props: RangePickerProps) { if (picker !== 'time' && !showTime) { const viewDate = viewDates[activePickerIndex]; const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); + const currentMode = mergedModes[activePickerIndex]; return ( <> @@ -575,18 +591,19 @@ function InnerRangePicker(props: RangePickerProps) { ); }, })} - {renderPanel(false, { - pickerValue: nextViewDate, - onPickerValueChange: newViewDate => { - setViewDates( - updateRangeValue( - viewDates, - getClosingViewDate(newViewDate, picker, generateConfig, -1), - activePickerIndex, - ), - ); - }, - })} + {currentMode === picker && + renderPanel(false, { + pickerValue: nextViewDate, + onPickerValueChange: newViewDate => { + setViewDates( + updateRangeValue( + viewDates, + getClosingViewDate(newViewDate, picker, generateConfig, -1), + activePickerIndex, + ), + ); + }, + })} ); } From 71152ca99163d1636b294374bc1937260d6ea00f Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 10:30:15 +0800 Subject: [PATCH 12/25] Update viewDate logic --- src/PickerPanel.tsx | 6 +-- src/RangeContext.tsx | 2 +- src/RangePicker.tsx | 92 +++++++++++++-------------------- src/panels/DatePanel/index.tsx | 4 +- src/panels/MonthPanel/index.tsx | 2 +- src/panels/YearPanel/index.tsx | 2 +- 6 files changed, 44 insertions(+), 64 deletions(-) diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index 3245c24cd..b8f79bd97 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -145,7 +145,7 @@ function PickerPanel(props: PickerPanelProps) { const panelContext = React.useContext(PanelContext); const { operationRef, panelRef: panelDivRef } = panelContext; - const { extraFooterSelections, inRange, startPanel } = React.useContext( + const { extraFooterSelections, inRange, panelPosition } = React.useContext( RangeContext, ); const panelRef = React.useRef({}); @@ -443,8 +443,8 @@ function PickerPanel(props: PickerPanelProps) { ...panelContext, hideHeader: 'hideHeader' in props ? hideHeader : panelContext.hideHeader, - hidePrevBtn: inRange && !startPanel, - hideNextBtn: inRange && startPanel, + hidePrevBtn: inRange && panelPosition === 'right', + hideNextBtn: inRange && panelPosition === 'left', }} >
, NullableDateType]; inRange?: boolean; - startPanel?: boolean; + panelPosition?: 'left' | 'right' | false; } const RangeContext = React.createContext({}); diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 3b3aaf0c2..a0f9b0adc 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -18,7 +18,6 @@ import PanelContext, { ContextOperationRefProps } from './PanelContext'; import { isEqual, getClosingViewDate, - isSameDate, isSameMonth, isSameYear, } from './utils/dateUtil'; @@ -425,77 +424,54 @@ function InnerRangePicker(props: RangePickerProps) { }); // ============================= Input ============================= - const sharedInputHookProps = { + const getSharedInputHookProps = ( + index: 0 | 1, + inputDivRef: React.RefObject, + resetText: () => void, + ) => ({ forwardKeyDown, onBlur, - }; - - const passOnFocus: React.FocusEventHandler = e => { - if (onFocus) { - onFocus(e); - } - }; - - const [ - startInputProps, - { focused: startFocused, typing: startTyping }, - ] = usePickerInput({ - ...sharedInputHookProps, - open: startOpen, isClickOutside: (target: EventTarget | null) => !!( panelDivRef.current && !panelDivRef.current.contains(target as Node) && - startInputDivRef.current && - !startInputDivRef.current.contains(target as Node) && + inputDivRef.current && + !inputDivRef.current.contains(target as Node) && onOpenChange ), - onFocus: e => { - setActivePickerIndex(0); - passOnFocus(e); + onFocus: (e: React.FocusEvent) => { + setActivePickerIndex(index); + if (onFocus) { + onFocus(e); + } }, - triggerOpen: newOpen => triggerOpen(newOpen, 0), + triggerOpen: (newOpen: boolean) => triggerOpen(newOpen, index), onSubmit: () => { triggerChange(selectedValue); - triggerOpen(false, 0, true); - resetStartText(); + triggerOpen(false, index, true); + resetText(); }, onCancel: () => { - triggerOpen(false, 0, true); + triggerOpen(false, index, true); setSelectedValue(mergedValue); - resetStartText(); + resetText(); }, }); + const [ + startInputProps, + { focused: startFocused, typing: startTyping }, + ] = usePickerInput({ + ...getSharedInputHookProps(0, startInputDivRef, resetStartText), + open: startOpen, + }); + const [ endInputProps, { focused: endFocused, typing: endTyping }, ] = usePickerInput({ - ...sharedInputHookProps, + ...getSharedInputHookProps(1, endInputDivRef, resetEndText), open: endOpen, - isClickOutside: (target: EventTarget | null) => - !!( - panelDivRef.current && - !panelDivRef.current.contains(target as Node) && - endInputDivRef.current && - !endInputDivRef.current.contains(target as Node) && - onOpenChange - ), - onFocus: e => { - setActivePickerIndex(1); - passOnFocus(e); - }, - triggerOpen: newOpen => triggerOpen(newOpen, 1), - onSubmit: () => { - triggerChange(selectedValue); - triggerOpen(false, 1, true); - resetEndText(); - }, - onCancel: () => { - triggerOpen(false, 1, true); - setSelectedValue(mergedValue); - resetEndText(); - }, }); // ============================= Sync ============================== @@ -534,14 +510,14 @@ function InnerRangePicker(props: RangePickerProps) { // ============================= Panel ============================= function renderPanel( - startPanel?: boolean, + panelPosition: 'left' | 'right' | false = false, panelProps: Partial> = {}, ) { return ( @@ -569,6 +545,8 @@ function InnerRangePicker(props: RangePickerProps) { updateRangeValue(mergedModes, newMode, activePickerIndex), updateRangeValue(selectedValue, date, activePickerIndex), ); + + setViewDates(updateRangeValue(viewDates, date, activePickerIndex)); }} /> @@ -581,18 +559,20 @@ function InnerRangePicker(props: RangePickerProps) { const nextViewDate = getClosingViewDate(viewDate, picker, generateConfig); const currentMode = mergedModes[activePickerIndex]; + const showDoublePanel = currentMode === picker; + return ( <> - {renderPanel(true, { + {renderPanel(showDoublePanel ? 'left' : false, { pickerValue: viewDate, - onPickerValueChange: newViewDate => { + onPickerValueChange: (newViewDate: DateType) => { setViewDates( updateRangeValue(viewDates, newViewDate, activePickerIndex), ); }, })} - {currentMode === picker && - renderPanel(false, { + {showDoublePanel && + renderPanel('right', { pickerValue: nextViewDate, onPickerValueChange: newViewDate => { setViewDates( diff --git a/src/panels/DatePanel/index.tsx b/src/panels/DatePanel/index.tsx index 4da2a649c..65812135a 100644 --- a/src/panels/DatePanel/index.tsx +++ b/src/panels/DatePanel/index.tsx @@ -90,10 +90,10 @@ function DatePanel(props: DatePanelProps) { onMonthChange(1); }} onMonthClick={() => { - onPanelChange('month', value || viewDate); + onPanelChange('month', viewDate); }} onYearClick={() => { - onPanelChange('year', value || viewDate); + onPanelChange('year', viewDate); }} /> (props: MonthPanelProps) { onYearChange(1); }} onYearClick={() => { - onPanelChange('year', value || viewDate); + onPanelChange('year', viewDate); }} /> diff --git a/src/panels/YearPanel/index.tsx b/src/panels/YearPanel/index.tsx index 8f9b263ac..23b88ef23 100644 --- a/src/panels/YearPanel/index.tsx +++ b/src/panels/YearPanel/index.tsx @@ -62,7 +62,7 @@ function YearPanel(props: YearPanelProps) { onDecadeChange(1); }} onDecadeClick={() => { - onPanelChange('decade', value || viewDate); + onPanelChange('decade', viewDate); }} /> Date: Tue, 3 Dec 2019 10:48:25 +0800 Subject: [PATCH 13/25] Adjust onSelect logic --- src/PickerPanel.tsx | 30 +++++++++++++++++++++--------- src/RangePicker.tsx | 3 ++- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index b8f79bd97..64deb9f79 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -1,5 +1,12 @@ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ +/** + * Logic: + * When `mode` === `picker`, + * click will trigger `onSelect` (if value changed trigger `onChange` also). + * Panel change will not trigger `onSelect` but trigger `onPanelChange` + */ + import * as React from 'react'; import classNames from 'classnames'; import KeyCode from 'rc-util/lib/KeyCode'; @@ -214,15 +221,20 @@ function PickerPanel(props: PickerPanelProps) { } }; - const triggerSelect = (date: DateType) => { - setInnerValue(date); + const triggerSelect = ( + date: DateType, + forceTriggerSelect: boolean = false, + ) => { + if (mergedMode === picker || forceTriggerSelect) { + setInnerValue(date); - if (onSelect) { - onSelect(date); - } + if (onSelect) { + onSelect(date); + } - if (onChange && !isEqual(generateConfig, date, mergedValue)) { - onChange(date); + if (onChange && !isEqual(generateConfig, date, mergedValue)) { + onChange(date); + } } }; @@ -403,7 +415,7 @@ function PickerPanel(props: PickerPanelProps) { { - triggerSelect(generateConfig.getNow()); + triggerSelect(generateConfig.getNow(), true); }} > {locale.today} @@ -420,7 +432,7 @@ function PickerPanel(props: PickerPanelProps) { mergedSelections.push({ label: locale.now, onClick: () => { - triggerSelect(generateConfig.getNow()); + triggerSelect(generateConfig.getNow(), true); }, }); } diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index a0f9b0adc..0fcb09c1a 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -535,7 +535,7 @@ function InnerRangePicker(props: RangePickerProps) { onMouseDown={e => { e.preventDefault(); }} - onChange={date => { + onSelect={date => { setSelectedValue( updateRangeValue(selectedValue, date, activePickerIndex), ); @@ -548,6 +548,7 @@ function InnerRangePicker(props: RangePickerProps) { setViewDates(updateRangeValue(viewDates, date, activePickerIndex)); }} + onChange={undefined} /> ); From 665ae04c77eabfc356973dd375b7ceba2d3a9e65 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 11:24:32 +0800 Subject: [PATCH 14/25] useMergedState accepts function render --- assets/index.less | 1 + examples/range.tsx | 9 ++++++++- src/RangePicker.tsx | 2 +- src/hooks/useMergeState.ts | 14 +++++++++----- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/assets/index.less b/assets/index.less index 1a5a94c80..eafe1c258 100644 --- a/assets/index.less +++ b/assets/index.less @@ -299,5 +299,6 @@ // ======================================================== &-range { display: inline-flex; + position: relative; } } diff --git a/examples/range.tsx b/examples/range.tsx index 6e6bee686..8bb4f245f 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -52,12 +52,19 @@ export default () => {

Basic

+ + {...sharedProps} + value={undefined} + locale={zhCN} + allowClear + ref={rangePickerRef} + // open + /> {...sharedProps} locale={zhCN} allowClear ref={rangePickerRef} - open /> {/* {...sharedProps} diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 0fcb09c1a..d5b22dd7c 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -261,7 +261,7 @@ function InnerRangePicker(props: RangePickerProps) { RangeValue, [DateType, DateType] >({ - defaultValue: + defaultValue: () => defaultPickerValue || updateRangeValue( mergedValue, diff --git a/src/hooks/useMergeState.ts b/src/hooks/useMergeState.ts index 8bbb024d8..5a00eb488 100644 --- a/src/hooks/useMergeState.ts +++ b/src/hooks/useMergeState.ts @@ -8,8 +8,8 @@ export default function useMergedState({ postState, }: { value?: T; - defaultValue?: T; - defaultStateValue: T; + defaultValue?: T | (() => T); + defaultStateValue: T | (() => T); onChange?: (value: T, prevValue: T) => void; postState?: (value: T) => T; }): [R, (value: T) => void] { @@ -18,9 +18,13 @@ export default function useMergedState({ return value; } if (defaultValue !== undefined) { - return defaultValue; + return typeof defaultValue === 'function' + ? (defaultValue as any)() + : defaultValue; } - return defaultStateValue; + return typeof defaultStateValue === 'function' + ? (defaultStateValue as any)() + : defaultStateValue; }); let mergedValue = value !== undefined ? value : innerValue; @@ -36,5 +40,5 @@ export default function useMergedState({ } } - return [mergedValue, triggerChange]; + return [(mergedValue as unknown) as R, triggerChange]; } From dde33929d5be89c714e34f328fa11b6b8d16be78 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 12:00:26 +0800 Subject: [PATCH 15/25] Next it of un-input picker --- src/RangePicker.tsx | 86 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index d5b22dd7c..c3242ec98 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -77,6 +77,14 @@ function reorderValues( return values; } +function canValueTrigger( + value: EventValue, + index: number, + allowEmpty?: [boolean, boolean] | null, +): boolean { + return !!(value || (allowEmpty && allowEmpty[index])); +} + export interface RangePickerSharedProps { value?: RangeValue; defaultValue?: RangeValue; @@ -175,6 +183,7 @@ function InnerRangePicker(props: RangePickerProps) { locale, placeholder, autoFocus, + allowEmpty, disabled, format, picker = 'date', @@ -325,36 +334,70 @@ function InnerRangePicker(props: RangePickerProps) { }, [mergedOpen]); // ============================ Trigger ============================ - const triggerChange = (newValue: RangeValue) => { + let triggerOpen: ( + newOpen: boolean, + index: 0 | 1, + preventChangeEvent?: boolean, + ) => void; + + const triggerChange = ( + newValue: RangeValue, + forceInput: boolean = true, + ) => { const values = reorderValues(newValue, generateConfig); setSelectedValue(values); - setInnerValue(values); const startValue = getIndexValue(values, 0); const endValue = getIndexValue(values, 1); - if ( - onChange && - (!isEqual(generateConfig, getIndexValue(mergedValue, 0), startValue) || - !isEqual(generateConfig, getIndexValue(mergedValue, 1), endValue)) - ) { - onChange(values, [ - startValue - ? generateConfig.locale.format( - locale.locale, - startValue, - formatList[0], - ) - : '', - endValue - ? generateConfig.locale.format(locale.locale, endValue, formatList[0]) - : '', - ]); + const canStartValueTrigger = canValueTrigger(startValue, 0, allowEmpty); + const canEndValueTrigger = canValueTrigger(endValue, 1, allowEmpty); + + const canTrigger = + values === null || (canStartValueTrigger && canEndValueTrigger); + + if (canTrigger) { + // Trigger onChange only when value is validate + setInnerValue(values); + + if ( + onChange && + (!isEqual(generateConfig, getIndexValue(mergedValue, 0), startValue) || + !isEqual(generateConfig, getIndexValue(mergedValue, 1), endValue)) + ) { + onChange(values, [ + startValue + ? generateConfig.locale.format( + locale.locale, + startValue, + formatList[0], + ) + : '', + endValue + ? generateConfig.locale.format( + locale.locale, + endValue, + formatList[0], + ) + : '', + ]); + } + } else if (forceInput) { + // Open miss value panel to force user input + const missingValueIndex = canStartValueTrigger ? 1 : 0; + triggerOpen(true, missingValueIndex); + + // Delay to focus to avoid input blur trigger expired selectedValues + setTimeout(() => { + if (endInputRef.current) { + endInputRef.current.focus(); + } + }, 0); } }; - const triggerOpen = ( + triggerOpen = ( newOpen: boolean, index: 0 | 1, preventChangeEvent: boolean = false, @@ -536,7 +579,8 @@ function InnerRangePicker(props: RangePickerProps) { e.preventDefault(); }} onSelect={date => { - setSelectedValue( + // triggerChange will also update selected values + triggerChange( updateRangeValue(selectedValue, date, activePickerIndex), ); }} From 758dfc76e76a07b115bf93767fd358233d2ad736 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 12:04:38 +0800 Subject: [PATCH 16/25] fix focus logic --- src/RangePicker.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index c3242ec98..94a8ae600 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -390,8 +390,9 @@ function InnerRangePicker(props: RangePickerProps) { // Delay to focus to avoid input blur trigger expired selectedValues setTimeout(() => { - if (endInputRef.current) { - endInputRef.current.focus(); + const inputRef = [startInputRef, endInputRef][missingValueIndex]; + if (inputRef.current) { + inputRef.current.focus(); } }, 0); } From d44729830100ff16661a739714fb35d50c37da82 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 14:00:55 +0800 Subject: [PATCH 17/25] close when all selected --- src/RangePicker.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 94a8ae600..9954c1019 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -360,6 +360,7 @@ function InnerRangePicker(props: RangePickerProps) { if (canTrigger) { // Trigger onChange only when value is validate setInnerValue(values); + triggerOpen(false, activePickerIndex, true); if ( onChange && @@ -386,6 +387,12 @@ function InnerRangePicker(props: RangePickerProps) { } else if (forceInput) { // Open miss value panel to force user input const missingValueIndex = canStartValueTrigger ? 1 : 0; + + // Same index means user choice to close picker + if (missingValueIndex === activePickerIndex) { + return; + } + triggerOpen(true, missingValueIndex); // Delay to focus to avoid input blur trigger expired selectedValues From a7cb2277ab81ab1788d22cb3d26ca14e87607f38 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 14:19:03 +0800 Subject: [PATCH 18/25] fix uncontrol pass pickerValue --- assets/index.less | 1 + examples/range.tsx | 27 +++++------------------ src/PickerPanel.tsx | 54 +++++++++++++++++++++++++-------------------- src/RangePicker.tsx | 18 ++++++++++++--- src/interface.ts | 9 +------- 5 files changed, 52 insertions(+), 57 deletions(-) diff --git a/assets/index.less b/assets/index.less index eafe1c258..b1a39bf8f 100644 --- a/assets/index.less +++ b/assets/index.less @@ -181,6 +181,7 @@ > li { padding: 0; margin: 0; + cursor: pointer; .@{prefix-cls}-time-panel-cell-inner { color: #333; diff --git a/examples/range.tsx b/examples/range.tsx index 8bb4f245f..05b9fd3ae 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -58,40 +58,23 @@ export default () => { locale={zhCN} allowClear ref={rangePickerRef} - // open /> {...sharedProps} locale={zhCN} allowClear ref={rangePickerRef} + showTime /> - {/* - {...sharedProps} - locale={zhCN} - allowClear - ref={rangePickerRef} - picker="month" - open - /> - - {...sharedProps} - locale={zhCN} - allowClear - ref={rangePickerRef} - picker="year" - open - /> */}
- {/*
+

Basic

{...sharedProps} locale={zhCN} allowClear ref={rangePickerRef} - open // style={{ width: 500 }} /> -
*/} - {/*
+
+

Basic

{...sharedProps} locale={zhCN} picker="year" />
@@ -143,7 +126,7 @@ export default () => { }} allowClear /> -
*/} +
); diff --git a/src/PickerPanel.tsx b/src/PickerPanel.tsx index 64deb9f79..c1dd70cec 100644 --- a/src/PickerPanel.tsx +++ b/src/PickerPanel.tsx @@ -198,9 +198,6 @@ function PickerPanel(props: PickerPanelProps) { return getNextMode(nextMode); } - if (nextMode === 'date' && showTime) { - return 'datetime'; - } return nextMode; }; @@ -210,8 +207,17 @@ function PickerPanel(props: PickerPanelProps) { } return getInternalNextMode('date'); }); + const mergedMode: PanelMode = mode || innerMode; + // const mergedMode: PanelMode = React.useMemo(() => { + // const newMode = mode || innerMode; + // if (newMode === 'date' && showTime) { + // return 'datetime'; + // } + // return newMode; + // }, [mode || innerMode]); + const onInternalPanelChange = (newMode: PanelMode, viewValue: DateType) => { const nextMode = getInternalNextMode(newMode); setInnerMode(nextMode); @@ -361,18 +367,6 @@ function PickerPanel(props: PickerPanelProps) { ); break; - case 'datetime': - panelNode = ( - { - setViewDate(date); - triggerSelect(date); - }} - /> - ); - break; - case 'time': delete pickerProps.showTime; panelNode = ( @@ -388,15 +382,27 @@ function PickerPanel(props: PickerPanelProps) { break; default: - panelNode = ( - - {...pickerProps} - onSelect={date => { - setViewDate(date); - triggerSelect(date); - }} - /> - ); + if (showTime) { + panelNode = ( + { + setViewDate(date); + triggerSelect(date); + }} + /> + ); + } else { + panelNode = ( + + {...pickerProps} + onSelect={date => { + setViewDate(date); + triggerSelect(date); + }} + /> + ); + } } // ============================ Footer ============================ diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 9954c1019..15ec4f814 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -587,10 +587,18 @@ function InnerRangePicker(props: RangePickerProps) { e.preventDefault(); }} onSelect={date => { - // triggerChange will also update selected values - triggerChange( - updateRangeValue(selectedValue, date, activePickerIndex), + const values = updateRangeValue( + selectedValue, + date, + activePickerIndex, ); + + if (picker === 'date' && showTime) { + setSelectedValue(values); + } else { + // triggerChange will also update selected values + triggerChange(values); + } }} onPanelChange={(date, newMode) => { triggerModesChange( @@ -601,6 +609,8 @@ function InnerRangePicker(props: RangePickerProps) { setViewDates(updateRangeValue(viewDates, date, activePickerIndex)); }} onChange={undefined} + defaultValue={undefined} + defaultPickerValue={undefined} /> ); @@ -614,6 +624,8 @@ function InnerRangePicker(props: RangePickerProps) { const showDoublePanel = currentMode === picker; + console.log('=>', viewDate); + return ( <> {renderPanel(showDoublePanel ? 'left' : false, { diff --git a/src/interface.ts b/src/interface.ts index 817e55a05..a07208f2c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -39,14 +39,7 @@ export interface Locale { shortMonths?: string[]; } -export type PanelMode = - | 'time' - | 'datetime' - | 'date' - | 'week' - | 'month' - | 'year' - | 'decade'; +export type PanelMode = 'time' | 'date' | 'week' | 'month' | 'year' | 'decade'; export type PickerMode = Exclude; From ccd4c4fab1c3c8aefae24a3e5d6a471eb4b8ef7f Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 14:23:55 +0800 Subject: [PATCH 19/25] fix year body select logic --- src/RangePicker.tsx | 2 -- src/panels/YearPanel/YearBody.tsx | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 15ec4f814..c1cde6c87 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -624,8 +624,6 @@ function InnerRangePicker(props: RangePickerProps) { const showDoublePanel = currentMode === picker; - console.log('=>', viewDate); - return ( <> {renderPanel(showDoublePanel ? 'left' : false, { diff --git a/src/panels/YearPanel/YearBody.tsx b/src/panels/YearPanel/YearBody.tsx index 69f3c62c9..3573223bc 100644 --- a/src/panels/YearPanel/YearBody.tsx +++ b/src/panels/YearPanel/YearBody.tsx @@ -11,6 +11,7 @@ export interface YearBodyProps { prefixCls: string; locale: Locale; generateConfig: GenerateConfig; + value: DateType; viewDate: DateType; disabledDate?: (date: DateType) => boolean; onSelect: (value: DateType) => void; @@ -18,6 +19,7 @@ export interface YearBodyProps { function YearBody({ prefixCls, + value, viewDate, locale, generateConfig, @@ -27,6 +29,7 @@ function YearBody({ const yearPrefixCls = `${prefixCls}-cell`; const rows: React.ReactNode[] = []; + const valueYearNumber = value ? generateConfig.getYear(value) : null; const yearNumber = generateConfig.getYear(viewDate); const startYear = Math.floor(yearNumber / YEAR_DECADE_COUNT) * YEAR_DECADE_COUNT; @@ -54,7 +57,8 @@ function YearBody({ [`${yearPrefixCls}-disabled`]: disabled, [`${yearPrefixCls}-in-view`]: startYear <= currentYearNumber && currentYearNumber <= endYear, - [`${yearPrefixCls}-selected`]: currentYearNumber === yearNumber, + [`${yearPrefixCls}-selected`]: + currentYearNumber === valueYearNumber, })} onClick={() => { if (disabled) { From fff171a985b4e86a204d249442cc6552217f5d80 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 14:59:51 +0800 Subject: [PATCH 20/25] init disabled range logic --- examples/range.tsx | 9 +++ src/RangePicker.tsx | 102 ++++++++++-------------------- src/hooks/useRangeDisabled.ts | 21 ++++++ src/interface.ts | 3 + src/panels/YearPanel/YearBody.tsx | 4 +- src/utils/miscUtil.ts | 31 +++++++++ 6 files changed, 101 insertions(+), 69 deletions(-) create mode 100644 src/hooks/useRangeDisabled.ts diff --git a/examples/range.tsx b/examples/range.tsx index 05b9fd3ae..989e44231 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -98,6 +98,15 @@ export default () => { locale={zhCN} allowClear allowEmpty={[true, true]} + /> +
+ +
+

Selectable

+ + {...sharedProps} + locale={zhCN} + allowClear selectable={[true, false]} />
diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index c1cde6c87..74da8d50c 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -1,6 +1,12 @@ import * as React from 'react'; import classNames from 'classnames'; -import { DisabledTimes, PanelMode, PickerMode } from './interface'; +import { + DisabledTimes, + PanelMode, + PickerMode, + RangeValue, + EventValue, +} from './interface'; import { PickerBaseProps, PickerDateProps, @@ -12,7 +18,11 @@ import useMergedState from './hooks/useMergeState'; import PickerTrigger from './PickerTrigger'; import PickerPanel from './PickerPanel'; import usePickerInput from './hooks/usePickerInput'; -import getDataOrAriaProps, { toArray } from './utils/miscUtil'; +import getDataOrAriaProps, { + toArray, + getValue, + updateValues, +} from './utils/miscUtil'; import { getDefaultFormat, getInputSize } from './utils/uiUtil'; import PanelContext, { ContextOperationRefProps } from './PanelContext'; import { @@ -27,40 +37,6 @@ import { GenerateConfig } from './generate'; import { PickerPanelProps } from '.'; import RangeContext from './RangeContext'; -type EventValue = DateType | null; -type RangeValue = [EventValue, EventValue] | null; - -function getIndexValue( - values: null | undefined | [T | null, T | null], - index: number, -): T | null { - return values ? values[index] : null; -} - -type UpdateValue = (prev: T) => T; - -function updateRangeValue( - values: [T | null, T | null] | null, - value: T | UpdateValue, - index: number, -): R { - const newValues: [T | null, T | null] = [ - getIndexValue(values, 0), - getIndexValue(values, 1), - ]; - - newValues[index] = - typeof value === 'function' - ? (value as UpdateValue)(newValues[index]) - : value; - - if (!newValues[0] && !newValues[1]) { - return (null as unknown) as R; - } - - return (newValues as unknown) as R; -} - function reorderValues( values: RangeValue, generateConfig: GenerateConfig, @@ -183,7 +159,6 @@ function InnerRangePicker(props: RangePickerProps) { locale, placeholder, autoFocus, - allowEmpty, disabled, format, picker = 'date', @@ -196,6 +171,8 @@ function InnerRangePicker(props: RangePickerProps) { open, defaultOpen, disabledDate, + selectable, + allowEmpty, allowClear, suffixIcon, clearIcon, @@ -253,13 +230,7 @@ function InnerRangePicker(props: RangePickerProps) { compareFunc = isSameYear; } - if ( - compareFunc( - generateConfig, - getIndexValue(values, 0), - getIndexValue(values, 1), - ) - ) { + if (compareFunc(generateConfig, getValue(values, 0), getValue(values, 1))) { return viewDate; } return getClosingViewDate(viewDate, picker, generateConfig, -1); @@ -272,7 +243,7 @@ function InnerRangePicker(props: RangePickerProps) { >({ defaultValue: () => defaultPickerValue || - updateRangeValue( + updateValues( mergedValue, (viewDate: DateType) => getEndViewDate(viewDate, mergedValue), 1, @@ -348,8 +319,8 @@ function InnerRangePicker(props: RangePickerProps) { setSelectedValue(values); - const startValue = getIndexValue(values, 0); - const endValue = getIndexValue(values, 1); + const startValue = getValue(values, 0); + const endValue = getValue(values, 1); const canStartValueTrigger = canValueTrigger(startValue, 0, allowEmpty); const canEndValueTrigger = canValueTrigger(endValue, 1, allowEmpty); @@ -364,8 +335,8 @@ function InnerRangePicker(props: RangePickerProps) { if ( onChange && - (!isEqual(generateConfig, getIndexValue(mergedValue, 0), startValue) || - !isEqual(generateConfig, getIndexValue(mergedValue, 1), endValue)) + (!isEqual(generateConfig, getValue(mergedValue, 0), startValue) || + !isEqual(generateConfig, getValue(mergedValue, 1), endValue)) ) { onChange(values, [ startValue @@ -437,12 +408,12 @@ function InnerRangePicker(props: RangePickerProps) { }; const startValueTexts = useValueTexts( - getIndexValue(selectedValue, 0), + getValue(selectedValue, 0), sharedTextHooksProps, ); const endValueTexts = useValueTexts( - getIndexValue(selectedValue, 1), + getValue(selectedValue, 1), sharedTextHooksProps, ); @@ -453,8 +424,8 @@ function InnerRangePicker(props: RangePickerProps) { formatList, ); if (inputDate && (!disabledDate || !disabledDate(inputDate))) { - setSelectedValue(updateRangeValue(selectedValue, inputDate, index)); - setViewDates(updateRangeValue(viewDates, inputDate, index)); + setSelectedValue(updateValues(selectedValue, inputDate, index)); + setViewDates(updateValues(viewDates, inputDate, index)); } }; @@ -580,18 +551,14 @@ function InnerRangePicker(props: RangePickerProps) { className={classNames({ [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, })} - value={getIndexValue(selectedValue, activePickerIndex)} + value={getValue(selectedValue, activePickerIndex)} locale={locale} tabIndex={-1} onMouseDown={e => { e.preventDefault(); }} onSelect={date => { - const values = updateRangeValue( - selectedValue, - date, - activePickerIndex, - ); + const values = updateValues(selectedValue, date, activePickerIndex); if (picker === 'date' && showTime) { setSelectedValue(values); @@ -602,11 +569,11 @@ function InnerRangePicker(props: RangePickerProps) { }} onPanelChange={(date, newMode) => { triggerModesChange( - updateRangeValue(mergedModes, newMode, activePickerIndex), - updateRangeValue(selectedValue, date, activePickerIndex), + updateValues(mergedModes, newMode, activePickerIndex), + updateValues(selectedValue, date, activePickerIndex), ); - setViewDates(updateRangeValue(viewDates, date, activePickerIndex)); + setViewDates(updateValues(viewDates, date, activePickerIndex)); }} onChange={undefined} defaultValue={undefined} @@ -630,7 +597,7 @@ function InnerRangePicker(props: RangePickerProps) { pickerValue: viewDate, onPickerValueChange: (newViewDate: DateType) => { setViewDates( - updateRangeValue(viewDates, newViewDate, activePickerIndex), + updateValues(viewDates, newViewDate, activePickerIndex), ); }, })} @@ -639,7 +606,7 @@ function InnerRangePicker(props: RangePickerProps) { pickerValue: nextViewDate, onPickerValueChange: newViewDate => { setViewDates( - updateRangeValue( + updateValues( viewDates, getClosingViewDate(newViewDate, picker, generateConfig, -1), activePickerIndex, @@ -682,7 +649,6 @@ function InnerRangePicker(props: RangePickerProps) { } const inputSharedProps = { - disabled, size: getInputSize(picker, formatList[0]), }; @@ -715,11 +681,12 @@ function InnerRangePicker(props: RangePickerProps) { >
(props: RangePickerProps) { {separator}
( + selectedValues: RangeValue, + disabledDate: (date: DateType) => boolean, + generateConfig: GenerateConfig, +) { + const startDate = getValue(selectedValues, 0); + const endDate = getValue(selectedValues, 1); + + function disableEndDate(date: DateType) { + if (disabledDate && disabledDate(date)) { + return true; + } + + if (startDate) {} + } +} diff --git a/src/interface.ts b/src/interface.ts index a07208f2c..3122868bd 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -90,3 +90,6 @@ export type OnPanelChange = ( value: DateType, mode: PanelMode, ) => void; + +export type EventValue = DateType | null; +export type RangeValue = [EventValue, EventValue] | null; diff --git a/src/panels/YearPanel/YearBody.tsx b/src/panels/YearPanel/YearBody.tsx index 3573223bc..cbf618e5e 100644 --- a/src/panels/YearPanel/YearBody.tsx +++ b/src/panels/YearPanel/YearBody.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import classNames from 'classnames'; import { GenerateConfig } from '../../generate'; import { YEAR_DECADE_COUNT } from '.'; -import { Locale } from '../../interface'; +import { Locale, NullableDateType } from '../../interface'; export const YEAR_COL_COUNT = 3; const YEAR_ROW_COUNT = 4; @@ -11,7 +11,7 @@ export interface YearBodyProps { prefixCls: string; locale: Locale; generateConfig: GenerateConfig; - value: DateType; + value: NullableDateType; viewDate: DateType; disabledDate?: (date: DateType) => boolean; onSelect: (value: DateType) => void; diff --git a/src/utils/miscUtil.ts b/src/utils/miscUtil.ts index badd058a0..ed2a203cb 100644 --- a/src/utils/miscUtil.ts +++ b/src/utils/miscUtil.ts @@ -37,3 +37,34 @@ export default function getDataOrAriaProps(props: any) { return retProps; } + +export function getValue( + values: null | undefined | [T | null, T | null], + index: number, +): T | null { + return values ? values[index] : null; +} + +type UpdateValue = (prev: T) => T; + +export function updateValues( + values: [T | null, T | null] | null, + value: T | UpdateValue, + index: number, +): R { + const newValues: [T | null, T | null] = [ + getValue(values, 0), + getValue(values, 1), + ]; + + newValues[index] = + typeof value === 'function' + ? (value as UpdateValue)(newValues[index]) + : value; + + if (!newValues[0] && !newValues[1]) { + return (null as unknown) as R; + } + + return (newValues as unknown) as R; +} From 17af9ef5638cc49b7b56c1ac412eccf9df14738b Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 15:00:10 +0800 Subject: [PATCH 21/25] init disabled range logic --- src/hooks/useRangeDisabled.ts | 3 ++- src/interface.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hooks/useRangeDisabled.ts b/src/hooks/useRangeDisabled.ts index fcfe98e0c..6ce907003 100644 --- a/src/hooks/useRangeDisabled.ts +++ b/src/hooks/useRangeDisabled.ts @@ -16,6 +16,7 @@ export default function useRangeDisabled( return true; } - if (startDate) {} + if (startDate) { + } } } diff --git a/src/interface.ts b/src/interface.ts index 3122868bd..ae038eec0 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -92,4 +92,6 @@ export type OnPanelChange = ( ) => void; export type EventValue = DateType | null; -export type RangeValue = [EventValue, EventValue] | null; +export type RangeValue = + | [EventValue, EventValue] + | null; From cc3f9cb93ddcd59cfb7c571d28ff605e44e5ce74 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 15:54:00 +0800 Subject: [PATCH 22/25] disabled will always trigger event --- examples/range.tsx | 4 +- src/RangePicker.tsx | 86 +++++++++++++++++++++++++++++------ src/hooks/useRangeDisabled.ts | 3 ++ 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/examples/range.tsx b/examples/range.tsx index 989e44231..2afb31bc2 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -102,12 +102,12 @@ export default () => {
-

Selectable

+

Part disabled

{...sharedProps} locale={zhCN} allowClear - selectable={[true, false]} + disabled={[true, false]} />
diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 74da8d50c..6936262ca 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -56,9 +56,23 @@ function reorderValues( function canValueTrigger( value: EventValue, index: number, + disabledList: [boolean, boolean], allowEmpty?: [boolean, boolean] | null, ): boolean { - return !!(value || (allowEmpty && allowEmpty[index])); + if (value) { + return true; + } + + if (allowEmpty && allowEmpty[index]) { + return true; + } + + // If another one is disabled, this can be trigger + if (disabledList[(index + 1) % 2]) { + return true; + } + + return false; } export interface RangePickerSharedProps { @@ -66,6 +80,7 @@ export interface RangePickerSharedProps { defaultValue?: RangeValue; defaultPickerValue?: [DateType, DateType]; placeholder?: [string, string]; + disabled?: boolean | [boolean, boolean]; disabledTime?: ( date: EventValue, type: 'start' | 'end', @@ -77,7 +92,6 @@ export interface RangePickerSharedProps { >; separator?: React.ReactNode; allowEmpty?: [boolean, boolean]; - selectable?: [boolean, boolean]; mode?: [PanelMode, PanelMode]; onChange?: ( values: RangeValue, @@ -101,6 +115,7 @@ type OmitPickerProps = Omit< | 'defaultValue' | 'defaultPickerValue' | 'placeholder' + | 'disabled' | 'disabledTime' | 'showToday' | 'showTime' @@ -171,7 +186,6 @@ function InnerRangePicker(props: RangePickerProps) { open, defaultOpen, disabledDate, - selectable, allowEmpty, allowClear, suffixIcon, @@ -206,8 +220,16 @@ function InnerRangePicker(props: RangePickerProps) { ContextOperationRefProps >(null); + const mergedDisabled = React.useMemo<[boolean, boolean]>(() => { + if (Array.isArray(disabled)) { + return disabled; + } + + return [disabled || false, disabled || false]; + }, [disabled]); + // ============================= Value ============================= - const [mergedValue, setInnerValue] = useMergedState({ + const [mergedValue, setInnerValue] = useMergedState>({ value, defaultValue, defaultStateValue: null, @@ -254,9 +276,18 @@ function InnerRangePicker(props: RangePickerProps) { }); // ========================= Select Values ========================= - const [selectedValue, setSelectedValue] = React.useState< - RangeValue - >(mergedValue); + const [selectedValue, setSelectedValue] = useMergedState({ + defaultStateValue: mergedValue, + postState: values => { + let postValues = values; + for (let i = 0; i < 2; i += 1) { + if (mergedDisabled[i] && !getValue(postValues, i)) { + postValues = updateValues(postValues, generateConfig.getNow(), i); + } + } + return postValues; + }, + }); // ============================= Modes ============================= const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>({ @@ -280,7 +311,8 @@ function InnerRangePicker(props: RangePickerProps) { value: open, defaultValue: defaultOpen, defaultStateValue: false, - postState: postOpen => (disabled ? false : postOpen), + postState: postOpen => + mergedDisabled[activePickerIndex] ? false : postOpen, onChange: newOpen => { if (onOpenChange) { onOpenChange(newOpen); @@ -322,8 +354,18 @@ function InnerRangePicker(props: RangePickerProps) { const startValue = getValue(values, 0); const endValue = getValue(values, 1); - const canStartValueTrigger = canValueTrigger(startValue, 0, allowEmpty); - const canEndValueTrigger = canValueTrigger(endValue, 1, allowEmpty); + const canStartValueTrigger = canValueTrigger( + startValue, + 0, + mergedDisabled, + allowEmpty, + ); + const canEndValueTrigger = canValueTrigger( + endValue, + 1, + mergedDisabled, + allowEmpty, + ); const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); @@ -634,12 +676,25 @@ function InnerRangePicker(props: RangePickerProps) { } let clearNode: React.ReactNode; - if (allowClear && mergedValue && !disabled) { + if ( + allowClear && + ((getValue(mergedValue, 0) && !mergedDisabled[0]) || + (getValue(mergedValue, 1) && !mergedDisabled[1])) + ) { clearNode = ( { e.stopPropagation(); - triggerChange(null); + let values = mergedValue; + + if (!mergedDisabled[0]) { + values = updateValues(values, null, 0); + } + if (!mergedDisabled[1]) { + values = updateValues(values, null, 1); + } + + triggerChange(values, false); }} className={`${prefixCls}-clear`} > @@ -673,7 +728,8 @@ function InnerRangePicker(props: RangePickerProps) {
(props: RangePickerProps) { >
(props: RangePickerProps) { {separator}
( } if (startDate) { + // return generateConfig. } + + return false; } } From bce84b1a397d47567ea303b5bb0809314d10f46e Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 16:21:41 +0800 Subject: [PATCH 23/25] auto adjust disabledDate range --- examples/range.tsx | 11 ++++++++- src/RangePicker.tsx | 25 ++++++++++++++++---- src/hooks/useRangeDisabled.ts | 44 ++++++++++++++++++++++++++++------- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/examples/range.tsx b/examples/range.tsx index 2afb31bc2..8b73d1019 100644 --- a/examples/range.tsx +++ b/examples/range.tsx @@ -102,7 +102,7 @@ export default () => {
-

Part disabled

+

Start disabled

{...sharedProps} locale={zhCN} @@ -110,6 +110,15 @@ export default () => { disabled={[true, false]} />
+
+

End disabled

+ + {...sharedProps} + locale={zhCN} + allowClear + disabled={[false, true]} + /> +

Uncontrolled

diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 6936262ca..81c3a2361 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -36,6 +36,7 @@ import useTextValueMapping from './hooks/useTextValueMapping'; import { GenerateConfig } from './generate'; import { PickerPanelProps } from '.'; import RangeContext from './RangeContext'; +import useRangeDisabled from './hooks/useRangeDisabled'; function reorderValues( values: RangeValue, @@ -272,7 +273,10 @@ function InnerRangePicker(props: RangePickerProps) { ), defaultStateValue: null, postState: postViewDates => - postViewDates || [generateConfig.getNow(), generateConfig.getNow()], + postViewDates || [ + getValue(mergedValue, 0) || generateConfig.getNow(), + getValue(mergedValue, 0) || generateConfig.getNow(), + ], }); // ========================= Select Values ========================= @@ -306,6 +310,14 @@ function InnerRangePicker(props: RangePickerProps) { } }; + // ========================= Disable Date ========================== + const [disabledStartDate, disabledEndDate] = useRangeDisabled({ + selectedValue, + disabled: mergedDisabled, + disabledDate, + generateConfig, + }); + // ============================= Open ============================== const [mergedOpen, triggerInnerOpen] = useMergedState({ value: open, @@ -465,7 +477,10 @@ function InnerRangePicker(props: RangePickerProps) { newText, formatList, ); - if (inputDate && (!disabledDate || !disabledDate(inputDate))) { + + const disabledFunc = index === 0 ? disabledStartDate : disabledEndDate; + + if (inputDate && !disabledFunc(inputDate)) { setSelectedValue(updateValues(selectedValue, inputDate, index)); setViewDates(updateValues(viewDates, inputDate, index)); } @@ -572,7 +587,6 @@ function InnerRangePicker(props: RangePickerProps) { } // ============================= Panel ============================= - function renderPanel( panelPosition: 'left' | 'right' | false = false, panelProps: Partial> = {}, @@ -590,6 +604,9 @@ function InnerRangePicker(props: RangePickerProps) { mode={mergedModes[activePickerIndex]} generateConfig={generateConfig} style={undefined} + disabledDate={ + activePickerIndex === 0 ? disabledStartDate : disabledEndDate + } className={classNames({ [`${prefixCls}-panel-focused`]: !startTyping && !endTyping, })} @@ -752,7 +769,7 @@ function InnerRangePicker(props: RangePickerProps) {
( - selectedValues: RangeValue, - disabledDate: (date: DateType) => boolean, - generateConfig: GenerateConfig, -) { - const startDate = getValue(selectedValues, 0); - const endDate = getValue(selectedValues, 1); +export default function useRangeDisabled({ + selectedValue, + disabledDate, + disabled, + generateConfig, +}: { + selectedValue: RangeValue; + disabledDate?: (date: DateType) => boolean; + disabled: [boolean, boolean]; + generateConfig: GenerateConfig; +}) { + const startDate = getValue(selectedValue, 0); + const endDate = getValue(selectedValue, 1); + + function disabledStartDate(date: DateType) { + if (disabledDate && disabledDate(date)) { + return true; + } + + if (disabled[1] && endDate) { + return ( + !isSameDate(generateConfig, date, endDate) && + generateConfig.isAfter(date, endDate) + ); + } + + return false; + } function disableEndDate(date: DateType) { if (disabledDate && disabledDate(date)) { @@ -17,9 +38,14 @@ export default function useRangeDisabled( } if (startDate) { - // return generateConfig. + return ( + !isSameDate(generateConfig, date, startDate) && + generateConfig.isAfter(startDate, date) + ); } return false; } + + return [disabledStartDate, disableEndDate]; } From 3215fe1324ef792c4828e54dc35ccf282d69395c Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 17:24:06 +0800 Subject: [PATCH 24/25] line hover of ranges --- assets/index.less | 28 +++++++++++++++++++++++ src/PanelContext.tsx | 2 ++ src/RangeContext.tsx | 3 ++- src/RangePicker.tsx | 21 +++++++++++++++++ src/panels/DatePanel/DateBody.tsx | 35 +++++++++++++++++++++++++---- src/panels/MonthPanel/MonthBody.tsx | 18 ++++++++++++--- src/panels/YearPanel/YearBody.tsx | 18 ++++++++++++--- 7 files changed, 114 insertions(+), 11 deletions(-) diff --git a/assets/index.less b/assets/index.less index b1a39bf8f..c52685ff5 100644 --- a/assets/index.less +++ b/assets/index.less @@ -35,6 +35,7 @@ table { text-align: center; + border-collapse: collapse; } } @@ -93,9 +94,36 @@ &-in-range > &-inner { background: fade(blue, 5%); } + &-range-hover-start, + &-range-hover-end, + &-range-hover { + position: relative; + &::after { + content: ''; + position: absolute; + top: 3px; + bottom: 0; + left: 0; + right: 0; + border: 1px solid green; + border-left: 0; + border-right: 0; + pointer-events: none; + } + } + + &-range-hover-start::after { + border-left: 1px solid green!important; + } + &-range-hover-end::after { + border-right: 1px solid green!important; + } + &-today > &-inner { border: 1px solid blue; } + &-range-start > &-inner, + &-range-end > &-inner, &-selected > &-inner { background: fade(blue, 20%); } diff --git a/src/PanelContext.tsx b/src/PanelContext.tsx index b186ef244..796bcdfdb 100644 --- a/src/PanelContext.tsx +++ b/src/PanelContext.tsx @@ -12,6 +12,8 @@ export interface PanelContextProps { panelRef?: React.Ref; hidePrevBtn?: boolean; hideNextBtn?: boolean; + onDateMouseEnter?: (date: any) => void; + onDateMouseLeave?: (date: any) => void; } const PanelContext = React.createContext({}); diff --git a/src/RangeContext.tsx b/src/RangeContext.tsx index cb23e6e8c..4856780c9 100644 --- a/src/RangeContext.tsx +++ b/src/RangeContext.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NullableDateType } from './interface'; +import { NullableDateType, RangeValue } from './interface'; export interface FooterSelection { label: string; @@ -13,6 +13,7 @@ interface RangeContextProps { * Panel only has one value, this is only style effect. */ rangedValue?: [NullableDateType, NullableDateType]; + hoverRangedValue?: RangeValue; inRange?: boolean; panelPosition?: 'left' | 'right' | false; } diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 81c3a2361..9110d164e 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -293,6 +293,18 @@ function InnerRangePicker(props: RangePickerProps) { }, }); + // ========================== Hover Range ========================== + const [hoverRangedValue, setHoverRangedValue] = React.useState< + RangeValue + >(null); + + const onDateMouseEnter = (date: DateType) => { + setHoverRangedValue(updateValues(selectedValue, date, activePickerIndex)); + }; + const onDateMouseLeave = () => { + setHoverRangedValue(updateValues(selectedValue, null, activePickerIndex)); + }; + // ============================= Modes ============================= const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>({ value: mode, @@ -591,11 +603,18 @@ function InnerRangePicker(props: RangePickerProps) { panelPosition: 'left' | 'right' | false = false, panelProps: Partial> = {}, ) { + let panelHoverRangedValue: RangeValue = null; + if (hoverRangedValue && hoverRangedValue[0] && hoverRangedValue[1]) { + panelHoverRangedValue = hoverRangedValue; + } + return ( @@ -730,6 +749,8 @@ function InnerRangePicker(props: RangePickerProps) { operationRef, hideHeader: picker === 'time', panelRef: panelDivRef, + onDateMouseEnter, + onDateMouseLeave, }} > = ( currentDate: DateType, @@ -48,7 +49,8 @@ function DateBody({ dateRender, onSelect, }: DateBodyProps) { - const { rangedValue } = React.useContext(RangeContext); + const { rangedValue, hoverRangedValue } = React.useContext(RangeContext); + const { onDateMouseEnter, onDateMouseLeave } = React.useContext(PanelContext); const datePrefixCls = `${prefixCls}-cell`; const weekFirstDay = generateConfig.locale.getWeekFirstDay(locale.locale); @@ -95,10 +97,19 @@ function DateBody({ 'YYYY-MM-DD', )} onClick={() => { - if (disabled) { - return; + if (!disabled) { + onSelect(currentDate); + } + }} + onMouseEnter={() => { + if (!disabled && onDateMouseEnter) { + onDateMouseEnter(currentDate); + } + }} + onMouseLeave={() => { + if (!disabled && onDateMouseLeave) { + onDateMouseLeave(currentDate); } - onSelect(currentDate); }} className={classNames(datePrefixCls, { [`${datePrefixCls}-disabled`]: disabled, @@ -123,6 +134,22 @@ function DateBody({ rangedValue && rangedValue[1], currentDate, ), + [`${datePrefixCls}-range-hover`]: isInRange( + generateConfig, + hoverRangedValue && hoverRangedValue[0], + hoverRangedValue && hoverRangedValue[1], + currentDate, + ), + [`${datePrefixCls}-range-hover-start`]: isSameDate( + generateConfig, + hoverRangedValue && hoverRangedValue[0], + currentDate, + ), + [`${datePrefixCls}-range-hover-end`]: isSameDate( + generateConfig, + hoverRangedValue && hoverRangedValue[1], + currentDate, + ), [`${datePrefixCls}-today`]: isSameDate( generateConfig, today, diff --git a/src/panels/MonthPanel/MonthBody.tsx b/src/panels/MonthPanel/MonthBody.tsx index c6b8eacd9..bc78c63db 100644 --- a/src/panels/MonthPanel/MonthBody.tsx +++ b/src/panels/MonthPanel/MonthBody.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { GenerateConfig } from '../../generate'; import { Locale } from '../../interface'; import { isSameMonth } from '../../utils/dateUtil'; +import PanelContext from '../../PanelContext'; export const MONTH_COL_COUNT = 3; const MONTH_ROW_COUNT = 4; @@ -33,6 +34,8 @@ function MonthBody({ monthCellRender, onSelect, }: MonthBodyProps) { + const { onDateMouseEnter, onDateMouseLeave } = React.useContext(PanelContext); + const monthPrefixCls = `${prefixCls}-cell`; const rows: React.ReactNode[] = []; @@ -70,10 +73,19 @@ function MonthBody({ ), })} onClick={() => { - if (disabled) { - return; + if (!disabled) { + onSelect(monthDate); + } + }} + onMouseEnter={() => { + if (!disabled && onDateMouseEnter) { + onDateMouseEnter(monthDate); + } + }} + onMouseLeave={() => { + if (!disabled && onDateMouseLeave) { + onDateMouseLeave(monthDate); } - onSelect(monthDate); }} > {monthCellRender ? ( diff --git a/src/panels/YearPanel/YearBody.tsx b/src/panels/YearPanel/YearBody.tsx index cbf618e5e..b1ff05adf 100644 --- a/src/panels/YearPanel/YearBody.tsx +++ b/src/panels/YearPanel/YearBody.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { GenerateConfig } from '../../generate'; import { YEAR_DECADE_COUNT } from '.'; import { Locale, NullableDateType } from '../../interface'; +import PanelContext from '../../PanelContext'; export const YEAR_COL_COUNT = 3; const YEAR_ROW_COUNT = 4; @@ -26,6 +27,8 @@ function YearBody({ disabledDate, onSelect, }: YearBodyProps) { + const { onDateMouseEnter, onDateMouseLeave } = React.useContext(PanelContext); + const yearPrefixCls = `${prefixCls}-cell`; const rows: React.ReactNode[] = []; @@ -61,10 +64,19 @@ function YearBody({ currentYearNumber === valueYearNumber, })} onClick={() => { - if (disabled) { - return; + if (!disabled) { + onSelect(yearDate); + } + }} + onMouseEnter={() => { + if (!disabled && onDateMouseEnter) { + onDateMouseEnter(yearDate); + } + }} + onMouseLeave={() => { + if (!disabled && onDateMouseLeave) { + onDateMouseLeave(yearDate); } - onSelect(yearDate); }} >
{currentYearNumber}
From dcabc7b4dcd876153cbf27e655cfe1d8129c52af Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 3 Dec 2019 17:48:26 +0800 Subject: [PATCH 25/25] add active className of input --- src/RangePicker.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 9110d164e..8a98365de 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -773,7 +773,12 @@ function InnerRangePicker(props: RangePickerProps) { style={style} {...getDataOrAriaProps(props)} > -
+
(props: RangePickerProps) { />
{separator} -
+