Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8fed495
Render toolbar UI
acstll Oct 27, 2025
c83ed61
Implement time window logic
acstll Oct 27, 2025
357e577
Correct method name
acstll Oct 27, 2025
4e742f8
[TimeWindowToolbar] Make which buttons to show configurable
acstll Oct 28, 2025
9e06fce
[TimeWindowToolbar] Add tilde prefix for _inexact_ ranges only
acstll Oct 28, 2025
596fc01
[TimeWindowToolbar] Rename variables for readability
acstll Oct 28, 2025
9c0cc3d
[TimeWindowToolbar] Fix displayInterval, consistent tilde, start addi…
acstll Oct 28, 2025
095dcb6
[TimeWindowButtons] Rename TimeWindowToolbar
acstll Oct 30, 2025
548659c
[TimeWindowButtons] Zoom factor default to 0.5
acstll Oct 30, 2025
f605001
[TimeWindowButtons] Display full units instead of short hand in tooltips
acstll Oct 30, 2025
9595438
[TimeWindowButtons] Add unit tests for useTimeWindow hook
acstll Oct 30, 2025
87df23b
[TimeWindowButtons] Test integration in EuiSuperDatePicker
acstll Oct 30, 2025
46bbc7c
[TimeWindowButtons] Translate labels and tooltips
acstll Oct 30, 2025
7c70b82
[TimeWindowButtons] Test disabled state
acstll Oct 30, 2025
034a66b
Remove leftovers of previous toolbar naming
acstll Oct 30, 2025
89ce75e
[Docs][EuiSuperDatePicker] Add Time window buttons section
acstll Oct 30, 2025
d52cddb
[EuiSuperDatePicker] Add TimeWindowButtons story
acstll Oct 30, 2025
2481264
[Docs] Polish example snippet
acstll Oct 31, 2025
68a1f47
[TimeWindowButtons] Improve screen-reader output
acstll Oct 31, 2025
cb338c7
[TimeWindowButtons] Fix compressed styles
acstll Oct 31, 2025
c7a1559
Update VRT
acstll Oct 31, 2025
ce59f27
Changelog
acstll Oct 31, 2025
da66b3c
Lint, argh
acstll Oct 31, 2025
5de7681
[EuiSuperDatePicker][Tests] Remove unnecessary bangs
acstll Nov 4, 2025
6c50c8b
[TimeWindowButtons] Rename config props to be more explicit
acstll Nov 4, 2025
0680122
[TimeWindowButtons] Do not show when isAutoRefreshOnly is true
acstll Nov 4, 2025
a5e2e0d
[EuiSuperDatePicker] Move isRelativeToNow util to shared utils file
acstll Nov 4, 2025
8b65549
[TimeWindowButtons] Improve date/time parsing, more gracefully handle…
acstll Nov 4, 2025
cd48c7a
[TimeWindowButtons] Improve util that provides final zoom factor value
acstll Nov 4, 2025
286650c
[EuiSuperDatePicker] Test actual time shifted or zoomed out
acstll Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love that VRT test... 🙃

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/eui/changelogs/upcoming/9151.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Updated `EuiSuperDatePicker` with new time window buttons for time shifting and zoom out, opt-in via `showTimeWindowButtons` boolean prop.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TimeWindowButtons renders 1`] = `
<div
class="euiSuperDatePicker__timeWindowButtons emotion-euiButtonGroup__buttons-m-TimeWindowButtons"
data-test-subj="timeWindowButtons"
>
<span
class="euiToolTipAnchor euiButtonGroup__tooltipWrapper emotion-euiToolTipAnchor-inlineBlock-tooltipWrapper-m"
>
<button
aria-pressed="false"
class="euiButtonGroupButton euiButtonGroupButton-isIconOnly emotion-euiButtonDisplay-m-defaultMinWidth-euiButtonGroupButton-iconOnly-hasToolTip-uncompressed-base-text"
data-test-subj="timeWindowButtonsPrevious"
title=""
type="button"
>
<span
class="emotion-euiButtonDisplayContent-euiButtonGroupButton__content"
>
<span
color="inherit"
data-euiicon-type="arrowLeft"
/>
<span
class="eui-textTruncate emotion-euiButtonGroupButton__iconOnly"
data-text="Previous"
>
Previous
</span>
</span>
</button>
</span>
<span
class="euiToolTipAnchor euiButtonGroup__tooltipWrapper emotion-euiToolTipAnchor-inlineBlock-tooltipWrapper-m"
>
<button
aria-pressed="false"
class="euiButtonGroupButton euiButtonGroupButton-isIconOnly emotion-euiButtonDisplay-m-defaultMinWidth-euiButtonGroupButton-iconOnly-hasToolTip-uncompressed-base-text"
data-test-subj="timeWindowButtonsZoomOut"
title=""
type="button"
>
<span
class="emotion-euiButtonDisplayContent-euiButtonGroupButton__content"
>
<span
color="inherit"
data-euiicon-type="magnifyWithMinus"
/>
<span
class="eui-textTruncate emotion-euiButtonGroupButton__iconOnly"
data-text="Zoom out"
>
Zoom out
</span>
</span>
</button>
</span>
<span
class="euiToolTipAnchor euiButtonGroup__tooltipWrapper emotion-euiToolTipAnchor-inlineBlock-tooltipWrapper-m"
>
<button
aria-pressed="false"
class="euiButtonGroupButton euiButtonGroupButton-isIconOnly emotion-euiButtonDisplay-m-defaultMinWidth-euiButtonGroupButton-iconOnly-hasToolTip-uncompressed-base-text"
data-test-subj="timeWindowButtonsNext"
title=""
type="button"
>
<span
class="emotion-euiButtonDisplayContent-euiButtonGroupButton__content"
>
<span
color="inherit"
data-euiicon-type="arrowRight"
/>
<span
class="eui-textTruncate emotion-euiButtonGroupButton__iconOnly"
data-text="Next"
>
Next
</span>
</span>
</button>
</span>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import dateMath from '@elastic/datemath';
import moment, { LocaleSpecifier, RelativeTimeKey } from 'moment'; // eslint-disable-line import/named
import { useEuiI18n } from '../../i18n';
import { getDateMode, DATE_MODES } from './date_modes';
import { parseRelativeParts } from './relative_utils';
import { parseRelativeParts, isRelativeToNow } from './relative_utils';
import { useI18nTimeOptions } from './time_options';
import {
DurationRange,
Expand Down Expand Up @@ -315,16 +315,6 @@ const hasRangeMatch = (
return ranges.find(({ start, end }) => timeFrom === start && timeTo === end);
};

const isRelativeToNow = (timeFrom: ShortDate, timeTo: ShortDate): boolean => {
const fromDateMode = getDateMode(timeFrom);
const toDateMode = getDateMode(timeTo);
const isLast =
fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.NOW;
const isNext =
fromDateMode === DATE_MODES.NOW && toDateMode === DATE_MODES.RELATIVE;
return isLast || isNext;
};

export const showPrettyDuration = (
timeFrom: ShortDate,
timeTo: ShortDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import moment from 'moment';

import { get } from '../../../services/objects';
import { isString } from '../../../services/predicate';
import { TimeUnitId, RelativeParts } from '../types';
import { TimeUnitId, RelativeParts, ShortDate } from '../types';
import { getDateMode, DATE_MODES } from './date_modes';

const ROUND_DELIMETER = '/';

Expand Down Expand Up @@ -80,3 +81,16 @@ export const toRelativeStringFromParts = (relativeParts: RelativeParts) => {

return `now${operator}${count}${unit}${round}`;
};

export const isRelativeToNow = (
timeFrom: ShortDate,
timeTo: ShortDate
): boolean => {
const fromDateMode = getDateMode(timeFrom);
const toDateMode = getDateMode(timeTo);
const isLast =
fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.NOW;
const isNext =
fromDateMode === DATE_MODES.NOW && toDateMode === DATE_MODES.RELATIVE;
return isLast || isNext;
};
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ export const QuickSelectOnly: Story = {
},
};

export const TimeWindowButtons: Story = {
args: {
showTimeWindowButtons: true,
showUpdateButton: false,
},
render: (args) => <StatefulSuperDatePicker {...args} />,
};

/**
* VRT only
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import React, { useState } from 'react';
import moment from 'moment';
import { fireEvent, act } from '@testing-library/react';
import { fireEvent, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { render, waitForEuiPopoverOpen, screen } from '../../../test/rtl';
Expand All @@ -19,6 +19,7 @@ import {
EuiSuperDatePicker,
EuiSuperDatePickerProps,
} from './super_date_picker';
import { ZOOM_FACTOR_DEFAULT } from './time_window_buttons';

const noop = () => {};

Expand Down Expand Up @@ -640,4 +641,136 @@ describe('EuiSuperDatePicker', () => {
expect(prevEnd).toBe('2025-01-01T10:00:00.000Z');
});
});

describe('Time window buttons', () => {
it('renders only when showTimeWindowButtons prop is passed', () => {
const start = '2025-10-30T12:00:00.000Z';
const end = '2025-10-30T13:00:00.000Z';

const { queryByTestSubject, rerender } = render(
<EuiSuperDatePicker
start={start}
end={end}
onTimeChange={() => {}}
showTimeWindowButtons
/>
);

expect(queryByTestSubject('timeWindowButtons')).toBeInTheDocument();

rerender(
<EuiSuperDatePicker start={start} end={end} onTimeChange={() => {}} />
);

expect(queryByTestSubject('timeWindowButtons')).not.toBeInTheDocument();
});

it('updates time when shifting', async () => {
const start = '2025-10-30T12:00:00.000Z';
const end = '2025-10-30T13:00:00.000Z';
const stepBackwardStart = '2025-10-30T11:00:00.000Z';
const stepBackwardEnd = '2025-10-30T12:00:00.000Z';
let lastTimeChange: { start: string; end: string } = { start, end };

const { getByTestSubject } = render(
<EuiSuperDatePicker
start={start}
end={end}
onTimeChange={({ start, end }) => {
lastTimeChange = { start, end };
}}
showUpdateButton={false}
showTimeWindowButtons={true}
/>
);

act(() => {
userEvent.click(getByTestSubject('timeWindowButtonsPrevious'));
});

await waitFor(() => {
expect(lastTimeChange.end).toEqual(stepBackwardEnd);
expect(lastTimeChange.start).toEqual(stepBackwardStart);

const initialTimeStart = new Date(start).getTime();
const updatedTimeStart = new Date(lastTimeChange.start).getTime();
const initialTimeEnd = new Date(end).getTime();
const updatedTimeEnd = new Date(lastTimeChange.end).getTime();

expect(initialTimeStart).toBeGreaterThan(updatedTimeStart);
expect(initialTimeEnd).toBeGreaterThan(updatedTimeEnd);
// Also check the diff is the same
expect(initialTimeEnd - initialTimeStart).toEqual(
updatedTimeEnd - updatedTimeStart
);
});
});

it('updates time when zooming out', async () => {
const start = '2025-10-30T12:00:00.000Z';
const end = '2025-10-31T12:00:00.000Z';
let lastTimeChange: { start: string; end: string } = { start, end };

const { getByTestSubject } = render(
<EuiSuperDatePicker
start={start}
end={end}
onTimeChange={({ start, end }) => {
lastTimeChange = { start, end };
}}
showUpdateButton={false}
showTimeWindowButtons={true}
/>
);

act(() => {
userEvent.click(getByTestSubject('timeWindowButtonsZoomOut'));
});

await waitFor(() => {
const initialTimeStart = new Date(start).getTime();
const updatedTimeStart = new Date(lastTimeChange.start).getTime();
const initialTimeEnd = new Date(end).getTime();
const updatedTimeEnd = new Date(lastTimeChange.end).getTime();
expect(initialTimeStart).toBeGreaterThan(updatedTimeStart);
expect(initialTimeEnd).toBeLessThan(updatedTimeEnd);
// Check the diff expanded by zoom factor
expect(
(initialTimeEnd - initialTimeStart) * (1 + ZOOM_FACTOR_DEFAULT)
).toEqual(updatedTimeEnd - updatedTimeStart);
});
});

it('is disabled when date/time range is invalid', async () => {
// reversed range (invalid)
const start = '2025-10-30T14:00:00.000Z';
const end = '2025-10-31T14:00:00.000Z';

const { rerender, getByTestSubject } = render(
<EuiSuperDatePicker
start={end}
end={start}
onTimeChange={() => {}}
showTimeWindowButtons={true}
/>
);

expect(getByTestSubject('timeWindowButtonsPrevious')).toBeDisabled();
expect(getByTestSubject('timeWindowButtonsZoomOut')).toBeDisabled();
expect(getByTestSubject('timeWindowButtonsNext')).toBeDisabled();

rerender(
<EuiSuperDatePicker
start={start}
end={end}
onTimeChange={() => {}}
showTimeWindowButtons={true}
/>
);

expect(getByTestSubject('timeWindowButtonsPrevious')).not.toBeDisabled();
expect(getByTestSubject('timeWindowButtonsZoomOut')).not.toBeDisabled();
expect(getByTestSubject('timeWindowButtonsNext')).not.toBeDisabled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {

import { TimeOptions, RenderI18nTimeOptions } from './time_options';
import { PrettyDuration, showPrettyDuration } from './pretty_duration';
import {
TimeWindowButtons,
type TimeWindowButtonsConfig,
} from './time_window_buttons';
import { AsyncInterval } from './async_interval';

import {
Expand Down Expand Up @@ -202,6 +206,12 @@ export type EuiSuperDatePickerProps = CommonProps & {
*/
showUpdateButton?: boolean | 'iconOnly';

/**
* Set to true to display buttons for time shifting and zooming out,
* next to the top-level control.
*/
showTimeWindowButtons?: boolean | TimeWindowButtonsConfig;

/**
* Hides the actual input reducing to just the quick select button.
*/
Expand Down Expand Up @@ -725,6 +735,27 @@ export class EuiSuperDatePickerInternal extends Component<
}
};

renderTimeWindowButtons = () => {
if (!this.props.showTimeWindowButtons || this.props.isAutoRefreshOnly) {
return null;
}
const { start, end, showTimeWindowButtons, compressed, isDisabled } =
this.props;
const config =
typeof showTimeWindowButtons === 'boolean' ? {} : showTimeWindowButtons;

return (
<TimeWindowButtons
applyTime={this.applyQuickTime}
start={start}
end={end}
compressed={compressed}
isDisabled={!!isDisabled || this.state.isInvalid}
{...config}
/>
);
};

renderUpdateButton = () => {
const {
isLoading,
Expand Down Expand Up @@ -805,6 +836,7 @@ export class EuiSuperDatePickerInternal extends Component<
) : (
<>
{this.renderDatePickerRange()}
{this.renderTimeWindowButtons()}
{this.renderUpdateButton()}
</>
)}
Expand Down
Loading