@instructure/ui-time-select 8.37.1-snapshot-13 → 8.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,7 +3,7 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [8.37.1-snapshot-13](https://github.com/instructure/instructure-ui/compare/v8.37.0...v8.37.1-snapshot-13) (2023-05-15)
6
+ # [8.38.0](https://github.com/instructure/instructure-ui/compare/v8.37.0...v8.38.0) (2023-05-15)
7
7
 
8
8
  **Note:** Version bump only for package @instructure/ui-time-select
9
9
 
@@ -55,15 +55,31 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
55
55
  };
56
56
  this.handleBlur = event => {
57
57
  var _this$props$onBlur, _this$props;
58
- this.setState({
59
- highlightedOptionId: void 0
60
- });
61
58
  (_this$props$onBlur = (_this$props = this.props).onBlur) === null || _this$props$onBlur === void 0 ? void 0 : _this$props$onBlur.call(_this$props, event);
62
59
  };
63
60
  this.handleInputChange = event => {
64
61
  var _this$props$onInputCh, _this$props3;
65
62
  const value = event.target.value;
66
63
  const newOptions = this.filterOptions(value);
64
+ if ((newOptions === null || newOptions === void 0 ? void 0 : newOptions.length) == 1) {
65
+ // if there is only 1 option, it will be automatically selected except
66
+ // if in controlled mode (it would commit this change)
67
+ if (!this.isControlled) {
68
+ this.setState({
69
+ selectedOptionId: newOptions[0].id
70
+ });
71
+ }
72
+ this.setState({
73
+ fireChangeOnBlur: newOptions[0]
74
+ });
75
+ } else {
76
+ this.setState({
77
+ // needs not to lose selectedOptionId in controlled mode otherwise it'd
78
+ // revert to the default or '' instead of the set value
79
+ selectedOptionId: this.isControlled ? this.state.selectedOptionId : void 0,
80
+ fireChangeOnBlur: void 0
81
+ });
82
+ }
67
83
  this.setState({
68
84
  inputValue: value,
69
85
  filteredOptions: newOptions,
@@ -77,112 +93,111 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
77
93
  const inputAsDate = this.parseInputText(value);
78
94
  (_this$props$onInputCh = (_this$props3 = this.props).onInputChange) === null || _this$props$onInputCh === void 0 ? void 0 : _this$props$onInputCh.call(_this$props3, event, value, inputAsDate.isValid() ? inputAsDate.toISOString() : void 0);
79
95
  };
96
+ this.onKeyDown = event => {
97
+ var _this$props$onKeyDown, _this$props4;
98
+ const input = this.parseInputText(this.state.inputValue);
99
+ if (event.key === 'Enter' && this.props.allowNonStepInput && input.isValid()) {
100
+ this.setState(() => ({
101
+ isShowingOptions: false,
102
+ highlightedOptionId: void 0
103
+ }));
104
+ // others are set in handleBlurOrEsc
105
+ }
106
+
107
+ (_this$props$onKeyDown = (_this$props4 = this.props).onKeyDown) === null || _this$props$onKeyDown === void 0 ? void 0 : _this$props$onKeyDown.call(_this$props4, event);
108
+ };
80
109
  this.handleShowOptions = event => {
81
- var _this$props$onShowOpt2, _this$props4;
110
+ var _this$props$onShowOpt2, _this$props5;
82
111
  this.setState({
83
112
  isShowingOptions: true,
84
113
  highlightedOptionId: this.state.selectedOptionId
85
114
  });
86
- (_this$props$onShowOpt2 = (_this$props4 = this.props).onShowOptions) === null || _this$props$onShowOpt2 === void 0 ? void 0 : _this$props$onShowOpt2.call(_this$props4, event);
115
+ (_this$props$onShowOpt2 = (_this$props5 = this.props).onShowOptions) === null || _this$props$onShowOpt2 === void 0 ? void 0 : _this$props$onShowOpt2.call(_this$props5, event);
87
116
  };
88
- this.handleHideOptions = event => {
89
- var _this$props$onHideOpt, _this$props5;
90
- const input = this.parseInputText(this.state.inputValue);
117
+ this.handleBlurOrEsc = event => {
118
+ var _this$props$onHideOpt, _this$props7;
119
+ const _this$state = this.state,
120
+ selectedOptionId = _this$state.selectedOptionId,
121
+ inputValue = _this$state.inputValue;
91
122
  let defaultValue = '';
92
123
  if (this.props.defaultValue) {
93
124
  const date = DateTime.parse(this.props.defaultValue, this.locale(), this.timezone());
94
125
  defaultValue = this.props.format ? date.format(this.props.format) : date.toISOString();
95
126
  }
96
- if (!this.props.allowNonStepInput || this.props.allowNonStepInput && input.isValid() && input.minute() % this.props.step === 0) {
97
- const selectedOptionId = this.state.selectedOptionId;
98
- const option = this.getOption('id', selectedOptionId);
99
- this.setState(() => ({
100
- isShowingOptions: false,
101
- highlightedOptionId: void 0,
102
- inputValue: selectedOptionId ? option.label : defaultValue,
103
- filteredOptions: this.filterOptions(''),
104
- ...this.matchValue()
105
- }));
106
- } else {
127
+ const selectedOption = this.getOption('id', selectedOptionId);
128
+ let newInputValue = defaultValue;
129
+ if (selectedOption) {
130
+ // If there is a selected option use its value in the input field.
131
+ newInputValue = selectedOption.label;
132
+ }
133
+ // if input was completely cleared, ensure it stays clear
134
+ // e.g. defaultValue defined, but no selection yet made
135
+ else if (inputValue === '') {
136
+ newInputValue = '';
137
+ }
138
+ this.setState(() => ({
139
+ isShowingOptions: false,
140
+ highlightedOptionId: void 0,
141
+ inputValue: newInputValue,
142
+ filteredOptions: this.filterOptions('')
143
+ }));
144
+ if (this.state.fireChangeOnBlur && event.key !== 'Escape') {
145
+ var _this$props$onChange, _this$props6;
107
146
  this.setState(() => ({
108
- isShowingOptions: false,
109
- highlightedOptionId: void 0,
110
- inputValue: this.state.lastValidInput || defaultValue || '',
111
- filteredOptions: this.filterOptions('')
147
+ fireChangeOnBlur: void 0
112
148
  }));
149
+ (_this$props$onChange = (_this$props6 = this.props).onChange) === null || _this$props$onChange === void 0 ? void 0 : _this$props$onChange.call(_this$props6, event, {
150
+ value: this.state.fireChangeOnBlur.value.toISOString(),
151
+ inputText: this.state.fireChangeOnBlur.label
152
+ });
113
153
  }
114
- (_this$props$onHideOpt = (_this$props5 = this.props).onHideOptions) === null || _this$props$onHideOpt === void 0 ? void 0 : _this$props$onHideOpt.call(_this$props5, event);
115
- };
116
- this.handleHighlightOption = (event, _ref) => {
117
- let id = _ref.id;
118
- if (id === this._emptyOptionId) return;
119
- const type = event.type;
120
- const option = this.getOption('id', id).label;
121
- this.setState(state => ({
122
- highlightedOptionId: id,
123
- inputValue: type === 'keydown' ? option : state.inputValue
124
- }));
154
+ // TODO only fire this if handleSelectOption was not called before.
155
+ (_this$props$onHideOpt = (_this$props7 = this.props).onHideOptions) === null || _this$props$onHideOpt === void 0 ? void 0 : _this$props$onHideOpt.call(_this$props7, event);
125
156
  };
126
- this.handleSelectOption = (event, _ref2) => {
127
- var _this$props$onHideOpt2, _this$props7;
128
- let id = _ref2.id;
129
- if (id === this._emptyOptionId) {
157
+ this.handleSelectOption = (event, data) => {
158
+ var _this$props$onHideOpt2, _this$props9;
159
+ if (data.id === this._emptyOptionId) {
130
160
  this.setState({
131
161
  isShowingOptions: false
132
162
  });
133
163
  return;
134
164
  }
135
- const option = this.getOption('id', id);
165
+ const selectedOption = this.getOption('id', data.id);
136
166
  let newInputValue;
167
+ const currentSelectedOptionId = this.state.selectedOptionId;
137
168
  if (this.isControlled) {
169
+ // in controlled mode we leave to the user to set the value of the
170
+ // component e.g. in the onChange event handler.
171
+ // This is accomplished by not setting a selectedOptionId
138
172
  const prev = this.getOption('id', this.state.selectedOptionId);
139
173
  newInputValue = prev ? prev.label : '';
140
174
  this.setState({
141
- isShowingOptions: false,
142
- inputValue: newInputValue,
143
- filteredOptions: this.filterOptions('')
175
+ isShowingOptions: false
144
176
  });
145
177
  } else {
146
- newInputValue = option.label;
178
+ newInputValue = selectedOption.label;
147
179
  this.setState({
148
180
  isShowingOptions: false,
149
- selectedOptionId: id,
150
- inputValue: newInputValue,
151
- filteredOptions: this.filterOptions('')
152
- });
153
- }
154
- if (id !== this.state.selectedOptionId) {
155
- var _this$props$onChange, _this$props6;
156
- this.setState({
157
- lastValidInput: newInputValue
158
- });
159
- (_this$props$onChange = (_this$props6 = this.props).onChange) === null || _this$props$onChange === void 0 ? void 0 : _this$props$onChange.call(_this$props6, event, {
160
- value: option.value,
161
- inputText: newInputValue
181
+ selectedOptionId: data.id,
182
+ inputValue: newInputValue
162
183
  });
163
184
  }
164
- (_this$props$onHideOpt2 = (_this$props7 = this.props).onHideOptions) === null || _this$props$onHideOpt2 === void 0 ? void 0 : _this$props$onHideOpt2.call(_this$props7, event);
165
- };
166
- this.onKeyDown = event => {
167
- var _this$props$onKeyDown, _this$props9;
168
- const input = this.parseInputText(this.state.inputValue);
169
- // validate input and if OK close dropdown
170
- if (event.key === 'Enter' && this.props.allowNonStepInput && input.isValid() && input.minute() % this.props.step !== 0 && this.state.lastValidInput != this.state.inputValue) {
185
+ if (data.id !== currentSelectedOptionId) {
171
186
  var _this$props$onChange2, _this$props8;
172
- this.setState(() => ({
173
- isShowingOptions: false,
174
- highlightedOptionId: void 0,
175
- filteredOptions: this.filterOptions(''),
176
- lastValidInput: this.state.inputValue
177
- }));
178
187
  (_this$props$onChange2 = (_this$props8 = this.props).onChange) === null || _this$props$onChange2 === void 0 ? void 0 : _this$props$onChange2.call(_this$props8, event, {
179
- value: input.toISOString(),
180
- inputText: this.state.inputValue
188
+ value: selectedOption.value.toISOString(),
189
+ inputText: newInputValue
181
190
  });
182
- // others are set in OnHideOptions
183
191
  }
184
-
185
- (_this$props$onKeyDown = (_this$props9 = this.props).onKeyDown) === null || _this$props$onKeyDown === void 0 ? void 0 : _this$props$onKeyDown.call(_this$props9, event);
192
+ (_this$props$onHideOpt2 = (_this$props9 = this.props).onHideOptions) === null || _this$props$onHideOpt2 === void 0 ? void 0 : _this$props$onHideOpt2.call(_this$props9, event);
193
+ };
194
+ this.handleHighlightOption = (event, data) => {
195
+ if (data.id === this._emptyOptionId) return;
196
+ const option = this.getOption('id', data.id).label;
197
+ this.setState(state => ({
198
+ highlightedOptionId: data.id,
199
+ inputValue: event.type === 'keydown' ? option : state.inputValue
200
+ }));
186
201
  };
187
202
  this.parseInputText = inputValue => {
188
203
  const input = DateTime.parse(inputValue, this.locale(), this.timezone(), [this.props.format], true);
@@ -237,22 +252,29 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
237
252
  return DateTime.browserTimeZone();
238
253
  }
239
254
  componentDidUpdate(prevProps) {
240
- if (this.props.step !== prevProps.step || this.props.format !== prevProps.format || this.props.locale !== prevProps.locale || this.props.timezone !== prevProps.timezone) {
255
+ if (this.props.step !== prevProps.step || this.props.format !== prevProps.format || this.props.locale !== prevProps.locale || this.props.timezone !== prevProps.timezone || this.props.allowNonStepInput !== prevProps.allowNonStepInput) {
241
256
  // options change, reset everything
242
257
  // when controlled, selection will be preserved
243
258
  // when uncontrolled, selection will be lost
244
259
  this.setState(this.getInitialState());
245
260
  }
246
261
  if (this.props.value !== prevProps.value) {
247
- const normalizedValue = this.normalizeISOTime(this.props.value);
262
+ let newValue;
263
+ if (this.props.value) {
264
+ newValue = DateTime.parse(this.props.value, this.locale(), this.timezone());
265
+ }
248
266
  // value changed
249
267
  const initState = this.getInitialState();
250
268
  this.setState(initState);
251
269
  // options need to be passed because state is not set immediately
252
- let option = this.getOption('value', normalizedValue, initState.options);
253
- if (typeof normalizedValue === 'undefined') {
270
+ let option;
271
+ if (!this.isControlled) {
254
272
  // preserve current value when changing from controlled to uncontrolled
255
- option = this.getOption('value', this.normalizeISOTime(prevProps.value));
273
+ if (prevProps.value) {
274
+ option = this.getOption('id', this.getFormattedId(DateTime.parse(prevProps.value, this.locale(), this.timezone())));
275
+ }
276
+ } else if (newValue) {
277
+ option = this.getOption('id', this.getFormattedId(newValue), initState.options);
256
278
  }
257
279
  const outsideVal = this.props.value ? this.props.value : '';
258
280
  // value does not match an existing option
@@ -267,14 +289,10 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
267
289
  });
268
290
  }
269
291
  }
270
-
271
- // value needs to be normalized because e.g. 2022-03-29T19:00Z and
272
- // 2022-03-29T19:00:00.000Z refer to the same time in the same timezone.
273
- normalizeISOTime(value) {
274
- if (!value) {
275
- return value;
276
- }
277
- return DateTime.parse(value, this.locale(), this.timezone()).toISOString();
292
+ getFormattedId(date) {
293
+ // ISO8601 strings may contain a space. Remove any spaces before using the
294
+ // date as the id.
295
+ return date.toISOString().replace(/\s/g, '');
278
296
  }
279
297
  getInitialState() {
280
298
  const initialOptions = this.generateOptions();
@@ -282,7 +300,8 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
282
300
  return {
283
301
  inputValue: initialSelection ? initialSelection.label : '',
284
302
  options: initialOptions,
285
- filteredOptions: initialOptions,
303
+ // 288 = 5 min step
304
+ filteredOptions: initialOptions.length > 288 ? initialOptions.filter(opt => opt.value.minute() % this.props.step === 0) : initialOptions,
286
305
  isShowingOptions: false,
287
306
  highlightedOptionId: initialSelection ? initialSelection.id : void 0,
288
307
  selectedOptionId: initialSelection ? initialSelection.id : void 0
@@ -296,16 +315,18 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
296
315
  format = _this$props10.format;
297
316
  const initialValue = value || defaultValue;
298
317
  if (typeof initialValue === 'string') {
318
+ const date = DateTime.parse(initialValue, this.locale(), this.timezone());
299
319
  // get option based on value or defaultValue, if provided
300
- const option = this.getOption('value', this.normalizeISOTime(initialValue), options);
320
+ const option = this.getOption('value', date, options);
301
321
  if (option) {
302
322
  // value matches an existing option
303
323
  return option;
304
324
  }
305
325
  // value does not match an existing option
306
- const date = DateTime.parse(initialValue, this.locale(), this.timezone());
307
326
  return {
308
- label: format ? date.format(format) : date.toISOString()
327
+ id: this.getFormattedId(date),
328
+ label: format ? date.format(format) : date.toISOString(),
329
+ value: date
309
330
  };
310
331
  }
311
332
  // otherwise, return first option, if desired
@@ -314,11 +335,6 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
314
335
  }
315
336
  return void 0;
316
337
  }
317
- getFormattedId(date) {
318
- // ISO8601 strings may contain a space. Remove any spaces before using the
319
- // date as the id.
320
- return date.toISOString().replace(/\s/g, '');
321
- }
322
338
  getBaseDate() {
323
339
  let baseDate;
324
340
  const baseValue = this.props.value || this.props.defaultValue;
@@ -335,9 +351,12 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
335
351
  generateOptions() {
336
352
  const date = this.getBaseDate();
337
353
  const options = [];
354
+ const step = this.props.step ? this.props.step : 30;
355
+ const maxMinute = this.props.allowNonStepInput ? 60 : 60 / step;
356
+ const minuteStep = this.props.allowNonStepInput ? 1 : step;
338
357
  for (let hour = 0; hour < 24; hour++) {
339
- for (let minute = 0; minute < 60 / this.props.step; minute++) {
340
- const minutes = minute * this.props.step;
358
+ for (let minute = 0; minute < maxMinute; minute++) {
359
+ const minutes = minute * minuteStep;
341
360
  const newDate = date.set({
342
361
  hour: hour,
343
362
  minute: minutes
@@ -345,8 +364,7 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
345
364
  // store time options
346
365
  options.push({
347
366
  id: this.getFormattedId(newDate),
348
- // iso no spaces
349
- value: newDate.toISOString(),
367
+ value: newDate.clone(),
350
368
  label: this.props.format ? newDate.format(this.props.format) : newDate.toISOString()
351
369
  });
352
370
  }
@@ -354,58 +372,38 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
354
372
  return options;
355
373
  }
356
374
  filterOptions(inputValue) {
357
- var _this$state;
358
- return (_this$state = this.state) === null || _this$state === void 0 ? void 0 : _this$state.options.filter(option => option.label.toLowerCase().startsWith(inputValue.toLowerCase()));
359
- }
360
- matchValue() {
361
- const _this$state2 = this.state,
362
- inputValue = _this$state2.inputValue,
363
- filteredOptions = _this$state2.filteredOptions,
364
- highlightedOptionId = _this$state2.highlightedOptionId,
365
- selectedOptionId = _this$state2.selectedOptionId;
366
-
367
- // an option matching user input exists
368
- if (filteredOptions.length === 1) {
369
- const onlyOption = filteredOptions[0];
370
- // automatically select the matching option
371
- if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
372
- return {
373
- inputValue: onlyOption.label,
374
- selectedOptionId: onlyOption.id,
375
- filteredOptions: this.filterOptions('')
376
- };
377
- }
378
- }
379
- // no match found, return selected option label to input
380
- const selectedOption = this.getOption('id', selectedOptionId);
381
- if (selectedOption) {
382
- return {
383
- inputValue: selectedOption.label
384
- };
385
- }
386
- // input value is from highlighted option, not user input
387
- if (highlightedOptionId) {
388
- if (inputValue === this.getOption('id', highlightedOptionId).label) {
389
- return {
390
- inputValue: '',
391
- filteredOptions: this.filterOptions('')
392
- };
375
+ var _this$state3;
376
+ let inputNoSeconds = inputValue;
377
+ // if the input contains seconds disregard them (e.g. if format = LTS)
378
+ if (inputValue.length > 5) {
379
+ // e.g. "5:34:"
380
+ const input = this.parseInputText(inputValue);
381
+ if (input.isValid()) {
382
+ input.set({
383
+ second: 0
384
+ });
385
+ inputNoSeconds = input.format(this.props.format);
393
386
  }
394
387
  }
395
- // if input was completely cleared, ensure it stays clear
396
- // e.g. defaultValue defined, but no selection yet made
397
- if (inputValue === '') {
398
- return {
399
- inputValue: ''
400
- };
388
+ if (this.props.allowNonStepInput && inputNoSeconds.length < 3) {
389
+ var _this$state2;
390
+ // could show too many results, show only step values
391
+ return (_this$state2 = this.state) === null || _this$state2 === void 0 ? void 0 : _this$state2.options.filter(option => {
392
+ return option.label.toLowerCase().startsWith(inputNoSeconds.toLowerCase()) && option.value.minute() % this.props.step == 0;
393
+ });
401
394
  }
402
- return void 0;
395
+ return (_this$state3 = this.state) === null || _this$state3 === void 0 ? void 0 : _this$state3.options.filter(option => option.label.toLowerCase().startsWith(inputNoSeconds.toLowerCase()));
403
396
  }
397
+
398
+ // Called when the input is blurred (=when clicking outside, tabbing away),
399
+ // when pressing ESC. NOT called when an item is selected via Enter/click,
400
+ // (but in this case it will be called later when the input is blurred.)
401
+ // Called when an option is selected via mouse click or Enter.
404
402
  renderOptions() {
405
- const _this$state3 = this.state,
406
- filteredOptions = _this$state3.filteredOptions,
407
- highlightedOptionId = _this$state3.highlightedOptionId,
408
- selectedOptionId = _this$state3.selectedOptionId;
403
+ const _this$state4 = this.state,
404
+ filteredOptions = _this$state4.filteredOptions,
405
+ highlightedOptionId = _this$state4.highlightedOptionId,
406
+ selectedOptionId = _this$state4.selectedOptionId;
409
407
  if (filteredOptions.length < 1) {
410
408
  return this.renderEmptyOption();
411
409
  }
@@ -455,9 +453,9 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
455
453
  onKeyDown = _this$props11.onKeyDown,
456
454
  mountNode = _this$props11.mountNode,
457
455
  rest = _objectWithoutProperties(_this$props11, _excluded);
458
- const _this$state4 = this.state,
459
- inputValue = _this$state4.inputValue,
460
- isShowingOptions = _this$state4.isShowingOptions;
456
+ const _this$state5 = this.state,
457
+ inputValue = _this$state5.inputValue,
458
+ isShowingOptions = _this$state5.isShowingOptions;
461
459
  return /*#__PURE__*/React.createElement(Select, Object.assign({
462
460
  mountNode: mountNode,
463
461
  renderLabel: renderLabel,
@@ -482,7 +480,7 @@ let TimeSelect = (_dec = withDeterministicId(), _dec2 = testable(), _dec(_class
482
480
  renderAfterInput: renderAfterInput,
483
481
  isShowingOptions: isShowingOptions,
484
482
  onRequestShowOptions: this.handleShowOptions,
485
- onRequestHideOptions: this.handleHideOptions,
483
+ onRequestHideOptions: this.handleBlurOrEsc,
486
484
  onRequestHighlightOption: this.handleHighlightOption,
487
485
  onRequestSelectOption: this.handleSelectOption,
488
486
  onInputChange: this.handleInputChange,