diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png index c76efb162dc..ffa99f4fdca 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png new file mode 100644 index 00000000000..585d9fdc753 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperUpdateButton_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperUpdateButton_Playground.png index e1b9ecdcc06..57abbb2620a 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperUpdateButton_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperUpdateButton_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png index 28a69a24121..6eb2a51e3d3 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png new file mode 100644 index 00000000000..32277cccdf5 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Restricted_Range.png differ diff --git a/packages/eui/changelogs/upcoming/8071.md b/packages/eui/changelogs/upcoming/8071.md new file mode 100644 index 00000000000..0d1f53ecdbc --- /dev/null +++ b/packages/eui/changelogs/upcoming/8071.md @@ -0,0 +1,2 @@ +- Added props `minDate` and `maxDate` on `EuiSuperDatePicker` to support restricting date range selections + diff --git a/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_example.js b/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_example.js index 4fea50c1a86..c520d7ddd6b 100644 --- a/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_example.js +++ b/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_example.js @@ -41,6 +41,9 @@ const superDatePickerPatternSource = require('!!raw-loader!./super_date_picker_p import SuperDatePickerLocale from './super_date_picker_locale'; const superDatePickerLocaleSource = require('!!raw-loader!./super_date_picker_locale'); +import SuperDatePickerRangeRestricted from './super_date_picker_range_restricted'; +const superDatePickerRangeRestrictedSource = require('!!raw-loader!./super_date_picker_range_restricted'); + const superDatePickerSnippet = ``; +const superDatePickerRangeRestrictedSnippet = ``; + export const SuperDatePickerExample = { title: 'Super date picker', intro: ( @@ -391,5 +403,28 @@ if (!endMoment || !endMoment.isValid()) { snippet: superDatePickerLocaleSnippet, demo: , }, + { + title: 'Restricted Range', + source: [ + { + type: GuideSectionTypes.JS, + code: superDatePickerRangeRestrictedSource, + }, + ], + text: ( + <> +

+ To limit the range from which users can choose a date, you can use{' '} + minDate and maxDate. By + updating the date input values for start and{' '} + end users get immediate feedback on what range + values are allowed. +

+ + ), + props: { EuiSuperDatePicker }, + snippet: superDatePickerRangeRestrictedSnippet, + demo: , + }, ], }; diff --git a/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_range_restricted.tsx b/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_range_restricted.tsx new file mode 100644 index 00000000000..583dbf00fe0 --- /dev/null +++ b/packages/eui/src-docs/src/views/super_date_picker/super_date_picker_range_restricted.tsx @@ -0,0 +1,29 @@ +import React, { useState } from 'react'; + +import { + EuiSuperDatePicker, + OnTimeChangeProps, +} from '../../../../src/components'; +import moment from 'moment'; + +export default () => { + const [start, setStart] = useState('now-30m'); + const [end, setEnd] = useState('now'); + const minDate = moment().subtract(1, 'M'); + const maxDate = moment().add(1, 'M'); + + const onTimeChange = ({ start, end }: OnTimeChangeProps) => { + setStart(start); + setEnd(end); + }; + + return ( + + ); +}; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx index 76b4b2717e2..e81813a8f9b 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx @@ -45,6 +45,8 @@ export interface EuiAbsoluteTabProps { roundUp: boolean; labelPrefix: string; utcOffset?: number; + minDate?: Moment; + maxDate?: Moment; } export const EuiAbsoluteTab: FunctionComponent = ({ @@ -55,6 +57,8 @@ export const EuiAbsoluteTab: FunctionComponent = ({ locale, roundUp, utcOffset, + minDate, + maxDate, labelPrefix, }) => { const styles = useEuiMemoizedStyles(euiAbsoluteTabDateFormStyles); @@ -157,6 +161,8 @@ export const EuiAbsoluteTab: FunctionComponent = ({ timeFormat={timeFormat} locale={locale} utcOffset={utcOffset} + minDate={minDate} + maxDate={maxDate} /> ); diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx index cf62ac294ae..4ee97b3d971 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx @@ -7,7 +7,7 @@ */ import React, { FunctionComponent } from 'react'; -import { LocaleSpecifier } from 'moment'; +import { LocaleSpecifier, Moment } from 'moment'; import { useEuiMemoizedStyles } from '../../../../services'; import { useEuiPaddingCSS } from '../../../../global_styling'; @@ -37,6 +37,8 @@ export interface EuiDatePopoverContentProps { locale?: LocaleSpecifier; position: 'start' | 'end'; utcOffset?: number; + minDate?: Moment; + maxDate?: Moment; timeOptions: TimeOptions; } @@ -53,6 +55,8 @@ export const EuiDatePopoverContent: FunctionComponent< position, utcOffset, timeOptions, + minDate, + maxDate, }) => { const styles = useEuiMemoizedStyles(euiDatePopoverContentStyles); @@ -101,6 +105,8 @@ export const EuiDatePopoverContent: FunctionComponent< roundUp={roundUp} labelPrefix={labelPrefix} utcOffset={utcOffset} + minDate={minDate} + maxDate={maxDate} /> ), 'data-test-subj': 'superDatePickerAbsoluteTab', diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx index 528d9baf4b3..39ada5dbf93 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.stories.tsx @@ -7,6 +7,7 @@ */ import React from 'react'; +import moment from 'moment'; import type { Meta, StoryObj } from '@storybook/react'; import { expect } from '@storybook/test'; import { within } from '../../../../.storybook/test'; @@ -48,6 +49,8 @@ const meta: Meta = { isLoading: false, isQuickSelectOnly: false, commonlyUsedRanges: [{ start: 'now/d', end: 'now/d', label: 'Today' }], + maxDate: undefined, + minDate: undefined, }, }; enableFunctionToggleControls(meta, ['onTimeChange']); @@ -89,6 +92,28 @@ export const CustomQuickSelectPanel: Story = { }, }; +export const RestrictedRange: Story = { + parameters: { + controls: { + include: [ + 'dateFormat', + 'start', + 'end', + 'minDate', + 'maxDate', + 'onTimeChange', + ], + }, + loki: { + chromeSelector: LOKI_SELECTORS.portal, + }, + }, + args: { + minDate: moment('10/01/2024'), + maxDate: moment('11/01/2024'), + }, +}; + function CustomPanel({ applyTime }: { applyTime?: ApplyTime }) { function applyMyCustomTime() { applyTime!({ start: 'now-30d', end: 'now+7d' }); diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx index f6d9e552a08..7c9520abf10 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx @@ -16,6 +16,7 @@ import { EuiSuperDatePicker, EuiSuperDatePickerProps, } from './super_date_picker'; +import moment from 'moment'; const noop = () => {}; @@ -396,5 +397,83 @@ describe('EuiSuperDatePicker', () => { expect(startButton).toHaveTextContent('300 days ago'); }); }); + + describe('minDate', () => { + const props = { + onTimeChange: noop, + end: 'now', + }; + + it('is valid when the start value is set after the minDate', () => { + const { container } = render( + + ); + + const formWraper = container.querySelector( + '.euiFormControlLayout__childrenWrapper' + )!; + + expect(formWraper.className).not.toContain('invalid'); + }); + + it('is invalid when the start value is set before the minDate', () => { + const { container } = render( + + ); + + const formWraper = container.querySelector( + '.euiFormControlLayout__childrenWrapper' + )!; + + expect(formWraper.className).toContain('invalid'); + }); + }); + + describe('maxDate', () => { + const props = { + onTimeChange: noop, + start: '10/01/2024', + }; + + it('is valid when the end value is set before the maxDate', () => { + const { container } = render( + + ); + + const formWraper = container.querySelector( + '.euiFormControlLayout__childrenWrapper' + )!; + + expect(formWraper.className).not.toContain('invalid'); + }); + + it('is invalid when the end date exceeds the maxDate', () => { + const { container } = render( + + ); + + const formWraper = container.querySelector( + '.euiFormControlLayout__childrenWrapper' + )!; + + expect(formWraper.className).toContain('invalid'); + }); + }); }); }); diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx index 0c804ca5f79..f63194683c5 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx @@ -13,7 +13,7 @@ import React, { ReactNode, } from 'react'; import classNames from 'classnames'; -import moment, { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named +import moment, { LocaleSpecifier, Moment } from 'moment'; // eslint-disable-line import/named import dateMath from '@elastic/datemath'; import { useEuiMemoizedStyles } from '../../../services'; @@ -174,6 +174,16 @@ export type EuiSuperDatePickerProps = CommonProps & { */ end?: ShortDate; + /** + * Defines min. date accepted as a selection (in moment format) + */ + minDate?: moment.Moment; + + /** + * Defines max. date accepted as a selection (in moment format) + */ + maxDate?: moment.Moment; + /** * Specifies the formatted used when displaying times * @default 'HH:mm' @@ -235,7 +245,12 @@ interface EuiSuperDatePickerState { start: ShortDate; } -function isRangeInvalid(start: ShortDate, end: ShortDate) { +function isRangeInvalid( + start: ShortDate, + end: ShortDate, + minDate?: Moment, + maxDate?: Moment +) { if (start === 'now' && end === 'now') { return true; } @@ -250,7 +265,10 @@ function isRangeInvalid(start: ShortDate, end: ShortDate) { !endMoment.isValid() || !moment(startMoment).isValid() || !moment(endMoment).isValid() || - startMoment.isAfter(endMoment); + startMoment.isAfter(endMoment) || + endMoment.isBefore(startMoment) || + (minDate != null && startMoment.isBefore(minDate)) || + (maxDate != null && endMoment.isAfter(maxDate)); return isInvalid; } @@ -283,7 +301,12 @@ export class EuiSuperDatePickerInternal extends Component< }, start: this.props.start, end: this.props.end, - isInvalid: isRangeInvalid(this.props.start, this.props.end), + isInvalid: isRangeInvalid( + this.props.start, + this.props.end, + this.props.minDate, + this.props.maxDate + ), hasChanged: false, showPrettyDuration: showPrettyDuration( this.props.start, @@ -309,7 +332,12 @@ export class EuiSuperDatePickerInternal extends Component< }, start: nextProps.start, end: nextProps.end, - isInvalid: isRangeInvalid(nextProps.start, nextProps.end), + isInvalid: isRangeInvalid( + nextProps.start, + nextProps.end, + nextProps.minDate, + nextProps.maxDate + ), hasChanged: false, showPrettyDuration: showPrettyDuration( nextProps.start, @@ -323,7 +351,12 @@ export class EuiSuperDatePickerInternal extends Component< } setTime = ({ end, start }: DurationRange) => { - const isInvalid = isRangeInvalid(start, end); + const isInvalid = isRangeInvalid( + start, + end, + this.props.minDate, + this.props.maxDate + ); this.setState({ start, @@ -516,6 +549,8 @@ export class EuiSuperDatePickerInternal extends Component< locale, timeFormat, utcOffset, + minDate, + maxDate, compressed, onFocus, memoizedStyles: styles, @@ -631,6 +666,8 @@ export class EuiSuperDatePickerInternal extends Component< utcOffset={utcOffset} timeFormat={timeFormat} locale={locale || contextLocale} + minDate={minDate} + maxDate={maxDate} canRoundRelativeUnits={canRoundRelativeUnits} isOpen={this.state.isStartDatePopoverOpen} onPopoverToggle={this.onStartDatePopoverToggle} @@ -653,6 +690,8 @@ export class EuiSuperDatePickerInternal extends Component< utcOffset={utcOffset} timeFormat={timeFormat} locale={locale || contextLocale} + minDate={minDate} + maxDate={maxDate} canRoundRelativeUnits={canRoundRelativeUnits} roundUp isOpen={this.state.isEndDatePopoverOpen}