Skip to content

Commit e7afcbe

Browse files
authored
fix: disabledHours & feat: now work with step (#88)
* fix: disabledHours works unexpectlly when using 12 hours & hour should starts from 12 when using 12 hours * chore * chore: typo fix * feat: now button now works with step * test: now with step * chore: fix typo * refactor: setTime, use useMemo * test: dayjs & moment util test * chore * refactor: set time * test: setTime * feat: dev warning on step * fix: dev warning * fix: dev warning * refactor: get lower bound time rather than closest time * refactor: use rc-utils memo * refactor: use shallowEqual * refactor * refactor: code styling * test: warning on time step * chore * chore * chore: revert format * fix: compare length in time units comparition * chore: revert dayjs util code * chore: revert some useMemo to React.useMemo * chore: revert code formatting * perf: remove useless destructuring assignment * perf: flatten parameters
1 parent ff21746 commit e7afcbe

File tree

7 files changed

+297
-42
lines changed

7 files changed

+297
-42
lines changed

src/PickerPanel.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { MonthCellRender } from './panels/MonthPanel/MonthBody';
3838
import RangeContext from './RangeContext';
3939
import getExtraFooter from './utils/getExtraFooter';
4040
import getRanges from './utils/getRanges';
41+
import { getLowerBoundTime, setTime } from './utils/timeUtil';
4142

4243
export interface PickerPanelSharedProps<DateType> {
4344
prefixCls?: string;
@@ -146,13 +147,29 @@ function PickerPanel<DateType>(props: PickerPanelProps<DateType>) {
146147
onOk,
147148
components,
148149
direction,
150+
hourStep = 1,
151+
minuteStep = 1,
152+
secondStep = 1,
149153
} = props as MergedPickerPanelProps<DateType>;
150154

151155
const needConfirmButton: boolean = (picker === 'date' && !!showTime) || picker === 'time';
152156

157+
const isHourStepValid = 24 % hourStep === 0;
158+
const isMinuteStepValid = 60 % minuteStep === 0;
159+
const isSecondStepValid = 60 % secondStep === 0;
160+
153161
if (process.env.NODE_ENV !== 'production') {
154162
warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `value`.');
155163
warning(!value || generateConfig.isValidate(value), 'Invalidate date pass to `defaultValue`.');
164+
warning(isHourStepValid, `\`hourStep\` ${hourStep} is invalid. It should be a factor of 24.`);
165+
warning(
166+
isMinuteStepValid,
167+
`\`minuteStep\` ${minuteStep} is invalid. It should be a factor of 60.`,
168+
);
169+
warning(
170+
isSecondStepValid,
171+
`\`secondStep\` ${secondStep} is invalid. It should be a factor of 60.`,
172+
);
156173
}
157174

158175
// ============================ State =============================
@@ -434,6 +451,26 @@ function PickerPanel<DateType>(props: PickerPanelProps<DateType>) {
434451
let extraFooter: React.ReactNode;
435452
let rangesNode: React.ReactNode;
436453

454+
const onNow = () => {
455+
const now = generateConfig.getNow();
456+
const lowerBoundTime = getLowerBoundTime(
457+
generateConfig.getHour(now),
458+
generateConfig.getMinute(now),
459+
generateConfig.getSecond(now),
460+
isHourStepValid ? hourStep : 1,
461+
isMinuteStepValid ? minuteStep : 1,
462+
isSecondStepValid ? secondStep : 1,
463+
);
464+
const adjustedNow = setTime(
465+
generateConfig,
466+
now,
467+
lowerBoundTime[0], // hour
468+
lowerBoundTime[1], // minute
469+
lowerBoundTime[2], // second
470+
);
471+
triggerSelect(adjustedNow, 'submit');
472+
};
473+
437474
if (!hideRanges) {
438475
extraFooter = getExtraFooter(prefixCls, mergedMode, renderExtraFooter);
439476
rangesNode = getRanges({
@@ -443,11 +480,7 @@ function PickerPanel<DateType>(props: PickerPanelProps<DateType>) {
443480
okDisabled: !mergedValue || (disabledDate && disabledDate(mergedValue)),
444481
locale,
445482
showNow,
446-
onNow:
447-
needConfirmButton &&
448-
(() => {
449-
triggerSelect(generateConfig.getNow(), 'submit');
450-
}),
483+
onNow: needConfirmButton && onNow,
451484
onOk: () => {
452485
if (mergedValue) {
453486
triggerSelect(mergedValue, 'submit', true);

src/panels/TimePanel/TimeBody.tsx

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import * as React from 'react';
2+
import useMemo from 'rc-util/lib/hooks/useMemo';
23
import { GenerateConfig } from '../../generate';
34
import { Locale, OnSelect } from '../../interface';
45
import TimeUnitColumn, { Unit } from './TimeUnitColumn';
56
import { leftPad } from '../../utils/miscUtil';
67
import { SharedTimeProps } from '.';
8+
import { setTime as utilSetTime } from '../../utils/timeUtil';
9+
10+
function shouldUnitsUpdate(prevUnits: Unit[], nextUnits: Unit[]) {
11+
if (prevUnits.length !== nextUnits.length) return true;
12+
// if any unit's disabled status is different, the units should be re-evaluted
13+
for (let i = 0; i < prevUnits.length; i += 1) {
14+
if (prevUnits[i].disabled !== nextUnits[i].disabled) return true;
15+
}
16+
return false;
17+
}
718

819
function generateUnits(
920
start: number,
@@ -83,37 +94,60 @@ function TimeBody<DateType>(props: TimeBodyProps<DateType>) {
8394
const mergedMinute = Math.max(0, newMinute);
8495
const mergedSecond = Math.max(0, newSecond);
8596

86-
newDate = generateConfig.setSecond(newDate, mergedSecond);
87-
newDate = generateConfig.setMinute(newDate, mergedMinute);
88-
newDate = generateConfig.setHour(
97+
newDate = utilSetTime(
98+
generateConfig,
8999
newDate,
90100
!use12Hours || !isNewPM ? mergedHour : mergedHour + 12,
101+
mergedMinute,
102+
mergedSecond,
91103
);
92104

93105
return newDate;
94106
};
95107

96108
// ========================= Unit =========================
97-
const hours = generateUnits(
98-
0,
99-
use12Hours ? 11 : 23,
100-
hourStep,
101-
disabledHours && disabledHours(),
102-
);
109+
const rawHours = generateUnits(0, 23, hourStep, disabledHours && disabledHours());
110+
111+
const memorizedRawHours = useMemo(() => rawHours, rawHours, shouldUnitsUpdate);
103112

104113
// Should additional logic to handle 12 hours
105-
if (use12Hours && hour !== -1) {
106-
isPM = hour >= 12;
114+
if (use12Hours) {
115+
isPM = hour >= 12; // -1 means should display AM
107116
hour %= 12;
108-
hours[0].label = '12';
109117
}
110118

111-
const minutes = generateUnits(
112-
0,
113-
59,
114-
minuteStep,
115-
disabledMinutes && disabledMinutes(hour),
116-
);
119+
const [AMDisabled, PMDisabled] = React.useMemo(() => {
120+
if (!use12Hours) {
121+
return [false, false];
122+
}
123+
const AMPMDisabled = [true, true];
124+
memorizedRawHours.forEach(({ disabled, value: hourValue }) => {
125+
if (disabled) return;
126+
if (hourValue >= 12) {
127+
AMPMDisabled[1] = false;
128+
} else {
129+
AMPMDisabled[0] = false;
130+
}
131+
});
132+
return AMPMDisabled;
133+
}, [use12Hours, memorizedRawHours]);
134+
135+
const hours = React.useMemo(() => {
136+
if (!use12Hours) return memorizedRawHours;
137+
return memorizedRawHours
138+
.filter(isPM ? hourMeta => hourMeta.value >= 12 : hourMeta => hourMeta.value < 12)
139+
.map(hourMeta => {
140+
const hourValue = hourMeta.value % 12;
141+
const hourLabel = hourValue === 0 ? '12' : leftPad(hourValue, 2);
142+
return {
143+
...hourMeta,
144+
label: hourLabel,
145+
value: hourValue,
146+
};
147+
});
148+
}, [use12Hours, memorizedRawHours]);
149+
150+
const minutes = generateUnits(0, 59, minuteStep, disabledMinutes && disabledMinutes(hour));
117151

118152
const seconds = generateUnits(
119153
0,
@@ -207,7 +241,10 @@ function TimeBody<DateType>(props: TimeBodyProps<DateType>) {
207241
use12Hours === true,
208242
<TimeUnitColumn key="12hours" />,
209243
PMIndex,
210-
[{ label: 'AM', value: 0 }, { label: 'PM', value: 1 }],
244+
[
245+
{ label: 'AM', value: 0, disabled: AMDisabled },
246+
{ label: 'PM', value: 1, disabled: PMDisabled },
247+
],
211248
num => {
212249
onSelect(setTime(!!num, hour, minute, second), 'mouse');
213250
},

src/panels/TimePanel/TimeUnitColumn.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import PanelContext from '../../PanelContext';
66
export interface Unit {
77
label: React.ReactText;
88
value: number;
9-
disabled?: boolean;
9+
disabled: boolean;
1010
}
1111

1212
export interface TimeUnitColumnProps {

src/utils/timeUtil.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { GenerateConfig } from '../generate';
2+
3+
export function setTime<DateType>(
4+
generateConfig: GenerateConfig<DateType>,
5+
date: DateType,
6+
hour: number,
7+
minute: number,
8+
second: number,
9+
): DateType {
10+
let nextTime = generateConfig.setHour(date, hour);
11+
nextTime = generateConfig.setMinute(nextTime, minute);
12+
nextTime = generateConfig.setSecond(nextTime, second);
13+
return nextTime;
14+
}
15+
16+
export function getLowerBoundTime(
17+
hour: number,
18+
minute: number,
19+
second: number,
20+
hourStep: number,
21+
minuteStep: number,
22+
secondStep: number,
23+
): [number, number, number] {
24+
const lowerBoundHour = Math.floor(hour / hourStep) * hourStep;
25+
if (lowerBoundHour < hour) {
26+
return [lowerBoundHour, 60 - minuteStep, 60 - secondStep];
27+
}
28+
const lowerBoundMinute = Math.floor(minute / minuteStep) * minuteStep;
29+
if (lowerBoundMinute < minute) {
30+
return [lowerBoundHour, lowerBoundMinute, 60 - secondStep];
31+
}
32+
const lowerBoundSecond = Math.floor(second / secondStep) * secondStep;
33+
return [lowerBoundHour, lowerBoundMinute, lowerBoundSecond];
34+
}

tests/panel.spec.tsx

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -344,24 +344,100 @@ describe('Picker.Panel', () => {
344344
});
345345
});
346346

347-
it('time with use12Hours', () => {
348-
const onChange = jest.fn();
349-
const wrapper = mount(
350-
<MomentPickerPanel
351-
picker="time"
352-
defaultValue={getMoment('2000-01-01 00:01:02')}
353-
use12Hours
354-
onChange={onChange}
355-
/>,
356-
);
347+
describe('time with use12Hours', () => {
348+
it('should work', () => {
349+
const onChange = jest.fn();
350+
const wrapper = mount(
351+
<MomentPickerPanel
352+
picker="time"
353+
defaultValue={getMoment('2000-01-01 00:01:02')}
354+
use12Hours
355+
onChange={onChange}
356+
/>,
357+
);
357358

358-
wrapper
359-
.find('.rc-picker-time-panel-column')
360-
.last()
361-
.find('li')
362-
.last()
363-
.simulate('click');
364-
expect(isSame(onChange.mock.calls[0][0], '2000-01-01 12:01:02', 'second')).toBeTruthy();
359+
wrapper
360+
.find('.rc-picker-time-panel-column')
361+
.last()
362+
.find('li')
363+
.last()
364+
.simulate('click');
365+
expect(isSame(onChange.mock.calls[0][0], '2000-01-01 12:01:02', 'second')).toBeTruthy();
366+
});
367+
368+
it('should display hour from 12 at AM', () => {
369+
const wrapper = mount(
370+
<MomentPickerPanel
371+
picker="time"
372+
defaultValue={getMoment('2000-01-01 00:00:00')}
373+
use12Hours
374+
/>,
375+
);
376+
377+
const startHour = wrapper
378+
.find('.rc-picker-time-panel-column')
379+
.first()
380+
.find('li')
381+
.first()
382+
.text();
383+
expect(startHour).toEqual('12');
384+
});
385+
386+
it('should display hour from 12 at AM', () => {
387+
const wrapper = mount(
388+
<MomentPickerPanel
389+
picker="time"
390+
defaultValue={getMoment('2000-01-01 12:00:00')}
391+
use12Hours
392+
/>,
393+
);
394+
395+
const startHour = wrapper
396+
.find('.rc-picker-time-panel-column')
397+
.first()
398+
.find('li')
399+
.first()
400+
.text();
401+
expect(startHour).toEqual('12');
402+
});
403+
404+
it('should disable AM when 00 ~ 11 is disabled', () => {
405+
const wrapper = mount(
406+
<MomentPickerPanel
407+
picker="time"
408+
defaultValue={getMoment('2000-01-01 12:00:00')}
409+
use12Hours
410+
disabledHours={() => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}
411+
/>,
412+
);
413+
414+
const disabledAMItem = wrapper
415+
.find('.rc-picker-time-panel-column')
416+
.last()
417+
.find('li')
418+
.first()
419+
.find('.rc-picker-time-panel-cell-disabled');
420+
expect(disabledAMItem.length).toEqual(1);
421+
});
422+
423+
it('should disable PM when 12 ~ 23 is disabled', () => {
424+
const wrapper = mount(
425+
<MomentPickerPanel
426+
picker="time"
427+
defaultValue={getMoment('2000-01-01 12:00:00')}
428+
use12Hours
429+
disabledHours={() => [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]}
430+
/>,
431+
);
432+
433+
const disabledPMItem = wrapper
434+
.find('.rc-picker-time-panel-column')
435+
.last()
436+
.find('li')
437+
.last()
438+
.find('.rc-picker-time-panel-cell-disabled');
439+
expect(disabledPMItem.length).toEqual(1);
440+
});
365441
});
366442

367443
it('time disabled columns', () => {

tests/picker.spec.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,46 @@ describe('Picker.Basic', () => {
543543
});
544544
});
545545

546+
describe('time step', () => {
547+
it('work with now', () => {
548+
MockDate.set(getMoment('1990-09-03 00:09:00').toDate());
549+
const onSelect = jest.fn();
550+
const wrapper = mount(<MomentPicker onSelect={onSelect} picker="time" minuteStep={10} />);
551+
wrapper.openPicker();
552+
wrapper.find('.rc-picker-now > a').simulate('click');
553+
expect(isSame(onSelect.mock.calls[0][0], '1990-09-03 00:00:59', 'second')).toBeTruthy();
554+
MockDate.set(getMoment('1990-09-03 00:00:00').toDate());
555+
});
556+
it('should show warning when hour step is invalid', () => {
557+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
558+
expect(spy).not.toBeCalled();
559+
const wrapper = mount(<MomentPicker picker="time" hourStep={9} />);
560+
wrapper.openPicker();
561+
expect(spy).toBeCalledWith('Warning: `hourStep` 9 is invalid. It should be a factor of 24.');
562+
spy.mockRestore();
563+
});
564+
it('should show warning when minute step is invalid', () => {
565+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
566+
expect(spy).not.toBeCalled();
567+
const wrapper = mount(<MomentPicker picker="time" minuteStep={9} />);
568+
wrapper.openPicker();
569+
expect(spy).toBeCalledWith(
570+
'Warning: `minuteStep` 9 is invalid. It should be a factor of 60.',
571+
);
572+
spy.mockRestore();
573+
});
574+
it('should show warning when second step is invalid', () => {
575+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
576+
expect(spy).not.toBeCalled();
577+
const wrapper = mount(<MomentPicker picker="time" secondStep={9} />);
578+
wrapper.openPicker();
579+
expect(spy).toBeCalledWith(
580+
'Warning: `secondStep` 9 is invalid. It should be a factor of 60.',
581+
);
582+
spy.mockRestore();
583+
});
584+
});
585+
546586
it('pass data- & aria- & role', () => {
547587
const wrapper = mount(<MomentPicker data-test="233" aria-label="3334" role="search" />);
548588

0 commit comments

Comments
 (0)