diff --git a/jest.config.js b/jest.config.js index a9c2977356..3d8666e509 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ module.exports = { functions: 80, lines: 80, }, - './src/components/**/!(ColumnHeaderSelect|FilterHeaderRow|TableToolbar|RowActionsCell|RowActionsError|StatefulTable|StatefulTableDetailWizard|CatalogContent|FileDrop|HeaderMenu|Dashboard|CardRenderer|Attribute|UnitRenderer|ImageHotspots|ImageControls|TimeSeriesCard|PageHero|PageTitle|EditPage|AsyncTable|ImageCard|WizardHeader|HierarchyList|TableHead|ColumnResize).jsx': { + './src/components/**/!(ColumnHeaderSelect|FilterHeaderRow|TableToolbar|RowActionsCell|RowActionsError|StatefulTable|StatefulTableDetailWizard|CatalogContent|FileDrop|HeaderMenu|Dashboard|CardRenderer|Attribute|UnitRenderer|ImageHotspots|ImageControls|TimeSeriesCard|PageHero|PageTitle|EditPage|AsyncTable|ImageCard|WizardHeader|HierarchyList|TableHead|ColumnResize|DateTimePicker).jsx': { statements: 80, branches: 80, functions: 80, @@ -83,6 +83,7 @@ module.exports = { './src/components/Page/PageTitle.jsx': { branches: 75 }, './src/components/ImageCard/ImageCard.jsx': { branches: 76 }, './src/components/Table/TableDetailWizard/StatefulTableDetailWizard.jsx': { branches: 76 }, + './src/components/DateTimePicker/DateTimePicker.jsx': { branches: 67 }, }, globals: { __DEV__: false, diff --git a/src/components/DateTimePicker/DateTimePicker.jsx b/src/components/DateTimePicker/DateTimePicker.jsx new file mode 100644 index 0000000000..2d32fdf338 --- /dev/null +++ b/src/components/DateTimePicker/DateTimePicker.jsx @@ -0,0 +1,887 @@ +/* eslint-disable no-underscore-dangle */ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + Button, + DatePicker, + DatePickerInput, + RadioButtonGroup, + RadioButton, + FormGroup, + Select, + SelectItem, + NumberInput, + TooltipDefinition, + OrderedList, + ListItem, +} from 'carbon-components-react'; +import moment from 'moment'; +import { Calendar16 } from '@carbon/icons-react'; +import classnames from 'classnames'; + +import TimePickerSpinner from '../TimePickerSpinner/TimePickerSpinner'; +import { settings } from '../../constants/Settings'; + +const { iotPrefix } = settings; + +export const PICKER_KINDS = { + PRESET: 'PRESET', + RELATIVE: 'RELATIVE', + ABSOLUTE: 'ABSOLUTE', +}; + +export const PRESET_VALUES = [ + { + label: 'Last 30 minutes', + offset: 30, + }, + { + label: 'Last 1 hour', + offset: 60, + }, + { + label: 'Last 6 hours', + offset: 360, + }, + { + label: 'Last 12 hours', + offset: 720, + }, + { + label: 'Last 24 hours', + offset: 1440, + }, +]; + +export const INTERVAL_VALUES = { + MINUTES: 'MINUTES', + HOURS: 'HOURS', + DAYS: 'DAYS', + WEEKS: 'WEEKS', + MONTHS: 'MONTHS', + YEARS: 'YEARS', +}; +export const RELATIVE_VALUES = { + YESTERDAY: 'YESTERDAY', + TODAY: 'TODAY', +}; + +const propTypes = { + /** default value for the picker */ + defaultValue: PropTypes.shape({ + kind: PropTypes.oneOf([PICKER_KINDS.PRESET, PICKER_KINDS.RELATIVE, PICKER_KINDS.ABSOLUTE]), + preset: PropTypes.shape({ + label: PropTypes.string, + offset: PropTypes.number, + }), + relative: PropTypes.shape({ + lastNumber: PropTypes.number, + lastInterval: PropTypes.string, + relativeToWhen: PropTypes.string, + relativeToTime: PropTypes.string, + }), + absolute: PropTypes.shape({ + startDate: PropTypes.instanceOf(Date), + startTime: PropTypes.string, + endDate: PropTypes.instanceOf(Date), + endTime: PropTypes.string, + }), + }), + /** the moment.js format for the human readable interval value */ + dateTimeMask: PropTypes.string, + /** a list of options to for the default presets */ + presets: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + offset: PropTypes.number, + }) + ), + /** a list of options to put on the 'Last' interval dropdown */ + intervals: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, + }) + ), + /** a list of options to put on the 'Relative to' dropdown */ + relatives: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, + }) + ), + /** show the picker in the expanded state */ + expanded: PropTypes.bool, + /** disable the input */ + disabled: PropTypes.bool, + /** show the relative custom range picker */ + showRelativeOption: PropTypes.bool, + /** triggered on cancel */ + onCancel: PropTypes.func, + /** triggered on apply with this returning object */ + /* + { + kind: String // the type of selection, one of PICKER_KINDS (PRESET, RELATIVE, ABSOLUTE) + preset: { + label: String // the label of the selected preset + offset: Number // the offset in minute + }, + relative: { + start: Date // the start point in time + end: Date // the end point in time + lastNumber: Number // quantity of interval + lastInterval: String // one of INTERVAL_VALUES + relativeToWhen: String // one of RELATIVE_VALUES, indicates to what point in time the selection is relative to + relativeToTime: String // in the HH:MM format + }, + absolute: { + start: Date // the start point in time + end: Date // the end point in time + startDate: String // start date in the mask or default format + startTime: String // in the HH:MM format + endDate: // end date in the mask or default format + endTime: String // in the HH:MM format + }, + } */ + onApply: PropTypes.func, + /** All the labels that need translation */ + i18n: PropTypes.shape({ + toLabel: PropTypes.string, + toNowLabel: PropTypes.string, + calendarLabel: PropTypes.string, + presetLabels: PropTypes.arrayOf(PropTypes.string), + intervalLabels: PropTypes.arrayOf(PropTypes.string), + relativeLabels: PropTypes.arrayOf(PropTypes.string), + customRangeLinkLabel: PropTypes.string, + customRangeLabel: PropTypes.string, + relativeLabel: PropTypes.string, + lastLabel: PropTypes.string, + invalidNumberLabel: PropTypes.string, + relativeToLabel: PropTypes.string, + absoluteLabel: PropTypes.string, + startTimeLabel: PropTypes.string, + endTimeLabel: PropTypes.string, + applyBtnLabel: PropTypes.string, + cancelBtnLabel: PropTypes.string, + backBtnLabel: PropTypes.string, + }), +}; + +const defaultProps = { + defaultValue: null, + dateTimeMask: 'YYYY-MM-DD HH:mm', + presets: PRESET_VALUES, + intervals: [ + { + label: 'minutes', + value: INTERVAL_VALUES.MINUTES, + }, + { + label: 'hours', + value: INTERVAL_VALUES.HOURS, + }, + { + label: 'days', + value: INTERVAL_VALUES.DAYS, + }, + { + label: 'weeks', + value: INTERVAL_VALUES.WEEKS, + }, + { + label: 'months', + value: INTERVAL_VALUES.MONTHS, + }, + { + label: 'years', + value: INTERVAL_VALUES.YEARS, + }, + ], + relatives: [ + { + label: 'Yesterday', + value: RELATIVE_VALUES.YESTERDAY, + }, + { + label: 'Today', + value: RELATIVE_VALUES.TODAY, + }, + { + label: '', + value: '', + }, + ], + expanded: false, + disabled: false, + showRelativeOption: true, + onCancel: null, + onApply: null, + i18n: { + toLabel: 'to', + toNowLabel: 'to Now', + calendarLabel: 'Calendar', + presetLabels: [ + 'Last 30 minutes', + 'Last 1 hour', + 'Last 6 hours', + 'Last 12 hours', + 'Last 24 hours', + ], + intervalLabels: ['minutes', 'hours', 'days', 'weeks', 'months', 'years'], + relativeLabels: ['Yesterday', 'Today'], + customRangeLinkLabel: 'Custom Range', + customRangeLabel: 'Custom range', + relativeLabel: 'Relative', + lastLabel: 'Last', + invalidNumberLabel: 'Number is not valid', + relativeToLabel: 'Relative to', + absoluteLabel: 'Absolute', + startTimeLabel: 'Start time', + endTimeLabel: 'End time', + applyBtnLabel: 'Apply', + cancelBtnLabel: 'Cancel', + backBtnLabel: 'Back', + }, +}; + +const __unstableDateTimePicker = ({ + defaultValue, + dateTimeMask, + presets, + intervals, + relatives, + expanded, + disabled, + showRelativeOption, + onCancel, + onApply, + i18n, + ...others +}) => { + const strings = { + ...defaultProps.i18n, + ...i18n, + }; + + const [isExpanded, setIsExpanded] = useState(expanded); + const [customRangeKind, setCustomRangeKind] = useState( + showRelativeOption ? PICKER_KINDS.RELATIVE : PICKER_KINDS.ABSOLUTE + ); + + const [isCustomRange, setIsCustomRange] = useState(false); + + const datePickerRef = React.createRef(); + + const [selectedPreset, setSelectedPreset] = useState(null); + + const [currentValue, setCurrentValue] = useState(null); + const [lastAppliedValue, setLastAppliedValue] = useState(null); + const [humanValue, setHumanValue] = useState(null); + + const [relativeValue, setRelativeValue] = useState(null); + const [absoluteValue, setAbsoluteValue] = useState(null); + + const [focusOnFirstField, setFocusOnFirstField] = useState(true); + + const dateTimePickerBaseValue = { + kind: '', + preset: { + label: null, + offset: null, + }, + relative: { + lastNumber: null, + lastInterval: null, + relativeToWhen: null, + relativeToTime: null, + }, + absolute: { + startDate: null, + startTime: null, + endDate: null, + endTime: null, + }, + }; + + useEffect(() => { + window.setTimeout(() => { + if (datePickerRef && datePickerRef.current) { + datePickerRef.current.cal.open(); + // while waiting for https://github.com/carbon-design-system/carbon/issues/5713 + // the only way to display the calendar inline is to reparent its DOM to our component + const wrapper = document.getElementById(`${iotPrefix}--date-time-picker__wrapper`); + if (typeof wrapper !== 'undefined' && wrapper !== null) { + const dp = document + .getElementById(`${iotPrefix}--date-time-picker__wrapper`) + .getElementsByClassName(`${iotPrefix}--date-time-picker__datepicker`)[0]; + dp.appendChild(datePickerRef.current.cal.calendarContainer); + } + } + }, 0); + }); + + /** + * Parses a value object into a human readable value + * @param {Object} value - the currently selected value + * @param {string} value.kind - preset/relative/absolute + * @param {Object} value.preset - the preset selection + * @param {Object} value.relative - the relative time selection + * @param {Object} value.absolute - the absolute time selection + * @returns {Object} a human readable value and a furtherly augmented value object + */ + const parseValue = value => { + setCurrentValue(value); + let readableValue = ''; + const returnValue = { ...value }; + switch (value.kind) { + case PICKER_KINDS.RELATIVE: { + let endDate = moment(); + if (value.relative.relativeToWhen !== '') { + endDate = + value.relative.relativeToWhen === RELATIVE_VALUES.YESTERDAY + ? moment().add(-1, INTERVAL_VALUES.DAYS) + : moment(); + if (value.relative.relativeToTime) { + endDate.hours(value.relative.relativeToTime.split(':')[0]); + endDate.minutes(value.relative.relativeToTime.split(':')[1]); + } + } + const startDate = endDate + .clone() + .subtract( + value.relative.lastNumber, + value.relative.lastInterval ? value.relative.lastInterval : INTERVAL_VALUES.MINUTES + ); + returnValue.relative.start = new Date(startDate.valueOf()); + returnValue.relative.end = new Date(endDate.valueOf()); + readableValue = `${moment(startDate).format(dateTimeMask)} ${strings.toLabel} ${moment( + endDate + ).format(dateTimeMask)}`; + break; + } + case PICKER_KINDS.ABSOLUTE: { + const startDate = moment(value.absolute.start); + if (value.absolute.startTime) { + startDate.hours(value.absolute.startTime.split(':')[0]); + startDate.minutes(value.absolute.startTime.split(':')[1]); + } + returnValue.absolute.start = new Date(startDate.valueOf()); + const endDate = moment(value.absolute.end); + if (value.absolute.endTime) { + endDate.hours(value.absolute.endTime.split(':')[0]); + endDate.minutes(value.absolute.endTime.split(':')[1]); + } + returnValue.absolute.end = new Date(endDate.valueOf()); + readableValue = `${moment(startDate).format(dateTimeMask)} ${strings.toLabel} ${moment( + endDate + ).format(dateTimeMask)}`; + break; + } + default: + readableValue = value.preset.label; + break; + } + setHumanValue(readableValue); + return { readableValue, ...returnValue }; + }; + + /** + * Transforms a default or selected value into a full blown returnable object + * @param {Object} [preset] clicked preset + * @param {string} preset.label preset label + * @param {number} preset.offset preset offset in minutes + * @returns {Object} the augmented value itself and the human readable value + */ + const renderValue = (clickedPreset = null) => { + const value = { ...dateTimePickerBaseValue }; + if (isCustomRange) { + if (customRangeKind === PICKER_KINDS.RELATIVE) { + value.relative = relativeValue; + } else { + value.absolute = absoluteValue; + } + value.kind = customRangeKind; + } else { + const preset = presets + .filter(p => p.offset === (clickedPreset ? clickedPreset.offset : selectedPreset)) + .pop(); + value.preset = preset; + value.kind = PICKER_KINDS.PRESET; + } + return { + ...value, + ...parseValue(value), + }; + }; + + useEffect( + () => { + if (absoluteValue || relativeValue) { + renderValue(); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [absoluteValue, relativeValue] + ); + + const onFieldClick = () => { + setIsExpanded(!isExpanded); + }; + + useEffect( + () => { + if ( + datePickerRef.current && + datePickerRef.current.inputField && + datePickerRef.current.toInputField + ) { + if (focusOnFirstField) { + datePickerRef.current.inputField.focus(); + } else { + datePickerRef.current.toInputField.focus(); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [focusOnFirstField] + ); + + const onDatePickerChange = range => { + if (range.length > 1) { + setFocusOnFirstField(!focusOnFirstField); + } + + const newAbsolute = { ...absoluteValue }; + [newAbsolute.start] = range; + newAbsolute.startDate = moment(newAbsolute.start).format('MM/DD/YYYY'); + newAbsolute.end = range[range.length - 1]; + newAbsolute.endDate = moment(newAbsolute.end).format('MM/DD/YYYY'); + setAbsoluteValue(newAbsolute); + }; + + const onDatePickerClose = (range, single, flatpickr) => { + // force it to stay open + if (flatpickr) { + flatpickr.open(); + } + }; + + const onCustomRangeChange = kind => { + setCustomRangeKind(kind); + }; + + const toggleIsCustomRange = () => { + setIsCustomRange(!isCustomRange); + setSelectedPreset(null); + }; + + const onPresetClick = preset => { + setSelectedPreset(preset.offset); + renderValue(preset); + }; + + const resetRelativeValue = () => { + setRelativeValue({ + lastNumber: 0, + lastInterval: intervals[0].value, + relativeToWhen: '', + relativeToTime: '', + }); + }; + + const resetAbsoluteValue = () => { + setAbsoluteValue({ + startDate: '', + startTime: '00:00', + endDate: '', + endTime: '00:00', + }); + }; + + const parseDefaultValue = () => { + const parsableValue = lastAppliedValue || defaultValue; + + if (parsableValue !== null) { + if (parsableValue.hasOwnProperty('offset')) { + // preset + resetAbsoluteValue(); + resetRelativeValue(); + setCustomRangeKind(PICKER_KINDS.RELATIVE); + onPresetClick(parsableValue); + } + if (parsableValue.hasOwnProperty('lastNumber')) { + // relative + resetAbsoluteValue(); + setIsCustomRange(true); + setCustomRangeKind(PICKER_KINDS.RELATIVE); + setRelativeValue(parsableValue); + } + + if (parsableValue.hasOwnProperty('startDate')) { + // absolute + const absolute = { ...parsableValue }; + resetRelativeValue(); + setIsCustomRange(true); + setCustomRangeKind(PICKER_KINDS.ABSOLUTE); + if (!absolute.hasOwnProperty('start')) { + absolute.start = moment(absolute.startDate).valueOf(); + } + if (!absolute.hasOwnProperty('end')) { + absolute.end = moment(absolute.endDate).valueOf(); + } + setAbsoluteValue(absolute); + } + } else { + resetAbsoluteValue(); + resetRelativeValue(); + setCustomRangeKind(PICKER_KINDS.RELATIVE); + onPresetClick(presets[0]); + } + }; + + useEffect( + () => { + if (defaultValue || humanValue === null) { + parseDefaultValue(); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [defaultValue] + ); + + const onCancelClick = () => { + setIsExpanded(false); + parseDefaultValue(); + + if (onCancel) { + onCancel(); + } + }; + + const onApplyClick = () => { + setIsExpanded(false); + const value = renderValue(); + + switch (value.kind) { + case PICKER_KINDS.ABSOLUTE: + setLastAppliedValue(value.absolute); + break; + case PICKER_KINDS.RELATIVE: + setLastAppliedValue(value.relative); + break; + default: + setLastAppliedValue(value.preset); + break; + } + + if (onApply) { + onApply(value); + } + }; + + /** + * Get an alternative human readable value for a preset to show in tooltips and dropdown + * ie. 'Last 30 minutes' displays '2020-04-01 11:30 to Now' on the tooltip + * @returns {string} an interval string, starting point in time to now + */ + const getIntervalValue = () => { + if (currentValue) { + if (currentValue.kind === PICKER_KINDS.PRESET) { + return `${moment() + .subtract(currentValue.preset.offset, 'minutes') + .format(dateTimeMask)} ${strings.toNowLabel}`; + } + } + return ''; + }; + + const changeRelativePropertyValue = (property, value) => { + const newRelative = { ...relativeValue }; + newRelative[property] = value; + setRelativeValue(newRelative); + }; + + const onRelativeLastNumberChange = event => { + changeRelativePropertyValue('lastNumber', Number(event.imaginaryTarget.value)); + }; + + const onRelativeLastIntervalChange = event => { + changeRelativePropertyValue('lastInterval', event.currentTarget.value); + }; + + const onRelativeToWhenChange = event => { + changeRelativePropertyValue('relativeToWhen', event.currentTarget.value); + }; + + const onRelativeToTimeChange = pickerValue => { + changeRelativePropertyValue('relativeToTime', pickerValue); + }; + + const changeAbsolutePropertyValue = (property, value) => { + const newAbsolute = { ...absoluteValue }; + newAbsolute[property] = value; + setAbsoluteValue(newAbsolute); + }; + + const onAbsoluteStartTimeChange = pickerValue => { + changeAbsolutePropertyValue('startTime', pickerValue); + }; + + const onAbsoluteEndTimeChange = pickerValue => { + changeAbsolutePropertyValue('endTime', pickerValue); + }; + + return ( +
+
+
+ {isExpanded || (currentValue && currentValue.kind !== PICKER_KINDS.PRESET) ? ( + {humanValue} + ) : humanValue ? ( + + {humanValue} + + ) : null} + +
+
+
+ {!isCustomRange ? ( + + {getIntervalValue() ? ( + + {getIntervalValue()} + + ) : null} + + {strings.customRangeLinkLabel} + + {presets.map((preset, i) => { + return ( + onPresetClick(preset)} + className={classnames( + `${iotPrefix}--date-time-picker__listitem ${iotPrefix}--date-time-picker__listitem--preset`, + { + [`${iotPrefix}--date-time-picker__listitem--preset-selected`]: + selectedPreset === preset.offset, + } + )} + > + {strings.presetLabels[i] || preset.label} + + ); + })} + + ) : ( +
+ {showRelativeOption ? ( + + + + + + + ) : null} + {customRangeKind === PICKER_KINDS.RELATIVE ? ( +
+ +
+ + +
+
+ +
+ + +
+
+
+ ) : ( +
+
+ + + + +
+ +
+ + +
+
+
+ )} +
+ )} +
+
+ {isCustomRange ? ( + + ) : ( + + )} + +
+
+
+
+ ); +}; + +__unstableDateTimePicker.propTypes = propTypes; +__unstableDateTimePicker.defaultProps = defaultProps; + +export default __unstableDateTimePicker; diff --git a/src/components/DateTimePicker/DateTimePicker.story.jsx b/src/components/DateTimePicker/DateTimePicker.story.jsx new file mode 100644 index 0000000000..a82a2788fd --- /dev/null +++ b/src/components/DateTimePicker/DateTimePicker.story.jsx @@ -0,0 +1,103 @@ +/* eslint-disable react/jsx-pascal-case */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { text, select } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; + +import { CARD_SIZES } from '../../constants/LayoutConstants'; +import { getCardMinSize } from '../../utils/componentUtilityFunctions'; + +// eslint-disable-next-line no-unused-vars +import __unstableDateTimePicker, { INTERVAL_VALUES, RELATIVE_VALUES } from './DateTimePicker'; + +const customPresets = [ + { + label: 'Last 30 minutes', + offset: 30, + }, + { + label: 'Last 1 hour', + offset: 60, + }, + { + label: 'Last 6 hours', + offset: 360, + }, + { + label: 'Last 12 hours', + offset: 720, + }, +]; + +const defaultRelativeValue = { + lastNumber: 20, + lastInterval: INTERVAL_VALUES.MINUTES, + relativeToWhen: RELATIVE_VALUES.TODAY, + relativeToTime: '13:30', +}; + +const defaultAbsoluteValue = { + startDate: '04/01/2020', + startTime: '12:34', + endDate: '04/06/2020', + endTime: '10:49', +}; + +storiesOf('Watson IoT Experimental/DateTime Picker', module) + .add('Default', () => { + const size = select('size', Object.keys(CARD_SIZES), CARD_SIZES.MEDIUMWIDE); + return ( +
+ <__unstableDateTimePicker dateTimeMask={text('dateTimeMask', 'YYYY-MM-DD HH:mm')} /> +
+ ); + }) + .add('Selected preset', () => { + const size = select('size', Object.keys(CARD_SIZES), CARD_SIZES.MEDIUMWIDE); + return ( +
+ <__unstableDateTimePicker + defaultValue={customPresets[3]} + onApply={action('onApply')} + onCancel={action('onCancel')} + /> +
+ ); + }) + .add('Selected relative', () => { + const size = select('size', Object.keys(CARD_SIZES), CARD_SIZES.MEDIUMWIDE); + return ( +
+ <__unstableDateTimePicker + defaultValue={defaultRelativeValue} + onApply={action('onApply')} + onCancel={action('onCancel')} + /> +
+ ); + }) + .add('Selected absolute', () => { + const size = select('size', Object.keys(CARD_SIZES), CARD_SIZES.MEDIUMWIDE); + return ( +
+ <__unstableDateTimePicker + defaultValue={defaultAbsoluteValue} + onApply={action('onApply')} + onCancel={action('onCancel')} + /> +
+ ); + }) + .add('Without a relative option', () => { + const size = select('size', Object.keys(CARD_SIZES), CARD_SIZES.MEDIUMWIDE); + return ( +
+ <__unstableDateTimePicker + defaultValue={defaultAbsoluteValue} + showRelativeOption={false} + onApply={action('onApply')} + onCancel={action('onCancel')} + /> +
+ ); + }); diff --git a/src/components/DateTimePicker/DateTimePicker.test.jsx b/src/components/DateTimePicker/DateTimePicker.test.jsx new file mode 100644 index 0000000000..e100ebbfd3 --- /dev/null +++ b/src/components/DateTimePicker/DateTimePicker.test.jsx @@ -0,0 +1,215 @@ +/* eslint-disable react/jsx-pascal-case */ +import React from 'react'; +import { mount } from 'enzyme'; +import moment from 'moment'; + +import __unstableDateTimePicker, { + INTERVAL_VALUES, + RELATIVE_VALUES, + PRESET_VALUES, + PICKER_KINDS, +} from './DateTimePicker'; + +const dateTimePickerProps = { + id: 'datetimepicker', + onCancel: jest.fn(), + onApply: jest.fn(), +}; + +const i18n = { + presetLabels: ['Last 30 minutes', 'Missed in translation'], + intervalLabels: ['minutes', 'Missed in translation'], + relativeLabels: ['Missed in translation'], +}; + +const defaultRelativeValue = { + lastNumber: 20, + lastInterval: INTERVAL_VALUES.MINUTES, + relativeToWhen: RELATIVE_VALUES.TODAY, + relativeToTime: '13:30', +}; + +const defaultAbsoluteValue = { + startDate: '04/01/2020', + startTime: '12:34', + endDate: '04/06/2020', + endTime: '10:49', +}; + +describe('DateTimePicker tests', () => { + jest.useFakeTimers(); + + test('it should have the first preset as value', () => { + const wrapper = mount(<__unstableDateTimePicker {...dateTimePickerProps} i18n={i18n} />); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + expect(wrapper.find('.bx--tooltip__trigger').text()).toEqual(PRESET_VALUES[0].label); + }); + + test('onApply should be called', () => { + const wrapper = mount(<__unstableDateTimePicker {...dateTimePickerProps} />); + wrapper + .find('.iot--date-time-picker__menu-btn-apply') + .first() + .simulate('click'); + expect(dateTimePickerProps.onApply).toHaveBeenCalled(); + }); + + test('onCancel should be called', () => { + const wrapper = mount(<__unstableDateTimePicker {...dateTimePickerProps} />); + wrapper + .find('.iot--date-time-picker__menu-btn-cancel') + .first() + .simulate('click'); + expect(dateTimePickerProps.onCancel).toHaveBeenCalled(); + }); + + test('it should render with a predefined preset', () => { + const wrapper = mount( + <__unstableDateTimePicker {...dateTimePickerProps} defaultValue={PRESET_VALUES[1]} /> + ); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + expect(wrapper.find('.bx--tooltip__trigger').text()).toEqual(PRESET_VALUES[1].label); + }); + + test('it should render with a predefined relative range', () => { + const wrapper = mount( + <__unstableDateTimePicker {...dateTimePickerProps} defaultValue={defaultRelativeValue} /> + ); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + + wrapper + .find('.bx--select-input') + .first() + .simulate('change', { target: { value: INTERVAL_VALUES.DAYS } }); + + wrapper + .find('.bx--select-input') + .at(1) + .simulate('change', { target: { value: RELATIVE_VALUES.YESTERDAY } }); + + const today = moment(); + expect( + wrapper + .find('.iot--date-time-picker__field') + .first() + .text() + ).toEqual(`${today.format('YYYY-MM-DD')} 13:10 to ${today.format('YYYY-MM-DD')} 13:30`); + + wrapper + .find('.bx--number__control-btn.up-icon') + .first() + .simulate('click'); + + expect( + wrapper + .find('.iot--date-time-picker__field') + .first() + .text() + ).toEqual(`${today.format('YYYY-MM-DD')} 13:09 to ${today.format('YYYY-MM-DD')} 13:30`); + + wrapper + .find('.iot--time-picker__controls--btn.up-icon') + .first() + .simulate('click'); + + expect( + wrapper + .find('.iot--date-time-picker__field') + .first() + .text() + ).toEqual(`${today.format('YYYY-MM-DD')} 14:09 to ${today.format('YYYY-MM-DD')} 14:30`); + + wrapper + .find('.iot--date-time-picker__menu-btn-apply') + .first() + .simulate('click'); + expect(dateTimePickerProps.onApply).toHaveBeenCalled(); + }); + + test('it should render with a predefined absolute range', () => { + const wrapper = mount( + <__unstableDateTimePicker {...dateTimePickerProps} defaultValue={defaultAbsoluteValue} /> + ); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + expect( + wrapper + .find('.iot--date-time-picker__field') + .first() + .text() + ).toEqual('2020-04-01 12:34 to 2020-04-06 10:49'); + + wrapper + .find('.iot--time-picker__controls--btn.up-icon') + .first() + .simulate('click'); + + expect( + wrapper + .find('.iot--date-time-picker__field') + .first() + .text() + ).toEqual('2020-04-01 13:34 to 2020-04-06 10:49'); + + wrapper + .find('.iot--time-picker__controls--btn.up-icon') + .at(1) + .simulate('click'); + + expect( + wrapper + .find('.iot--date-time-picker__field') + .first() + .text() + ).toEqual('2020-04-01 13:34 to 2020-04-06 11:49'); + + wrapper + .find('.iot--date-time-picker__menu-btn-apply') + .first() + .simulate('click'); + expect(dateTimePickerProps.onApply).toHaveBeenCalled(); + }); + + test('it should switch from relative to absolute', () => { + const wrapper = mount( + <__unstableDateTimePicker {...dateTimePickerProps} defaultValue={defaultRelativeValue} /> + ); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + + wrapper + .find('.bx--radio-button') + .at(1) + .simulate('change', { target: { value: PICKER_KINDS.ABSOLUTE } }); + + expect(wrapper.find('.iot--time-picker__controls--btn')).toHaveLength(4); + }); + + test('it should not show the relative option', () => { + const wrapper = mount( + <__unstableDateTimePicker + {...dateTimePickerProps} + defaultValue={defaultAbsoluteValue} + showRelativeOption={false} + /> + ); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + expect(wrapper.find('.bx--radio-button')).toHaveLength(0); + }); + + test('it should switch from relative to presets', () => { + const wrapper = mount( + <__unstableDateTimePicker {...dateTimePickerProps} defaultValue={defaultRelativeValue} /> + ); + expect(wrapper.find('.iot--date-time-picker__field')).toHaveLength(1); + wrapper + .find('.iot--date-time-picker__field') + .first() + .simulate('click'); + + wrapper + .find('.iot--date-time-picker__menu-btn-back') + .first() + .simulate('click'); + + expect(wrapper.find('.iot--time-picker__controls--btn')).toHaveLength(0); + }); +}); diff --git a/src/components/DateTimePicker/_date-time-picker.scss b/src/components/DateTimePicker/_date-time-picker.scss new file mode 100644 index 0000000000..cc54fb1569 --- /dev/null +++ b/src/components/DateTimePicker/_date-time-picker.scss @@ -0,0 +1,221 @@ +@import '../../globals/vars'; +@import '~@carbon/motion/scss/motion.scss'; + +.#{$iot-prefix}--date-time-picker { + padding: 0 $spacing-05 $spacing-05; + position: absolute; + width: 100%; + height: 100%; +} + +.#{$iot-prefix}--date-time-picker__wrapper { + width: 20rem; + + .#{$prefix}--tooltip__trigger.#{$prefix}--tooltip__trigger--definition { + font-size: 100%; + border-bottom: none; + letter-spacing: unset; + } + + .#{$prefix}--date-picker--range { + position: absolute; + } + + .#{$prefix}--date-picker-container { + opacity: 0; + } + + .flatpickr-calendar.open { + padding-bottom: 0; + position: unset !important; + top: unset !important; + left: unset !important; + box-shadow: none; + margin-left: auto; + margin-right: auto; + -webkit-animation: none; + animation: none; + } + + .#{$prefix}--number { + margin-right: $spacing-05; + .#{$prefix}--number__input-wrapper { + input { + min-width: 8.5rem; + } + } + } + + .#{$prefix}--select-input { + width: 8.5rem; + } + + .#{$iot-prefix}--time-picker__wrapper { + &.#{$iot-prefix}--time-picker__wrapper--with-spinner:first-of-type { + margin-right: $spacing-05; + } + &.#{$iot-prefix}--time-picker__wrapper--with-spinner { + .bx--time-picker__input-field { + width: 8.5rem; + padding-right: $spacing-07; + } + } + .#{$iot-prefix}--time-picker__controls { + left: 6.7rem; + } + } + + .#{$iot-prefix}--date-time-picker__box { + outline-offset: -0.125rem; + position: relative; + list-style: none; + display: block; + background-color: $ui-01; + border: none; + border-bottom: 1px solid $carbon--gray-50; + width: 100%; + height: $spacing-08; + cursor: pointer; + color: $carbon--gray-100; + outline: 0.125rem solid transparent; + transition: background-color $duration--fast-01 carbon--motion(standard); + .#{$iot-prefix}--date-time-picker__field { + background: none; + appearance: none; + border: 0; + width: 100%; + position: relative; + display: inline-flex; + align-items: center; + vertical-align: top; + height: calc(100% + 1px); + padding: 0 $spacing-09 0 $spacing-05; + cursor: pointer; + outline: none; + white-space: nowrap; + & > span { + overflow: hidden; + text-overflow: ellipsis; + } + .#{$iot-prefix}--date-time-picker__icon { + position: absolute; + top: 0; + right: $spacing-05; + bottom: 0; + height: 100%; + transition: transform $duration--fast-01 carbon--motion(standard); + cursor: pointer; + } + } + .#{$iot-prefix}--date-time-picker__menu { + display: none; + cursor: default; + box-shadow: 0 0.1875rem 0.1875rem 0 rgba(0, 0, 0, 0.1); + position: absolute; + left: 0; + right: 0; + width: 100%; + background-color: $carbon--white-0; + z-index: 9100; + &.#{$iot-prefix}--date-time-picker__menu-expanded { + display: block; + } + + .#{$iot-prefix}--date-time-picker__menu-scroll { + overflow-y: auto; + padding: $spacing-04 0; + + .#{$prefix}--fieldset { + margin-bottom: $spacing-lg; + padding: 0 $spacing-baseline; + } + + .#{$iot-prefix}--date-time-picker__fields-wrapper { + display: flex; + align-items: flex-end; + .#{$prefix}--label { + margin-bottom: 0; + } + } + + &.#{$iot-prefix}--date-time-picker__menu-formgroup { + display: block; + } + + .#{$iot-prefix}--date-time-picker__listitem { + padding: $spacing-04 $spacing-baseline; + cursor: pointer; + border-left: 2px solid transparent; + } + + .#{$iot-prefix}--date-time-picker__listitem--preset { + &:hover { + background-color: $carbon--gray-20; + } + &.#{$iot-prefix}--date-time-picker__listitem--preset-selected { + background-color: $carbon--gray-20; + border-left-color: $carbon--blue-60; + } + } + + .#{$iot-prefix}--date-time-picker__listitem--current { + color: $carbon--gray-60; + cursor: default; + } + + .#{$iot-prefix}--date-time-picker__listitem--custom { + color: $carbon--blue-60; + } + } + + .#{$iot-prefix}--date-time-picker__menu-btn-set { + display: flex; + + .#{$iot-prefix}--date-time-picker__menu-btn { + flex-grow: 1; + } + } + } + } +} + +html[dir='rtl'] { + .#{$iot-prefix}--date-time-picker__wrapper { + .#{$prefix}--number { + margin-right: unset; + margin-left: $spacing-05; + } + + .#{$iot-prefix}--time-picker__wrapper { + &.#{$iot-prefix}--time-picker__wrapper--with-spinner:first-of-type { + margin-right: unset; + margin-left: $spacing-05; + } + } + + .#{$iot-prefix}--date-time-picker__box { + .#{$iot-prefix}--date-time-picker__field { + padding-right: $spacing-05; + padding-left: $spacing-09; + .#{$iot-prefix}--date-time-picker__icon { + right: unset; + left: $spacing-05; + } + } + .#{$iot-prefix}--date-time-picker__menu { + .#{$iot-prefix}--date-time-picker__menu-scroll { + .#{$iot-prefix}--date-time-picker__listitem { + border-right: 2px solid transparent; + border-left: none; + } + + .#{$iot-prefix}--date-time-picker__listitem--preset { + &.#{$iot-prefix}--date-time-picker__listitem--preset-selected { + border-right-color: $carbon--blue-60; + } + } + } + } + } + } +} diff --git a/src/components/RadioButton/_radio-button.scss b/src/components/RadioButton/_radio-button.scss index 19253c3c0e..7fce7595b1 100644 --- a/src/components/RadioButton/_radio-button.scss +++ b/src/components/RadioButton/_radio-button.scss @@ -1 +1,13 @@ @import '~carbon-components/scss/components/radio-button/radio-button'; +@import '../../globals/vars'; + +html[dir='rtl'] { + .#{$prefix}--radio-button-wrapper:not(:last-of-type) { + margin-left: $spacing-05; + margin-right: unset; + } + .#{$prefix}--radio-button__appearance { + margin-left: $spacing-03; + margin-right: unset; + } +} diff --git a/src/components/Table/TableBody/TableBodyRow/TableBodyRow.story.jsx b/src/components/Table/TableBody/TableBodyRow/TableBodyRow.story.jsx index 20d13b2ab0..93f30fa6f4 100644 --- a/src/components/Table/TableBody/TableBodyRow/TableBodyRow.story.jsx +++ b/src/components/Table/TableBody/TableBodyRow/TableBodyRow.story.jsx @@ -54,11 +54,23 @@ storiesOf('Watson IoT/TableBodyRow', module) isExpanded={boolean('isExpanded', false)} rowActions={[ { id: 'add', renderIcon: Add32 }, - { id: 'edit', renderIcon: Edit16, isOverflow: true, labelText: 'Edit', }, - { id: 'test1', renderIcon: Stop16, isOverflow: true, labelText: 'Test 1', hasDivider: true}, - { id: 'test2', renderIcon: Stop16, isOverflow: true, labelText: 'Test 2'}, - { id: 'test3', renderIcon: Stop16, isOverflow: true, labelText: 'Test 3'}, - { id: 'delete', renderIcon: Delete16, isOverflow: true, labelText: 'Delete', isDelete: true}, + { id: 'edit', renderIcon: Edit16, isOverflow: true, labelText: 'Edit' }, + { + id: 'test1', + renderIcon: Stop16, + isOverflow: true, + labelText: 'Test 1', + hasDivider: true, + }, + { id: 'test2', renderIcon: Stop16, isOverflow: true, labelText: 'Test 2' }, + { id: 'test3', renderIcon: Stop16, isOverflow: true, labelText: 'Test 3' }, + { + id: 'delete', + renderIcon: Delete16, + isOverflow: true, + labelText: 'Delete', + isDelete: true, + }, ]} options={{ ...tableBodyRowProps.options, hasRowActions: true, hasRowExpansion: true }} /> diff --git a/src/components/TimePickerSpinner/TimePickerSpinner.jsx b/src/components/TimePickerSpinner/TimePickerSpinner.jsx index d3abf175a4..8238ba3644 100644 --- a/src/components/TimePickerSpinner/TimePickerSpinner.jsx +++ b/src/components/TimePickerSpinner/TimePickerSpinner.jsx @@ -73,10 +73,6 @@ const TimePickerSpinner = ({ timeGroups.push('00'); } let groupValue = Number(timeGroups[currentTimeGroup]); - if (Number.isNaN(groupValue)) { - groupValue = 0; - } - const maxForGroup = currentTimeGroup === 0 ? (is12hour ? 12 : 23) : 59; if (direction === 'down') { @@ -86,7 +82,11 @@ const TimePickerSpinner = ({ } timeGroups[currentTimeGroup] = groupValue.toString().padStart(2, '0'); - setPickerValue(timeGroups.join(':')); + const newValue = timeGroups.join(':'); + setPickerValue(newValue); + if (onChange) { + onChange(newValue); + } window.setTimeout(() => { if (focusTarget) { focusTarget.selectionStart = keyUpOrDownPosition; @@ -106,9 +106,12 @@ const TimePickerSpinner = ({ }; const onInputChange = e => { - setPickerValue(e.currentTarget.value); + const { + currentTarget: { value: currentValue }, + } = e; + setPickerValue(currentValue); if (onChange) { - onChange(e); + onChange(currentValue, e); } }; @@ -125,11 +128,33 @@ const TimePickerSpinner = ({ } }; + const onInputBlur = e => { + const target = e.currentTarget; + const regex = /[^\d:]/g; + if (target.value.search(regex) > -1) { + setPickerValue(target.value.replace(regex, '')); + } + }; + + let lastSelectionStart = -1; const onInputKeyUp = e => { switch (e.keyCode) { case keyCodes.LEFT: case keyCodes.RIGHT: setCurrentTimeGroup(e.currentTarget.selectionStart <= 2 ? 0 : 1); + + // this is to fix the event hijacking from sibling components, ie. DatePicker + // in this case we need to set the proper cursor position artificially + if (e.currentTarget.selectionStart === lastSelectionStart) { + if (e.keyCode === keyCodes.LEFT) { + e.currentTarget.selectionStart -= 1; + } else { + e.currentTarget.selectionStart += 1; + } + e.currentTarget.selectionEnd = e.currentTarget.selectionStart; + } + lastSelectionStart = e.currentTarget.selectionStart; + break; case keyCodes.UP: handleArrowClick('up'); @@ -182,6 +207,7 @@ const TimePickerSpinner = ({ value={pickerValue} onKeyDown={onInputKeyDown} onKeyUp={onInputKeyUp} + onBlur={onInputBlur} disabled={disabled} {...others} > diff --git a/src/components/TimePickerSpinner/TimePickerSpinner.test.jsx b/src/components/TimePickerSpinner/TimePickerSpinner.test.jsx index 99b6cbe37f..1d568deb26 100644 --- a/src/components/TimePickerSpinner/TimePickerSpinner.test.jsx +++ b/src/components/TimePickerSpinner/TimePickerSpinner.test.jsx @@ -11,7 +11,7 @@ const timePickerProps = { onChange: jest.fn(), }; -describe('TimePicker tests', () => { +describe('TimePickerSpinner tests', () => { jest.useFakeTimers(); test('show/hide spinner', () => { @@ -39,6 +39,7 @@ describe('TimePicker tests', () => { wrapper.find('input').simulate('focus'); wrapper.find('input').simulate('keyup', { keyCode: keyCodes.LEFT }); + wrapper.find('input').simulate('keyup', { keyCode: keyCodes.RIGHT }); wrapper.find('input').simulate('keyup', { keyCode: keyCodes.UP }); wrapper.find('input').simulate('keyup', { keyCode: keyCodes.ESC }); expect(wrapper.find('input').props().value).toEqual('01:00'); @@ -56,6 +57,10 @@ describe('TimePicker tests', () => { test('work with strings', () => { const wrapper = mount(); + wrapper.find('input').simulate('blur'); + + expect(wrapper.find('input').props().value).toEqual(''); + wrapper .find('.iot--time-picker__controls--btn.down-icon') .first() diff --git a/src/components/TimePickerSpinner/_time-picker-spinner.scss b/src/components/TimePickerSpinner/_time-picker-spinner.scss index 9267f802ca..56d9011749 100644 --- a/src/components/TimePickerSpinner/_time-picker-spinner.scss +++ b/src/components/TimePickerSpinner/_time-picker-spinner.scss @@ -37,9 +37,7 @@ flex-direction: column; justify-content: center; align-items: center; - // vertically center controls within parent container on IE11 - top: 50%; - transform: translateY(-50%); + bottom: 2px; .#{$iot-prefix}--time-picker__controls--btn { border: none; diff --git a/src/index.js b/src/index.js index fd502f8c37..4a7aff2651 100755 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,7 @@ export HierarchyList from './components/List/HierarchyList'; export BarChartCard from './components/BarChartCard/BarChartCard'; export TileCatalogNew from './components/TileCatalogNew/TileCatalogNew'; export TimePickerSpinner from './components/TimePickerSpinner/TimePickerSpinner'; +export __unstableDateTimePicker from './components/DateTimePicker/DateTimePicker'; // reusable reducers export { baseTableReducer } from './components/Table/baseTableReducer'; diff --git a/src/styles.scss b/src/styles.scss index db0b87baca..a594bed4da 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -246,4 +246,5 @@ $deprecations--message: 'Deprecated code was found, this code will be removed be @import 'components/ValueCard/value-card'; @import 'components/TimePickerSpinner/time-picker-spinner'; @import 'components/WizardModal/wizard-modal'; +@import 'components/DateTimePicker/date-time-picker'; @import 'components/IconSwitch/icon-switch'; diff --git a/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap b/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap index 614430c4e8..3e3cad8dad 100644 --- a/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap +++ b/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap @@ -4307,6 +4307,339 @@ Map { }, }, }, + "__unstableDateTimePicker" => Object { + "defaultProps": Object { + "dateTimeMask": "YYYY-MM-DD HH:mm", + "defaultValue": null, + "disabled": false, + "expanded": false, + "i18n": Object { + "absoluteLabel": "Absolute", + "applyBtnLabel": "Apply", + "backBtnLabel": "Back", + "calendarLabel": "Calendar", + "cancelBtnLabel": "Cancel", + "customRangeLabel": "Custom range", + "customRangeLinkLabel": "Custom Range", + "endTimeLabel": "End time", + "intervalLabels": Array [ + "minutes", + "hours", + "days", + "weeks", + "months", + "years", + ], + "invalidNumberLabel": "Number is not valid", + "lastLabel": "Last", + "presetLabels": Array [ + "Last 30 minutes", + "Last 1 hour", + "Last 6 hours", + "Last 12 hours", + "Last 24 hours", + ], + "relativeLabel": "Relative", + "relativeLabels": Array [ + "Yesterday", + "Today", + ], + "relativeToLabel": "Relative to", + "startTimeLabel": "Start time", + "toLabel": "to", + "toNowLabel": "to Now", + }, + "intervals": Array [ + Object { + "label": "minutes", + "value": "MINUTES", + }, + Object { + "label": "hours", + "value": "HOURS", + }, + Object { + "label": "days", + "value": "DAYS", + }, + Object { + "label": "weeks", + "value": "WEEKS", + }, + Object { + "label": "months", + "value": "MONTHS", + }, + Object { + "label": "years", + "value": "YEARS", + }, + ], + "onApply": null, + "onCancel": null, + "presets": Array [ + Object { + "label": "Last 30 minutes", + "offset": 30, + }, + Object { + "label": "Last 1 hour", + "offset": 60, + }, + Object { + "label": "Last 6 hours", + "offset": 360, + }, + Object { + "label": "Last 12 hours", + "offset": 720, + }, + Object { + "label": "Last 24 hours", + "offset": 1440, + }, + ], + "relatives": Array [ + Object { + "label": "Yesterday", + "value": "YESTERDAY", + }, + Object { + "label": "Today", + "value": "TODAY", + }, + Object { + "label": "", + "value": "", + }, + ], + "showRelativeOption": true, + }, + "propTypes": Object { + "dateTimeMask": Object { + "type": "string", + }, + "defaultValue": Object { + "args": Array [ + Object { + "absolute": Object { + "args": Array [ + Object { + "endDate": Object { + "args": Array [ + [Function], + ], + "type": "instanceOf", + }, + "endTime": Object { + "type": "string", + }, + "startDate": Object { + "args": Array [ + [Function], + ], + "type": "instanceOf", + }, + "startTime": Object { + "type": "string", + }, + }, + ], + "type": "shape", + }, + "kind": Object { + "args": Array [ + Array [ + "PRESET", + "RELATIVE", + "ABSOLUTE", + ], + ], + "type": "oneOf", + }, + "preset": Object { + "args": Array [ + Object { + "label": Object { + "type": "string", + }, + "offset": Object { + "type": "number", + }, + }, + ], + "type": "shape", + }, + "relative": Object { + "args": Array [ + Object { + "lastInterval": Object { + "type": "string", + }, + "lastNumber": Object { + "type": "number", + }, + "relativeToTime": Object { + "type": "string", + }, + "relativeToWhen": Object { + "type": "string", + }, + }, + ], + "type": "shape", + }, + }, + ], + "type": "shape", + }, + "disabled": Object { + "type": "bool", + }, + "expanded": Object { + "type": "bool", + }, + "i18n": Object { + "args": Array [ + Object { + "absoluteLabel": Object { + "type": "string", + }, + "applyBtnLabel": Object { + "type": "string", + }, + "backBtnLabel": Object { + "type": "string", + }, + "calendarLabel": Object { + "type": "string", + }, + "cancelBtnLabel": Object { + "type": "string", + }, + "customRangeLabel": Object { + "type": "string", + }, + "customRangeLinkLabel": Object { + "type": "string", + }, + "endTimeLabel": Object { + "type": "string", + }, + "intervalLabels": Object { + "args": Array [ + Object { + "type": "string", + }, + ], + "type": "arrayOf", + }, + "invalidNumberLabel": Object { + "type": "string", + }, + "lastLabel": Object { + "type": "string", + }, + "presetLabels": Object { + "args": Array [ + Object { + "type": "string", + }, + ], + "type": "arrayOf", + }, + "relativeLabel": Object { + "type": "string", + }, + "relativeLabels": Object { + "args": Array [ + Object { + "type": "string", + }, + ], + "type": "arrayOf", + }, + "relativeToLabel": Object { + "type": "string", + }, + "startTimeLabel": Object { + "type": "string", + }, + "toLabel": Object { + "type": "string", + }, + "toNowLabel": Object { + "type": "string", + }, + }, + ], + "type": "shape", + }, + "intervals": Object { + "args": Array [ + Object { + "args": Array [ + Object { + "label": Object { + "type": "string", + }, + "value": Object { + "type": "string", + }, + }, + ], + "type": "shape", + }, + ], + "type": "arrayOf", + }, + "onApply": Object { + "type": "func", + }, + "onCancel": Object { + "type": "func", + }, + "presets": Object { + "args": Array [ + Object { + "args": Array [ + Object { + "label": Object { + "type": "string", + }, + "offset": Object { + "type": "number", + }, + }, + ], + "type": "shape", + }, + ], + "type": "arrayOf", + }, + "relatives": Object { + "args": Array [ + Object { + "args": Array [ + Object { + "label": Object { + "type": "string", + }, + "value": Object { + "type": "string", + }, + }, + ], + "type": "shape", + }, + ], + "type": "arrayOf", + }, + "showRelativeOption": Object { + "type": "bool", + }, + }, + }, "baseTableReducer" => Object {}, "tableReducer" => Object {}, "tileCatalogReducer" => Object {}, diff --git a/yarn.lock b/yarn.lock index f04711f461..73fda65791 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1864,7 +1864,7 @@ resolved "https://registry.yarnpkg.com/@carbon/icon-helpers/-/icon-helpers-10.7.0.tgz#7ad56c3f377bfaa687ccca0b0813011606d6cb28" integrity sha512-zIswRvP4JyZOq7+4A4/ro7K05xVNid/+HxBOSM1ApuqcXg1e1Vl9fg37+rGY/in9v7vVgsmA8s5Arlx1mJzPIw== -"@carbon/icons-react@^10.10.0", "@carbon/icons-react@^10.9.3": +"@carbon/icons-react@10.10.0", "@carbon/icons-react@^10.9.3": version "10.10.0" resolved "https://registry.yarnpkg.com/@carbon/icons-react/-/icons-react-10.10.0.tgz#b8d61709583be1310ca5f2bdabfbdab2c0f6a892" integrity sha512-8H7RRUnvfZUlwad95Au+aR7CynIQwVcJ9OYEdEz4i16IdqFpWHql4RAb1jemLq6Q3kaHCpW4qKLDDLv8lWGu6g==