@itwin/itwinui-react
Version:
A react component library for iTwinUI
461 lines (460 loc) • 13.7 kB
JavaScript
import cx from 'classnames';
import * as React from 'react';
import { Box } from '../../utils/index.js';
let isSameHour = (date1, date2, meridiem) => {
let adjustedHours = meridiem
? formatHourFrom12(date1.getHours(), meridiem)
: date1.getHours();
if (!!meridiem)
return !!date2 && adjustedHours % 12 === date2.getHours() % 12;
return !!date2 && adjustedHours === date2.getHours();
};
let isSameMinute = (date1, date2) =>
!!date2 && date1.getMinutes() === date2.getMinutes();
let isSameSecond = (date1, date2) =>
!!date2 && date1.getSeconds() === date2.getSeconds();
let isSameTime = (date1, date2, precision, meridiem) => {
let isSameTime = true;
switch (precision) {
case 'seconds':
isSameTime = isSameSecond(date1, date2);
if (!isSameTime) break;
case 'minutes':
isSameTime = isSameMinute(date1, date2);
if (!isSameTime) break;
case 'hours':
isSameTime = isSameHour(date1, date2, meridiem);
}
return isSameTime;
};
let isSameMeridiem = (meridiem, date) =>
!!date && ('AM' === meridiem ? date.getHours() < 12 : date.getHours() >= 12);
let formatHourFrom12 = (hour, meridiem) => {
let adjustedHour = hour % 12;
return 'PM' === meridiem ? adjustedHour + 12 : adjustedHour;
};
let setHours = (hour, date) =>
new Date(
date.getFullYear(),
date.getMonth(),
date.getDate(),
hour,
date.getMinutes(),
date.getSeconds(),
);
let defaultCombinedRenderer = (date, precision) => {
let dateString = '';
switch (precision) {
case 'seconds':
dateString =
':' +
date.getSeconds().toLocaleString(void 0, {
minimumIntegerDigits: 2,
});
case 'minutes':
dateString =
':' +
date.getMinutes().toLocaleString(void 0, {
minimumIntegerDigits: 2,
}) +
dateString;
case 'hours':
dateString =
date.getHours().toLocaleString(void 0, {
minimumIntegerDigits: 2,
}) + dateString;
}
return dateString;
};
export const TimePicker = React.forwardRef((props, forwardedRef) => {
let {
date,
onChange,
use12Hours = false,
precision = 'minutes',
hourStep = 1,
minuteStep = 1,
secondStep = 1,
setFocusHour = false,
hourRenderer = (date) =>
date.getHours().toLocaleString(void 0, {
minimumIntegerDigits: 2,
}),
minuteRenderer = (date) =>
date.getMinutes().toLocaleString(void 0, {
minimumIntegerDigits: 2,
}),
secondRenderer = (date) =>
date.getSeconds().toLocaleString(void 0, {
minimumIntegerDigits: 2,
}),
meridiemRenderer = (meridiem) => meridiem,
useCombinedRenderer = false,
combinedRenderer = defaultCombinedRenderer,
className,
...rest
} = props;
let [selectedTime, setSelectedTime] = React.useState(date);
let [focusedTime, setFocusedTime] = React.useState(
selectedTime ?? new Date(),
);
let [meridiem, setMeridiem] = React.useState(
use12Hours ? (focusedTime?.getHours() > 11 ? 'PM' : 'AM') : void 0,
);
React.useEffect(() => {
setFocusedTime(date ?? new Date());
setSelectedTime(date);
}, [date]);
let onHourClick = (date) => {
let adjustedHour = use12Hours
? formatHourFrom12(date.getHours(), meridiem)
: date.getHours();
let adjustedSelectedTime = setHours(
adjustedHour,
selectedTime ?? new Date(),
);
updateCurrentTime(adjustedSelectedTime);
};
let onTimeClick = (date) => {
let adjustedHour = use12Hours
? formatHourFrom12(date.getHours(), meridiem)
: date.getHours();
let adjustedSelectedTime = setHours(adjustedHour, date);
updateCurrentTime(adjustedSelectedTime);
};
let onMeridiemClick = (value) => {
let adjustedSelectedTime = selectedTime ?? new Date();
let currentHours = adjustedSelectedTime.getHours();
setMeridiem(value);
if ('AM' === value && currentHours > 11)
adjustedSelectedTime = setHours(currentHours - 12, adjustedSelectedTime);
if ('PM' === value && currentHours <= 12)
adjustedSelectedTime = setHours(currentHours + 12, adjustedSelectedTime);
updateCurrentTime(adjustedSelectedTime);
};
let updateCurrentTime = (time) => {
let adjustedTime = time;
if ('hours' === precision)
adjustedTime = new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
time.getHours(),
0,
0,
);
if ('minutes' === precision)
adjustedTime = new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
time.getHours(),
time.getMinutes(),
0,
);
setFocusedTime(adjustedTime);
setSelectedTime(adjustedTime);
onChange?.(adjustedTime);
};
let onHourFocus = (date) => {
let adjustedHour = use12Hours
? formatHourFrom12(date.getHours(), meridiem)
: date.getHours();
setFocusedTime(setHours(adjustedHour, focusedTime));
};
let onTimeFocus = (date) => {
let adjustedHour = use12Hours
? formatHourFrom12(date.getHours(), meridiem)
: date.getHours();
setFocusedTime(setHours(adjustedHour, date));
};
let onMeridiemFocus = (value) => {
let adjustedSelectedTime = selectedTime ?? new Date();
let currentHours = adjustedSelectedTime.getHours();
if ('AM' === value && currentHours > 11) {
setMeridiem(value);
adjustedSelectedTime = setHours(currentHours - 12, adjustedSelectedTime);
}
if ('PM' === value && currentHours <= 12) {
setMeridiem(value);
adjustedSelectedTime = setHours(currentHours + 12, adjustedSelectedTime);
}
setFocusedTime(adjustedSelectedTime);
};
let generateDataList = (size, value, step) => {
let data = [];
for (let i = 0; i < size; i++) if (i % step === 0) data.push(value(i));
return data;
};
let time = React.useMemo(() => {
let time = selectedTime ?? new Date();
let data = [];
let hoursArray = Array.from(Array(use12Hours ? 12 : 24).keys())
.filter((i) => i % hourStep === 0)
.map((i) => (use12Hours && 0 === i ? 12 : i));
let minutesArray = Array.from(Array(60).keys()).filter(
(i) => i % minuteStep === 0,
);
let secondsArray = Array.from(Array(60).keys()).filter(
(i) => i % secondStep === 0,
);
hoursArray.forEach((hour) => {
if ('hours' === precision)
data.push(
new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
hour,
time.getMinutes(),
time.getSeconds(),
),
);
else
minutesArray.forEach((minute) => {
if ('minutes' === precision)
data.push(
new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
hour,
minute,
time.getSeconds(),
),
);
else
secondsArray.forEach((second) => {
data.push(
new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
hour,
minute,
second,
),
);
});
});
});
return data;
}, [hourStep, minuteStep, secondStep, selectedTime, use12Hours, precision]);
let hours = React.useMemo(() => {
let time = selectedTime ?? new Date();
return generateDataList(
use12Hours ? 12 : 24,
(i) =>
new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
use12Hours && 0 === i ? 12 : i,
time.getMinutes(),
time.getSeconds(),
),
hourStep,
);
}, [hourStep, selectedTime, use12Hours]);
let minutes = React.useMemo(() => {
let time = selectedTime ?? new Date();
return generateDataList(
60,
(i) =>
new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
time.getHours(),
i,
time.getSeconds(),
),
minuteStep,
);
}, [minuteStep, selectedTime]);
let seconds = React.useMemo(() => {
let time = selectedTime ?? new Date();
return generateDataList(
60,
(i) =>
new Date(
time.getFullYear(),
time.getMonth(),
time.getDate(),
time.getHours(),
time.getMinutes(),
i,
),
secondStep,
);
}, [secondStep, selectedTime]);
return React.createElement(
Box,
{
className: cx('iui-time-picker', className),
ref: forwardedRef,
...rest,
},
useCombinedRenderer
? React.createElement(TimePickerColumn, {
data: time,
isSameFocused: (val) =>
isSameTime(
val,
focusedTime,
precision,
use12Hours ? meridiem : void 0,
),
isSameSelected: (val) =>
isSameTime(
val,
selectedTime,
precision,
use12Hours ? meridiem : void 0,
),
onFocusChange: onTimeFocus,
onSelectChange: onTimeClick,
setFocus: setFocusHour,
precision: precision,
valueRenderer: combinedRenderer,
})
: React.createElement(
React.Fragment,
null,
React.createElement(TimePickerColumn, {
data: hours,
isSameFocused: (val) =>
isSameHour(val, focusedTime, use12Hours ? meridiem : void 0),
isSameSelected: (val) =>
isSameHour(val, selectedTime, use12Hours ? meridiem : void 0),
onFocusChange: onHourFocus,
onSelectChange: onHourClick,
setFocus: setFocusHour,
valueRenderer: hourRenderer,
}),
'hours' !== precision &&
React.createElement(TimePickerColumn, {
data: minutes,
isSameFocused: (val) => isSameMinute(val, focusedTime),
isSameSelected: (val) => isSameMinute(val, selectedTime),
onFocusChange: (date) => setFocusedTime(date),
onSelectChange: (date) => updateCurrentTime(date),
valueRenderer: minuteRenderer,
}),
'seconds' === precision &&
React.createElement(TimePickerColumn, {
data: seconds,
isSameFocused: (val) => isSameSecond(val, focusedTime),
isSameSelected: (val) => isSameSecond(val, selectedTime),
onFocusChange: (date) => setFocusedTime(date),
onSelectChange: (date) => updateCurrentTime(date),
valueRenderer: secondRenderer,
}),
),
use12Hours &&
React.createElement(TimePickerColumn, {
data: ['AM', 'PM'],
isSameFocused: (val) => isSameMeridiem(val, focusedTime),
isSameSelected: (val) => isSameMeridiem(val, selectedTime),
onFocusChange: (date) => onMeridiemFocus(date),
onSelectChange: (value) => onMeridiemClick(value),
valueRenderer: meridiemRenderer,
className: 'iui-period',
}),
);
});
if ('development' === process.env.NODE_ENV)
TimePicker.displayName = 'TimePicker';
let TimePickerColumn = (props) => {
let {
data,
onFocusChange,
onSelectChange,
isSameFocused,
isSameSelected,
setFocus = false,
valueRenderer,
precision = 'minutes',
className = 'iui-time',
} = props;
let needFocus = React.useRef(setFocus);
let handleTimeKeyDown = (
event,
maxValue,
onFocus,
onSelect,
currentValue,
) => {
if (event.altKey) return;
switch (event.key) {
case 'ArrowDown':
if (currentValue + 1 > maxValue) break;
onFocus(currentValue + 1);
needFocus.current = true;
event.preventDefault();
break;
case 'ArrowUp':
if (currentValue - 1 < 0) break;
onFocus(currentValue - 1);
needFocus.current = true;
event.preventDefault();
break;
case 'Enter':
case ' ':
case 'Spacebar':
onSelect(currentValue);
event.preventDefault();
break;
}
};
return React.createElement(
Box,
{
className: `${className}`,
},
React.createElement(
'ol',
null,
data.map((value, index) => {
let isSameFocus = isSameFocused(value);
return React.createElement(
Box,
{
as: 'li',
onKeyDown: (event) => {
handleTimeKeyDown(
event,
data.length - 1,
(index) => onFocusChange(data[index]),
(index) => onSelectChange(data[index]),
index,
);
},
className: cx({
'iui-selected': isSameSelected(value),
}),
key: index,
tabIndex: isSameFocus ? 0 : void 0,
ref: (ref) => {
if (!ref || !isSameFocus) return;
setTimeout(() => {
ref.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
if (needFocus.current) {
ref.focus();
needFocus.current = false;
}
});
},
onClick: () => {
onSelectChange(value);
},
},
valueRenderer(value, precision),
);
}),
),
);
};