Skip to content

Commit fd4b290

Browse files
authored
feat: improve hovering experience (#106)
* feat: improve hovering experience * chore: remove raf of useHoverPlaceholder * test: add case for hover placeholder * fix: dayjs type error * chore: optimize logic * chore(day.js): fix parseLocale type
1 parent ead450f commit fd4b290

File tree

7 files changed

+205
-32
lines changed

7 files changed

+205
-32
lines changed

src/Picker.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { getDefaultFormat, getInputSize, elementsContains } from './utils/uiUtil
3030
import usePickerInput from './hooks/usePickerInput';
3131
import useTextValueMapping from './hooks/useTextValueMapping';
3232
import useValueTexts from './hooks/useValueTexts';
33+
import useHoverPlaceholder from './hooks/useHoverPlaceholder';
3334

3435
export interface PickerRefConfig {
3536
focus: () => void;
@@ -435,6 +436,12 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
435436
};
436437
const popupPlacement = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
437438

439+
const [hoverPlaceholder, onEnter, onLeave] = useHoverPlaceholder(placeholder, text, {
440+
formatList,
441+
generateConfig,
442+
locale,
443+
});
444+
438445
return (
439446
<PanelContext.Provider
440447
value={{
@@ -444,6 +451,8 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
444451
onSelect: onContextSelect,
445452
open: mergedOpen,
446453
defaultOpenValue,
454+
onDateMouseEnter: onEnter,
455+
onDateMouseLeave: onLeave,
447456
}}
448457
>
449458
<PickerTrigger
@@ -483,7 +492,7 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
483492
triggerTextChange(e.target.value);
484493
}}
485494
autoFocus={autoFocus}
486-
placeholder={placeholder}
495+
placeholder={hoverPlaceholder}
487496
ref={inputRef}
488497
title={text}
489498
{...inputProps}

src/RangePicker.tsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import getExtraFooter from './utils/getExtraFooter';
2929
import getRanges from './utils/getRanges';
3030
import useRangeViewDates from './hooks/useRangeViewDates';
3131
import { DateRender } from './panels/DatePanel/DateBody';
32+
import useHoverPlaceholder from './hooks/useHoverPlaceholder';
3233

3334
function reorderValues<DateType>(
3435
values: RangeValue<DateType>,
@@ -284,18 +285,6 @@ function InnerRangePicker<DateType>(props: RangePickerProps<DateType>) {
284285
},
285286
});
286287

287-
const [rangeHoverValue, setRangeHoverValue] = useState<RangeValue<DateType>>(null);
288-
289-
// ========================== Hover Range ==========================
290-
const [hoverRangedValue, setHoverRangedValue] = useState<RangeValue<DateType>>(null);
291-
292-
const onDateMouseEnter = (date: DateType) => {
293-
setHoverRangedValue(updateValues(selectedValue, date, mergedActivePickerIndex));
294-
};
295-
const onDateMouseLeave = () => {
296-
setHoverRangedValue(updateValues(selectedValue, null, mergedActivePickerIndex));
297-
};
298-
299288
// ============================= Modes =============================
300289
const [mergedModes, setInnerModes] = useMergedState<[PanelMode, PanelMode]>([picker, picker], {
301290
value: mode,
@@ -546,6 +535,49 @@ function InnerRangePicker<DateType>(props: RangePickerProps<DateType>) {
546535
onTextChange: newText => onTextChange(newText, 1),
547536
});
548537

538+
const [rangeHoverValue, setRangeHoverValue] = useState<RangeValue<DateType>>(null);
539+
540+
// ========================== Hover Range ==========================
541+
const [hoverRangedValue, setHoverRangedValue] = useState<RangeValue<DateType>>(null);
542+
543+
const [startPlaceholder, onStartEnter, onStartLeave] = useHoverPlaceholder(
544+
getValue(placeholder, 0) || '',
545+
startText,
546+
{
547+
formatList,
548+
generateConfig,
549+
locale,
550+
},
551+
);
552+
553+
const [endPlaceholder, onEndEnter, onEndLeave] = useHoverPlaceholder(
554+
getValue(placeholder, 1) || '',
555+
endText,
556+
{
557+
formatList,
558+
generateConfig,
559+
locale,
560+
},
561+
);
562+
563+
const onDateMouseEnter = (date: DateType) => {
564+
setHoverRangedValue(updateValues(selectedValue, date, mergedActivePickerIndex));
565+
if (mergedActivePickerIndex === 0) {
566+
onStartEnter(date);
567+
} else {
568+
onEndEnter(date);
569+
}
570+
};
571+
572+
const onDateMouseLeave = () => {
573+
setHoverRangedValue(updateValues(selectedValue, null, mergedActivePickerIndex));
574+
if (mergedActivePickerIndex === 0) {
575+
onStartLeave(null);
576+
} else {
577+
onEndLeave(null);
578+
}
579+
};
580+
549581
// ============================= Input =============================
550582
const getSharedInputHookProps = (index: 0 | 1, resetText: () => void) => ({
551583
blurToCancel: needConfirmButton,
@@ -1040,7 +1072,7 @@ function InnerRangePicker<DateType>(props: RangePickerProps<DateType>) {
10401072
triggerStartTextChange(e.target.value);
10411073
}}
10421074
autoFocus={autoFocus}
1043-
placeholder={getValue(placeholder, 0) || ''}
1075+
placeholder={startPlaceholder}
10441076
ref={startInputRef}
10451077
{...startInputProps}
10461078
{...inputSharedProps}
@@ -1063,7 +1095,7 @@ function InnerRangePicker<DateType>(props: RangePickerProps<DateType>) {
10631095
onChange={e => {
10641096
triggerEndTextChange(e.target.value);
10651097
}}
1066-
placeholder={getValue(placeholder, 1) || ''}
1098+
placeholder={endPlaceholder}
10671099
ref={endInputRef}
10681100
{...endInputProps}
10691101
{...inputSharedProps}

src/generate/dayjs.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,13 @@ const localeMap: IlocaleMapObject = {
3535

3636
const parseLocale = (locale: string) => {
3737
const mapLocale = localeMap[locale];
38-
return mapLocale || locale.split('_')[0];
38+
return (mapLocale || locale.split('_')[0]) as LocalePresetType;
3939
};
4040

4141
const parseNoMatchNotice = () => {
4242
/* istanbul ignore next */
43-
noteOnce(
44-
false,
45-
'Not match any format. Please help to fire a issue about this.',
46-
);
47-
}
43+
noteOnce(false, 'Not match any format. Please help to fire a issue about this.');
44+
};
4845

4946
const generateConfig: GenerateConfig<Dayjs> = {
5047
// get
@@ -92,24 +89,26 @@ const generateConfig: GenerateConfig<Dayjs> = {
9289
.locale(parseLocale(locale))
9390
.localeData()
9491
.monthsShort(),
95-
format: (locale, date, format) =>
96-
date.locale(parseLocale(locale)).format(format),
92+
format: (locale, date, format) => date.locale(parseLocale(locale)).format(format),
9793
parse: (locale, text, formats) => {
98-
const localeStr = parseLocale(locale)
94+
const localeStr = parseLocale(locale);
9995
for (let i = 0; i < formats.length; i += 1) {
10096
const format = formats[i];
10197
const formatText = text;
102-
if (format.includes('wo') || format.includes('Wo')) { // parse Wo
103-
const year = formatText.split('-')[0]
104-
const weekStr = formatText.split('-')[1]
105-
const firstWeek = dayjs(year, 'YYYY').startOf('year').locale(localeStr)
98+
if (format.includes('wo') || format.includes('Wo')) {
99+
// parse Wo
100+
const year = formatText.split('-')[0];
101+
const weekStr = formatText.split('-')[1];
102+
const firstWeek = dayjs(year, 'YYYY')
103+
.startOf('year')
104+
.locale(localeStr);
106105
for (let j = 0; j <= 52; j += 1) {
107-
const nextWeek = firstWeek.add(j, 'week')
106+
const nextWeek = firstWeek.add(j, 'week');
108107
if (nextWeek.format('Wo') === weekStr) {
109-
return nextWeek
108+
return nextWeek;
110109
}
111110
}
112-
parseNoMatchNotice()
111+
parseNoMatchNotice();
113112
return null;
114113
}
115114
const date = dayjs(formatText, format).locale(localeStr);

src/hooks/useHoverPlaceholder.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useState } from 'react';
2+
import useValueTexts, { ValueTextConfig } from './useValueTexts';
3+
4+
export default function useHoverPlaceholder<DateType>(
5+
placeholder: string,
6+
text: string,
7+
{ formatList, generateConfig, locale }: ValueTextConfig<DateType>,
8+
): [string, (date: DateType) => void, (date: DateType) => void] {
9+
const [value, setValue] = useState(null);
10+
11+
const [valueTexts] = useValueTexts(value, {
12+
formatList,
13+
generateConfig,
14+
locale,
15+
});
16+
17+
function onEnter(date: DateType) {
18+
if (!text) {
19+
setValue(date);
20+
}
21+
}
22+
23+
function onLeave() {
24+
setValue(null);
25+
}
26+
27+
return [(valueTexts && valueTexts[0]) || placeholder, onEnter, onLeave];
28+
}

src/hooks/useValueTexts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import useMemo from 'rc-util/lib/hooks/useMemo';
33
import { GenerateConfig } from '../generate';
44
import { Locale } from '../interface';
55

6-
interface ValueTextConfig<DateType> {
6+
export interface ValueTextConfig<DateType> {
77
formatList: string[];
88
generateConfig: GenerateConfig<DateType>;
99
locale: Locale;

tests/picker.spec.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,4 +729,34 @@ describe('Picker.Basic', () => {
729729
const wrapper = mount(<MomentPicker open panelRender={() => <h1>Light</h1>} />);
730730
expect(wrapper.render()).toMatchSnapshot();
731731
});
732+
733+
describe('hover placeholder', () => {
734+
const placeholder = 'custom placeholder';
735+
it('placeholder should be origin value when input value is not empty', () => {
736+
const wrapper = mount(
737+
<MomentPicker placeholder={placeholder} open defaultValue={getMoment('2020-07-22')} />,
738+
);
739+
const cell = wrapper.findCell(24);
740+
cell.simulate('mouseEnter');
741+
expect(wrapper.find('input').prop('placeholder')).toBe(placeholder);
742+
});
743+
744+
it('placeholder should be target date when input value is empty', () => {
745+
const wrapper = mount(
746+
<MomentPicker placeholder={placeholder} defaultValue={getMoment('2020-07-22')} />,
747+
);
748+
wrapper.openPicker();
749+
wrapper.find('input').simulate('change', {
750+
target: {
751+
value: '',
752+
},
753+
});
754+
const cell = wrapper.findCell(24);
755+
cell.simulate('mouseEnter');
756+
expect(wrapper.find('input').prop('placeholder')).toBe('2020-07-24');
757+
cell.simulate('mouseLeave');
758+
expect(wrapper.find('input').prop('placeholder')).toBe(placeholder);
759+
wrapper.closePicker();
760+
});
761+
});
732762
});

tests/range.spec.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,4 +1426,79 @@ describe('Picker.Range', () => {
14261426
);
14271427
});
14281428
});
1429+
1430+
describe('hover placeholder', () => {
1431+
const placeholder: [string, string] = ['custom placeholder1', 'custom placeholder2'];
1432+
const defaultValue: [Moment, Moment] = [getMoment('2020-07-22'), getMoment('2020-08-22')];
1433+
1434+
it('placeholder should be target date when input value is empty', () => {
1435+
const wrapper = mount(
1436+
<MomentRangePicker placeholder={placeholder} defaultValue={defaultValue} />,
1437+
);
1438+
wrapper.openPicker(0);
1439+
wrapper.inputValue('');
1440+
// left
1441+
const leftCell = wrapper.findCell('24');
1442+
leftCell.simulate('mouseEnter');
1443+
expect(
1444+
wrapper
1445+
.find('input')
1446+
.first()
1447+
.prop('placeholder'),
1448+
).toBe('2020-07-24');
1449+
expect(
1450+
wrapper
1451+
.find('input')
1452+
.last()
1453+
.prop('placeholder'),
1454+
).toBe(placeholder[1]);
1455+
leftCell.simulate('mouseLeave');
1456+
expect(
1457+
wrapper
1458+
.find('input')
1459+
.first()
1460+
.prop('placeholder'),
1461+
).toBe(placeholder[0]);
1462+
expect(
1463+
wrapper
1464+
.find('input')
1465+
.last()
1466+
.prop('placeholder'),
1467+
).toBe(placeholder[1]);
1468+
wrapper.closePicker(0);
1469+
1470+
// right
1471+
wrapper.openPicker(1);
1472+
wrapper.inputValue('', 1);
1473+
1474+
const rightCell = wrapper.findCell('24', 1);
1475+
rightCell.simulate('mouseEnter');
1476+
expect(
1477+
wrapper
1478+
.find('input')
1479+
.last()
1480+
.prop('placeholder'),
1481+
).toBe('2020-08-24');
1482+
expect(
1483+
wrapper
1484+
.find('input')
1485+
.first()
1486+
.prop('placeholder'),
1487+
).toBe(placeholder[0]);
1488+
rightCell.simulate('mouseLeave');
1489+
expect(
1490+
wrapper
1491+
.find('input')
1492+
.first()
1493+
.prop('placeholder'),
1494+
).toBe(placeholder[0]);
1495+
expect(
1496+
wrapper
1497+
.find('input')
1498+
.last()
1499+
.prop('placeholder'),
1500+
).toBe(placeholder[1]);
1501+
wrapper.closePicker(1);
1502+
});
1503+
});
14291504
});

0 commit comments

Comments
 (0)