UNPKG

@patternfly/react-core

Version:

This library provides a set of common React components for use with the PatternFly reference implementation.

368 lines • 20.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TimePicker = void 0; const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const react_styles_1 = require("@patternfly/react-styles"); const date_picker_1 = tslib_1.__importDefault(require("@patternfly/react-styles/css/components/DatePicker/date-picker")); const menu_1 = tslib_1.__importDefault(require("@patternfly/react-styles/css/components/Menu/menu")); const helpers_1 = require("../../helpers"); const Popper_1 = require("../../helpers/Popper/Popper"); const Menu_1 = require("../Menu"); const InputGroup_1 = require("../InputGroup"); const TextInput_1 = require("../TextInput"); const constants_1 = require("../../helpers/constants"); const TimePickerUtils_1 = require("./TimePickerUtils"); const HelperText_1 = require("../HelperText"); const outlined_clock_icon_1 = tslib_1.__importDefault(require('@patternfly/react-icons/dist/js/icons/outlined-clock-icon')); const c_date_picker__input_c_form_control_Width_1 = tslib_1.__importDefault(require('@patternfly/react-tokens/dist/js/c_date_picker__input_c_form_control_Width')); class TimePicker extends react_1.Component { constructor(props) { super(props); this.baseComponentRef = (0, react_1.createRef)(); this.toggleRef = (0, react_1.createRef)(); this.inputRef = (0, react_1.createRef)(); this.menuRef = (0, react_1.createRef)(); this.onDocClick = (event) => { var _a, _b, _c, _d; const clickedOnToggle = (_b = (_a = this.toggleRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.contains(event.target); const clickedWithinMenu = (_d = (_c = this.menuRef) === null || _c === void 0 ? void 0 : _c.current) === null || _d === void 0 ? void 0 : _d.contains(event.target); if (this.state.isTimeOptionsOpen && !(clickedOnToggle || clickedWithinMenu)) { this.onToggle(false); } }; this.handleGlobalKeys = (event) => { var _a, _b, _c, _d; const { isTimeOptionsOpen, focusedIndex, scrollIndex } = this.state; // keyboard pressed while focus on toggle if ((_b = (_a = this.inputRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.contains(event.target)) { if (!isTimeOptionsOpen && event.key !== constants_1.KeyTypes.Tab && event.key !== constants_1.KeyTypes.Escape) { this.onToggle(true); } else if (isTimeOptionsOpen) { if (event.key === constants_1.KeyTypes.Escape || event.key === constants_1.KeyTypes.Tab) { this.onToggle(false); } else if (event.key === constants_1.KeyTypes.Enter) { if (focusedIndex !== null) { this.focusSelection(focusedIndex); event.stopPropagation(); } else { this.onToggle(false); } } else if (event.key === constants_1.KeyTypes.ArrowDown || event.key === constants_1.KeyTypes.ArrowUp) { this.focusSelection(scrollIndex); this.updateFocusedIndex(0); event.preventDefault(); } } // keyboard pressed while focus on menu item } else if ((_d = (_c = this.menuRef) === null || _c === void 0 ? void 0 : _c.current) === null || _d === void 0 ? void 0 : _d.contains(event.target)) { if (event.key === constants_1.KeyTypes.ArrowDown) { this.updateFocusedIndex(1); event.preventDefault(); } else if (event.key === constants_1.KeyTypes.ArrowUp) { this.updateFocusedIndex(-1); event.preventDefault(); } else if (event.key === constants_1.KeyTypes.Escape || event.key === constants_1.KeyTypes.Tab) { this.inputRef.current.focus(); this.onToggle(false); } } }; this.updateFocusedIndex = (increment) => { this.setState((prevState) => { const maxIndex = this.getOptions().length - 1; let nextIndex = prevState.focusedIndex !== null ? prevState.focusedIndex + increment : prevState.scrollIndex + increment; if (nextIndex < 0) { nextIndex = maxIndex; } else if (nextIndex > maxIndex) { nextIndex = 0; } this.scrollToIndex(nextIndex); return { focusedIndex: nextIndex }; }); }; // fixes issue where menuAppendTo="inline" results in the menu item that should be scrolled to being out of view; this will select the menu item that comes before the intended one, causing that before-item to be placed out of view instead this.getIndexToScroll = (index) => { if (this.props.menuAppendTo === 'inline') { return index > 0 ? index - 1 : 0; } return index; }; this.scrollToIndex = (index) => { this.getOptions()[index].closest(`.${menu_1.default.menuContent}`).scrollTop = this.getOptions()[this.getIndexToScroll(index)].offsetTop; }; this.focusSelection = (index) => { var _a; const indexToFocus = index !== -1 ? index : 0; if ((_a = this.menuRef) === null || _a === void 0 ? void 0 : _a.current) { this.getOptions()[indexToFocus].querySelector(`.${menu_1.default.menuItem}`).focus(); } }; this.scrollToSelection = (time) => { const { delimiter, is24Hour } = this.props; let splitTime = time.split(this.props.delimiter); let focusedIndex = null; // build out the rest of the time assuming hh:00 if it's a partial time if (splitTime.length < 2) { time = `${time}${delimiter}00`; splitTime = time.split(delimiter); // due to only the input including seconds when includeSeconds=true, we need to build a temporary time here without those seconds so that an exact or close match can be scrolled to within the menu (which does not include seconds in any of the options) } else if (splitTime.length > 2) { time = (0, TimePickerUtils_1.parseTime)(time, this.state.timeRegex, delimiter, !is24Hour, false); splitTime = time.split(delimiter); } // for 12hr variant, autoscroll to pm if it's currently the afternoon, otherwise autoscroll to am if (!is24Hour && splitTime.length > 1 && splitTime[1].length < 2) { const minutes = splitTime[1].length === 0 ? '00' : splitTime[1] + '0'; time = `${splitTime[0]}${delimiter}${minutes}${new Date().getHours() > 11 ? TimePickerUtils_1.pmSuffix : TimePickerUtils_1.amSuffix}`; } else if (!is24Hour && splitTime.length > 1 && splitTime[1].length === 2 && !time.toUpperCase().includes(TimePickerUtils_1.amSuffix.toUpperCase().trim()) && !time.toUpperCase().includes(TimePickerUtils_1.pmSuffix.toUpperCase().trim())) { time = `${time}${new Date().getHours() > 11 ? TimePickerUtils_1.pmSuffix : TimePickerUtils_1.amSuffix}`; } let scrollIndex = this.getOptions().findIndex((option) => option.textContent === time); // if we found an exact match, scroll to match and return index of match for focus if (scrollIndex !== -1) { this.scrollToIndex(scrollIndex); focusedIndex = scrollIndex; } else if (splitTime.length === 2) { // no exact match, scroll to closest hour but don't return index for focus let amPm = ''; if (!is24Hour) { if (splitTime[1].toUpperCase().includes('P')) { amPm = TimePickerUtils_1.pmSuffix; } else if (splitTime[1].toUpperCase().includes('A')) { amPm = TimePickerUtils_1.amSuffix; } } time = `${splitTime[0]}${delimiter}00${amPm}`; scrollIndex = this.getOptions().findIndex((option) => option.textContent === time); if (scrollIndex !== -1) { this.scrollToIndex(scrollIndex); } } this.setState({ focusedIndex, scrollIndex }); }; this.getRegExp = (includeSeconds = true) => { const { is24Hour, delimiter } = this.props; let baseRegex = `\\s*(\\d\\d?)${delimiter}([0-5]\\d)`; if (includeSeconds) { baseRegex += `${delimiter}?([0-5]\\d)?`; } return new RegExp(`^${baseRegex}${is24Hour ? '' : '\\s*([AaPp][Mm])?'}\\s*$`); }; this.getOptions = () => { var _a; return (((_a = this.menuRef) === null || _a === void 0 ? void 0 : _a.current) ? Array.from(this.menuRef.current.querySelectorAll(`.${menu_1.default.menuListItem}`)) : []); }; this.isValidFormat = (time) => { if (this.props.validateTime) { return this.props.validateTime(time); } const { delimiter, is24Hour, includeSeconds } = this.props; return (0, TimePickerUtils_1.validateTime)(time, this.getRegExp(includeSeconds), delimiter, !is24Hour); }; this.isValidTime = (time) => { const { delimiter, includeSeconds } = this.props; const { minTimeState, maxTimeState } = this.state; return (0, TimePickerUtils_1.isWithinMinMax)(minTimeState, maxTimeState, time, delimiter, includeSeconds); }; this.isValid = (time) => this.isValidFormat(time) && this.isValidTime(time); this.onToggle = (isOpen) => { // on close, parse and validate input this.setState((prevState) => { const { timeRegex, isInvalid, timeState } = prevState; const { delimiter, is24Hour, includeSeconds, onChange } = this.props; const time = (0, TimePickerUtils_1.parseTime)(timeState, timeRegex, delimiter, !is24Hour, includeSeconds); // Call onChange when Enter is pressed in input and timeoption does not exist in menu if (onChange && !isOpen && time !== timeState) { onChange(null, time, (0, TimePickerUtils_1.getHours)(time, timeRegex), (0, TimePickerUtils_1.getMinutes)(time, timeRegex), (0, TimePickerUtils_1.getSeconds)(time, timeRegex), this.isValid(time)); } return { isTimeOptionsOpen: isOpen, timeState: time, isInvalid: isOpen ? isInvalid : !this.isValid(time) }; }); this.props.setIsOpen(isOpen); if (!isOpen) { this.inputRef.current.focus(); } }; this.onSelect = (e) => { const { timeRegex, timeState } = this.state; const { delimiter, is24Hour, includeSeconds, setIsOpen } = this.props; const time = (0, TimePickerUtils_1.parseTime)(e.target.textContent, timeRegex, delimiter, !is24Hour, includeSeconds); if (time !== timeState) { this.onInputChange(e, time); } this.inputRef.current.focus(); this.setState({ isTimeOptionsOpen: false, isInvalid: false }); setIsOpen(false); }; this.onInputClick = (e) => { if (!this.state.isTimeOptionsOpen) { this.onToggle(true); } e.stopPropagation(); }; this.onInputChange = (event, newTime) => { const { onChange } = this.props; const { timeRegex } = this.state; if (onChange) { onChange(event, newTime, (0, TimePickerUtils_1.getHours)(newTime, timeRegex), (0, TimePickerUtils_1.getMinutes)(newTime, timeRegex), (0, TimePickerUtils_1.getSeconds)(newTime, timeRegex), this.isValid(newTime)); } this.scrollToSelection(newTime); this.setState({ timeState: newTime }); }; const { is24Hour, delimiter, time, includeSeconds, isOpen } = this.props; let { minTime, maxTime } = this.props; if (minTime === '') { const minSeconds = includeSeconds ? `${delimiter}00` : ''; minTime = is24Hour ? `00${delimiter}00${minSeconds}` : `12${delimiter}00${minSeconds} AM`; } if (maxTime === '') { const maxSeconds = includeSeconds ? `${delimiter}59` : ''; maxTime = is24Hour ? `23${delimiter}59${maxSeconds}` : `11${delimiter}59${maxSeconds} PM`; } const timeRegex = this.getRegExp(); this.state = { isInvalid: false, isTimeOptionsOpen: isOpen, timeState: (0, TimePickerUtils_1.parseTime)(time, timeRegex, delimiter, !is24Hour, includeSeconds), focusedIndex: null, scrollIndex: 0, timeRegex, minTimeState: (0, TimePickerUtils_1.parseTime)(minTime, timeRegex, delimiter, !is24Hour, includeSeconds), maxTimeState: (0, TimePickerUtils_1.parseTime)(maxTime, timeRegex, delimiter, !is24Hour, includeSeconds) }; } componentDidMount() { document.addEventListener('mousedown', this.onDocClick); document.addEventListener('touchstart', this.onDocClick); document.addEventListener('keydown', this.handleGlobalKeys); this.setState({ isInvalid: !this.isValid(this.state.timeState) }); } componentWillUnmount() { document.removeEventListener('mousedown', this.onDocClick); document.removeEventListener('touchstart', this.onDocClick); document.removeEventListener('keydown', this.handleGlobalKeys); } componentDidUpdate(prevProps, prevState) { const { timeState, isTimeOptionsOpen, isInvalid, timeRegex } = this.state; const { time, is24Hour, delimiter, includeSeconds, isOpen, minTime, maxTime } = this.props; if (prevProps.isOpen !== isOpen) { this.onToggle(isOpen); } if (isTimeOptionsOpen && !prevState.isTimeOptionsOpen && timeState && !isInvalid) { this.scrollToSelection(timeState); } if (delimiter !== prevProps.delimiter) { this.setState({ timeRegex: this.getRegExp() }); } if (time !== '' && time !== prevProps.time) { const parsedTime = (0, TimePickerUtils_1.parseTime)(time, timeRegex, delimiter, !is24Hour, includeSeconds); this.setState({ timeState: parsedTime, isInvalid: !this.isValid(parsedTime) }); } if (minTime !== '' && minTime !== prevProps.minTime) { this.setState({ minTimeState: (0, TimePickerUtils_1.parseTime)(minTime, timeRegex, delimiter, !is24Hour, includeSeconds) }); } if (maxTime !== '' && maxTime !== prevProps.maxTime) { this.setState({ maxTimeState: (0, TimePickerUtils_1.parseTime)(maxTime, timeRegex, delimiter, !is24Hour, includeSeconds) }); } } render() { const _a = this.props, { 'aria-label': ariaLabel, isDisabled, className, placeholder, id, menuAppendTo, is24Hour, invalidFormatErrorMessage, invalidMinMaxErrorMessage, stepMinutes, width, delimiter, inputProps, /* eslint-disable @typescript-eslint/no-unused-vars */ onChange, /* eslint-disable @typescript-eslint/no-unused-vars */ setIsOpen, /* eslint-disable @typescript-eslint/no-unused-vars */ isOpen, time, validateTime, minTime, maxTime, includeSeconds, zIndex } = _a, props = tslib_1.__rest(_a, ['aria-label', "isDisabled", "className", "placeholder", "id", "menuAppendTo", "is24Hour", "invalidFormatErrorMessage", "invalidMinMaxErrorMessage", "stepMinutes", "width", "delimiter", "inputProps", "onChange", "setIsOpen", "isOpen", "time", "validateTime", "minTime", "maxTime", "includeSeconds", "zIndex"]); const { timeState, isTimeOptionsOpen, isInvalid, minTimeState, maxTimeState } = this.state; const style = { [c_date_picker__input_c_form_control_Width_1.default.name]: width }; const options = (0, TimePickerUtils_1.makeTimeOptions)(stepMinutes, !is24Hour, delimiter, minTimeState, maxTimeState, includeSeconds); const isValidFormat = this.isValidFormat(timeState); const randomId = id || (0, helpers_1.getUniqueId)('time-picker'); const getParentElement = () => { if (this.baseComponentRef && this.baseComponentRef.current) { return this.baseComponentRef.current.parentElement; } return null; }; const menuContainer = ((0, jsx_runtime_1.jsx)(Menu_1.Menu, { ref: this.menuRef, isScrollable: true, children: (0, jsx_runtime_1.jsx)(Menu_1.MenuContent, { maxMenuHeight: "200px", children: (0, jsx_runtime_1.jsx)(Menu_1.MenuList, { "aria-label": ariaLabel, children: options.map((option, index) => ((0, jsx_runtime_1.jsx)(Menu_1.MenuItem, { onClick: this.onSelect, id: `${randomId}-option-${index}`, children: option }, option))) }) }) })); const textInput = ((0, jsx_runtime_1.jsx)(TextInput_1.TextInput, Object.assign({ "aria-haspopup": "menu", id: `${randomId}-input`, "aria-label": ariaLabel, validated: isInvalid ? 'error' : 'default', placeholder: placeholder, value: timeState || '', type: "text", customIcon: (0, jsx_runtime_1.jsx)(outlined_clock_icon_1.default, {}), onClick: this.onInputClick, onChange: this.onInputChange, autoComplete: "off", isDisabled: isDisabled, isExpanded: isTimeOptionsOpen, ref: this.inputRef }, inputProps))); let calculatedAppendTo; switch (menuAppendTo) { case 'inline': calculatedAppendTo = () => this.toggleRef.current; break; case 'parent': calculatedAppendTo = getParentElement; break; default: calculatedAppendTo = menuAppendTo; } return ((0, jsx_runtime_1.jsx)("div", { ref: this.baseComponentRef, className: (0, react_styles_1.css)(date_picker_1.default.datePicker, className), children: (0, jsx_runtime_1.jsxs)("div", Object.assign({ className: (0, react_styles_1.css)(date_picker_1.default.datePickerInput), style: style }, props, { children: [(0, jsx_runtime_1.jsx)(InputGroup_1.InputGroup, { children: (0, jsx_runtime_1.jsx)(InputGroup_1.InputGroupItem, { children: (0, jsx_runtime_1.jsx)("div", { id: randomId, children: (0, jsx_runtime_1.jsx)("div", { ref: this.toggleRef, style: { paddingLeft: '0' }, children: (0, jsx_runtime_1.jsx)(Popper_1.Popper, { appendTo: calculatedAppendTo, trigger: textInput, triggerRef: this.toggleRef, popper: menuContainer, popperRef: this.menuRef, isVisible: isTimeOptionsOpen, zIndex: zIndex }) }) }) }) }), isInvalid && ((0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(date_picker_1.default.datePickerHelperText), children: (0, jsx_runtime_1.jsx)(HelperText_1.HelperText, { children: (0, jsx_runtime_1.jsx)(HelperText_1.HelperTextItem, { variant: "error", children: !isValidFormat ? invalidFormatErrorMessage : invalidMinMaxErrorMessage }) }) }))] })) })); } } exports.TimePicker = TimePicker; TimePicker.displayName = 'TimePicker'; TimePicker.defaultProps = { className: '', isDisabled: false, time: '', is24Hour: false, invalidFormatErrorMessage: 'Invalid time format', invalidMinMaxErrorMessage: 'Invalid time entered', placeholder: 'hh:mm', delimiter: ':', 'aria-label': 'Time picker', width: '150px', menuAppendTo: 'inline', stepMinutes: 30, inputProps: {}, minTime: '', maxTime: '', isOpen: false, setIsOpen: () => { }, zIndex: 9999 }; //# sourceMappingURL=TimePicker.js.map