UNPKG

14 kBJavaScriptView Raw
1/*
2 * Copyright 2015 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import classNames from "classnames";
17import * as React from "react";
18import { Classes as CoreClasses, Utils as CoreUtils, DISPLAYNAME_PREFIX, HTMLSelect, Icon, Intent, Keys, } from "@blueprintjs/core";
19import * as Classes from "./common/classes";
20import * as DateUtils from "./common/dateUtils";
21import { getDefaultMaxTime, getDefaultMinTime, getTimeUnit, getTimeUnitClassName, getTimeUnitMax, getTimeUnitPrintStr, isTimeUnitValid, setTimeUnit, TimeUnit, wrapTimeAtUnit, } from "./common/timeUnit";
22import * as Utils from "./common/utils";
23export const TimePrecision = {
24 MILLISECOND: "millisecond",
25 MINUTE: "minute",
26 SECOND: "second",
27};
28/**
29 * Time picker component.
30 *
31 * @see https://blueprintjs.com/docs/#datetime/timepicker
32 */
33export class TimePicker extends React.Component {
34 static defaultProps = {
35 autoFocus: false,
36 disabled: false,
37 maxTime: getDefaultMaxTime(),
38 minTime: getDefaultMinTime(),
39 precision: TimePrecision.MINUTE,
40 selectAllOnFocus: false,
41 showArrowButtons: false,
42 useAmPm: false,
43 };
44 static displayName = `${DISPLAYNAME_PREFIX}.TimePicker`;
45 constructor(props, context) {
46 super(props, context);
47 this.state = this.getFullStateFromValue(this.getInitialValue(), props.useAmPm);
48 }
49 timeInputIds = {
50 [TimeUnit.HOUR_24]: CoreUtils.uniqueId(TimeUnit.HOUR_24 + "-input"),
51 [TimeUnit.HOUR_12]: CoreUtils.uniqueId(TimeUnit.HOUR_12 + "-input"),
52 [TimeUnit.MINUTE]: CoreUtils.uniqueId(TimeUnit.MINUTE + "-input"),
53 [TimeUnit.SECOND]: CoreUtils.uniqueId(TimeUnit.SECOND + "-input"),
54 [TimeUnit.MS]: CoreUtils.uniqueId(TimeUnit.MS + "-input"),
55 };
56 render() {
57 const shouldRenderMilliseconds = this.props.precision === TimePrecision.MILLISECOND;
58 const shouldRenderSeconds = shouldRenderMilliseconds || this.props.precision === TimePrecision.SECOND;
59 const hourUnit = this.props.useAmPm ? TimeUnit.HOUR_12 : TimeUnit.HOUR_24;
60 const classes = classNames(Classes.TIMEPICKER, this.props.className, {
61 [CoreClasses.DISABLED]: this.props.disabled,
62 });
63 return (React.createElement("div", { className: classes },
64 React.createElement("div", { className: Classes.TIMEPICKER_ARROW_ROW },
65 this.maybeRenderArrowButton(true, hourUnit),
66 this.maybeRenderArrowButton(true, TimeUnit.MINUTE),
67 shouldRenderSeconds && this.maybeRenderArrowButton(true, TimeUnit.SECOND),
68 shouldRenderMilliseconds && this.maybeRenderArrowButton(true, TimeUnit.MS)),
69 React.createElement("div", { className: Classes.TIMEPICKER_INPUT_ROW },
70 this.renderInput(Classes.TIMEPICKER_HOUR, hourUnit, this.state.hourText),
71 this.renderDivider(),
72 this.renderInput(Classes.TIMEPICKER_MINUTE, TimeUnit.MINUTE, this.state.minuteText),
73 shouldRenderSeconds && this.renderDivider(),
74 shouldRenderSeconds &&
75 this.renderInput(Classes.TIMEPICKER_SECOND, TimeUnit.SECOND, this.state.secondText),
76 shouldRenderMilliseconds && this.renderDivider("."),
77 shouldRenderMilliseconds &&
78 this.renderInput(Classes.TIMEPICKER_MILLISECOND, TimeUnit.MS, this.state.millisecondText)),
79 this.maybeRenderAmPm(),
80 React.createElement("div", { className: Classes.TIMEPICKER_ARROW_ROW },
81 this.maybeRenderArrowButton(false, hourUnit),
82 this.maybeRenderArrowButton(false, TimeUnit.MINUTE),
83 shouldRenderSeconds && this.maybeRenderArrowButton(false, TimeUnit.SECOND),
84 shouldRenderMilliseconds && this.maybeRenderArrowButton(false, TimeUnit.MS))));
85 }
86 componentDidUpdate(prevProps) {
87 const didMinTimeChange = prevProps.minTime !== this.props.minTime;
88 const didMaxTimeChange = prevProps.maxTime !== this.props.maxTime;
89 const didBoundsChange = didMinTimeChange || didMaxTimeChange;
90 const didPropValueChange = prevProps.value !== this.props.value;
91 const shouldStateUpdate = didBoundsChange || didPropValueChange;
92 let value = this.state.value;
93 if (this.props.value == null) {
94 value = this.getInitialValue();
95 }
96 if (didBoundsChange) {
97 value = DateUtils.getTimeInRange(this.state.value, this.props.minTime, this.props.maxTime);
98 }
99 if (this.props.value != null && !DateUtils.areSameTime(this.props.value, prevProps.value)) {
100 value = this.props.value;
101 }
102 if (shouldStateUpdate) {
103 this.setState(this.getFullStateFromValue(value, this.props.useAmPm));
104 }
105 }
106 // begin method definitions: rendering
107 maybeRenderArrowButton(isDirectionUp, timeUnit) {
108 if (!this.props.showArrowButtons) {
109 return null;
110 }
111 const classes = classNames(Classes.TIMEPICKER_ARROW_BUTTON, getTimeUnitClassName(timeUnit));
112 const onClick = () => (isDirectionUp ? this.incrementTime : this.decrementTime)(timeUnit);
113 const label = `${isDirectionUp ? "Increase" : "Decrease"} ${getTimeUnitPrintStr(timeUnit)}`;
114 // set tabIndex=-1 to ensure a valid FocusEvent relatedTarget when focused
115 return (React.createElement("span", { "aria-controls": this.timeInputIds[timeUnit], "aria-label": label, tabIndex: -1, className: classes, onClick: onClick },
116 React.createElement(Icon, { icon: isDirectionUp ? "chevron-up" : "chevron-down", title: label })));
117 }
118 renderDivider(text = ":") {
119 return React.createElement("span", { className: Classes.TIMEPICKER_DIVIDER_TEXT }, text);
120 }
121 renderInput(className, unit, value) {
122 const valueNumber = parseInt(value, 10);
123 const isValid = isTimeUnitValid(unit, valueNumber);
124 const isHour = unit === TimeUnit.HOUR_12 || unit === TimeUnit.HOUR_24;
125 return (React.createElement("input", { "aria-label": getTimeUnitPrintStr(unit), "aria-valuemin": 0, "aria-valuenow": valueNumber, "aria-valuemax": getTimeUnitMax(unit), className: classNames(Classes.TIMEPICKER_INPUT, { [CoreClasses.intentClass(Intent.DANGER)]: !isValid }, className), id: this.timeInputIds[unit], onBlur: this.getInputBlurHandler(unit), onChange: this.getInputChangeHandler(unit), onFocus: this.getInputFocusHandler(unit), onKeyDown: this.getInputKeyDownHandler(unit), onKeyUp: this.getInputKeyUpHandler(unit), role: this.props.showArrowButtons ? "spinbutton" : undefined, value: value, disabled: this.props.disabled, autoFocus: isHour && this.props.autoFocus }));
126 }
127 maybeRenderAmPm() {
128 if (!this.props.useAmPm) {
129 return null;
130 }
131 return (React.createElement(HTMLSelect, { className: Classes.TIMEPICKER_AMPM_SELECT, disabled: this.props.disabled, onChange: this.handleAmPmChange, value: this.state.isPm ? "pm" : "am" },
132 React.createElement("option", { value: "am" }, "AM"),
133 React.createElement("option", { value: "pm" }, "PM")));
134 }
135 // begin method definitions: event handlers
136 getInputChangeHandler = (unit) => (e) => {
137 const text = getStringValueFromInputEvent(e);
138 switch (unit) {
139 case TimeUnit.HOUR_12:
140 case TimeUnit.HOUR_24:
141 this.setState({ hourText: text });
142 break;
143 case TimeUnit.MINUTE:
144 this.setState({ minuteText: text });
145 break;
146 case TimeUnit.SECOND:
147 this.setState({ secondText: text });
148 break;
149 case TimeUnit.MS:
150 this.setState({ millisecondText: text });
151 break;
152 }
153 };
154 getInputBlurHandler = (unit) => (e) => {
155 const text = getStringValueFromInputEvent(e);
156 this.updateTime(parseInt(text, 10), unit);
157 this.props.onBlur?.(e, unit);
158 };
159 getInputFocusHandler = (unit) => (e) => {
160 if (this.props.selectAllOnFocus) {
161 e.currentTarget.select();
162 }
163 this.props.onFocus?.(e, unit);
164 };
165 getInputKeyDownHandler = (unit) => (e) => {
166 handleKeyEvent(e, {
167 [Keys.ARROW_UP]: () => this.incrementTime(unit),
168 [Keys.ARROW_DOWN]: () => this.decrementTime(unit),
169 [Keys.ENTER]: () => {
170 e.currentTarget.blur();
171 },
172 });
173 this.props.onKeyDown?.(e, unit);
174 };
175 getInputKeyUpHandler = (unit) => (e) => {
176 this.props.onKeyUp?.(e, unit);
177 };
178 handleAmPmChange = (e) => {
179 const isNextPm = e.currentTarget.value === "pm";
180 if (isNextPm !== this.state.isPm) {
181 const hour = DateUtils.convert24HourMeridiem(this.state.value.getHours(), isNextPm);
182 this.setState({ isPm: isNextPm }, () => this.updateTime(hour, TimeUnit.HOUR_24));
183 }
184 };
185 // begin method definitions: state modification
186 /**
187 * Generates a full ITimePickerState object with all text fields set to formatted strings based on value
188 */
189 getFullStateFromValue(value, useAmPm) {
190 const timeInRange = DateUtils.getTimeInRange(value, this.props.minTime, this.props.maxTime);
191 const hourUnit = useAmPm ? TimeUnit.HOUR_12 : TimeUnit.HOUR_24;
192 /* tslint:disable:object-literal-sort-keys */
193 return {
194 hourText: formatTime(timeInRange.getHours(), hourUnit),
195 minuteText: formatTime(timeInRange.getMinutes(), TimeUnit.MINUTE),
196 secondText: formatTime(timeInRange.getSeconds(), TimeUnit.SECOND),
197 millisecondText: formatTime(timeInRange.getMilliseconds(), TimeUnit.MS),
198 value: timeInRange,
199 isPm: DateUtils.getIsPmFrom24Hour(timeInRange.getHours()),
200 };
201 /* tslint:enable:object-literal-sort-keys */
202 }
203 incrementTime = (unit) => this.shiftTime(unit, 1);
204 decrementTime = (unit) => this.shiftTime(unit, -1);
205 shiftTime(unit, amount) {
206 if (this.props.disabled) {
207 return;
208 }
209 const newTime = getTimeUnit(unit, this.state.value) + amount;
210 this.updateTime(wrapTimeAtUnit(unit, newTime), unit);
211 }
212 updateTime(time, unit) {
213 const newValue = DateUtils.clone(this.state.value);
214 if (isTimeUnitValid(unit, time)) {
215 setTimeUnit(unit, time, newValue, this.state.isPm);
216 if (DateUtils.isTimeInRange(newValue, this.props.minTime, this.props.maxTime)) {
217 this.updateState({ value: newValue });
218 }
219 else {
220 this.updateState(this.getFullStateFromValue(this.state.value, this.props.useAmPm));
221 }
222 }
223 else {
224 this.updateState(this.getFullStateFromValue(this.state.value, this.props.useAmPm));
225 }
226 }
227 updateState(state) {
228 let newState = state;
229 const hasNewValue = newState.value != null && !DateUtils.areSameTime(newState.value, this.state.value);
230 if (this.props.value == null) {
231 // component is uncontrolled
232 if (hasNewValue) {
233 newState = this.getFullStateFromValue(newState.value, this.props.useAmPm);
234 }
235 this.setState(newState);
236 }
237 else {
238 // component is controlled, and there's a new value
239 // so set inputs' text based off of _old_ value and later fire onChange with new value
240 if (hasNewValue) {
241 this.setState(this.getFullStateFromValue(this.state.value, this.props.useAmPm));
242 }
243 else {
244 // no new value, this means only text has changed (from user typing)
245 // we want inputs to change, so update state with new text for the inputs
246 // but don't change actual value
247 this.setState({ ...newState, value: DateUtils.clone(this.state.value) });
248 }
249 }
250 if (hasNewValue) {
251 this.props.onChange?.(newState.value);
252 }
253 }
254 getInitialValue() {
255 let value = this.props.minTime;
256 if (this.props.value != null) {
257 value = this.props.value;
258 }
259 else if (this.props.defaultValue != null) {
260 value = this.props.defaultValue;
261 }
262 return value;
263 }
264}
265function formatTime(time, unit) {
266 switch (unit) {
267 case TimeUnit.HOUR_24:
268 return time.toString();
269 case TimeUnit.HOUR_12:
270 return DateUtils.get12HourFrom24Hour(time).toString();
271 case TimeUnit.MINUTE:
272 case TimeUnit.SECOND:
273 return Utils.padWithZeroes(time.toString(), 2);
274 case TimeUnit.MS:
275 return Utils.padWithZeroes(time.toString(), 3);
276 default:
277 throw Error("Invalid TimeUnit");
278 }
279}
280function getStringValueFromInputEvent(e) {
281 return e.target.value;
282}
283function handleKeyEvent(e, actions, preventDefault = true) {
284 for (const k of Object.keys(actions)) {
285 const key = Number(k);
286 // HACKHACK: https://github.com/palantir/blueprint/issues/4165
287 // eslint-disable-next-line deprecation/deprecation
288 if (e.which === key) {
289 if (preventDefault) {
290 e.preventDefault();
291 }
292 actions[key]();
293 }
294 }
295}
296//# sourceMappingURL=timePicker.js.map
\No newline at end of file