From 5e2c0911bd5e408a838215e0fe41286de26b2129 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 1 Feb 2024 13:28:30 -0800 Subject: [PATCH 1/5] Set up new `preferLargerRelativeUnits` prop + default it to true and pass it down to the subcomponents that will need it --- src-docs/src/views/super_date_picker/playground.js | 7 +++++++ .../date_popover/date_popover_button.tsx | 3 +++ .../date_popover/date_popover_content.tsx | 6 +++++- .../super_date_picker/super_date_picker.tsx | 13 +++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src-docs/src/views/super_date_picker/playground.js b/src-docs/src/views/super_date_picker/playground.js index b593cdc7924..de43f3fdbbf 100644 --- a/src-docs/src/views/super_date_picker/playground.js +++ b/src-docs/src/views/super_date_picker/playground.js @@ -32,6 +32,13 @@ export const superDatePickerConfig = () => { value: true, }; + propsToUse.preferLargerRelativeUnits = { + ...propsToUse.preferLargerRelativeUnits, + type: PropTypes.Boolean, + defaultValue: true, + value: true, + }; + propsToUse.locale = { ...propsToUse.locale, type: PropTypes.String, diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx index 536e1e178fb..4977c200a76 100644 --- a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx @@ -38,6 +38,7 @@ export interface EuiDatePopoverButtonProps { onPopoverClose: EuiPopoverProps['closePopover']; onPopoverToggle: MouseEventHandler; position: 'start' | 'end'; + preferLargerRelativeUnits?: boolean; roundUp?: boolean; timeFormat: string; value: string; @@ -56,6 +57,7 @@ export const EuiDatePopoverButton: FunctionComponent< needsUpdating, value, buttonProps, + preferLargerRelativeUnits, roundUp, onChange, locale, @@ -133,6 +135,7 @@ export const EuiDatePopoverButton: FunctionComponent< void; + preferLargerRelativeUnits?: boolean; roundUp?: boolean; dateFormat: string; timeFormat: string; @@ -41,6 +42,7 @@ export const EuiDatePopoverContent: FunctionComponent< EuiDatePopoverContentProps > = ({ value, + preferLargerRelativeUnits = true, roundUp = false, onChange, dateFormat, @@ -108,7 +110,9 @@ export const EuiDatePopoverContent: FunctionComponent< Date: Thu, 1 Feb 2024 13:55:10 -0800 Subject: [PATCH 2/5] Update pretty duration utils to accept `preferLargerRelativeUnits` + add tests for `useFormatTimeString` - except for the roundUp option because I can't figure out what the heck that does :shrug: --- .../date_popover/date_popover_button.tsx | 9 ++-- .../pretty_duration.test.tsx | 51 +++++++++++++++++++ .../super_date_picker/pretty_duration.tsx | 39 ++++++++++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx index 4977c200a76..5b1af6f28c8 100644 --- a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx @@ -84,12 +84,11 @@ export const EuiDatePopoverButton: FunctionComponent< }, ]); - const formattedValue = useFormatTimeString( - value, - dateFormat, + const formattedValue = useFormatTimeString(value, dateFormat, { roundUp, - locale - ); + locale, + preferLargerRelativeUnits, + }); let title = formattedValue; const invalidTitle = useEuiI18n( diff --git a/src/components/date_picker/super_date_picker/pretty_duration.test.tsx b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx index 92004915bf2..b9db7f53885 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.test.tsx +++ b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx @@ -13,6 +13,7 @@ import { usePrettyDuration, PrettyDuration, showPrettyDuration, + useFormatTimeString, } from './pretty_duration'; const dateFormat = 'MMMM Do YYYY, HH:mm:ss.SSS'; @@ -123,3 +124,53 @@ describe('showPrettyDuration', () => { ).toBe(false); }); }); + +describe('useFormatTimeString', () => { + it('it takes a time string and formats it into a humanized date', () => { + expect( + renderHook(() => useFormatTimeString('now-3s', dateFormat)).result.current + ).toEqual('~ a few seconds ago'); + expect( + renderHook(() => useFormatTimeString('now+1m', dateFormat)).result.current + ).toEqual('~ in a minute'); + expect( + renderHook(() => useFormatTimeString('now+100w', dateFormat)).result + .current + ).toEqual('~ in 2 years'); + }); + + it("always parses the 'now' string as-is", () => { + expect( + renderHook(() => useFormatTimeString('now', dateFormat)).result.current + ).toEqual('now'); + }); + + describe('options', () => { + test('locale', () => { + expect( + renderHook(() => + useFormatTimeString('now+15m', dateFormat, { locale: 'ja' }) + ).result.current + ).toBe('~ 15分後'); + }); + + describe('preferLargerRelativeUnits', () => { + const option = { preferLargerRelativeUnits: false }; + + it("allows skipping moment.fromNow()'s default rounding", () => { + expect( + renderHook(() => useFormatTimeString('now-3s', dateFormat, option)) + .result.current + ).toEqual('3 seconds ago'); + expect( + renderHook(() => useFormatTimeString('now+1m', dateFormat, option)) + .result.current + ).toEqual('in a minute'); + expect( + renderHook(() => useFormatTimeString('now+100w', dateFormat, option)) + .result.current + ).toEqual('in 100 weeks'); + }); + }); + }); +}); diff --git a/src/components/date_picker/super_date_picker/pretty_duration.tsx b/src/components/date_picker/super_date_picker/pretty_duration.tsx index e64d1b9f568..7803c3a35fd 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.tsx +++ b/src/components/date_picker/super_date_picker/pretty_duration.tsx @@ -8,7 +8,7 @@ import React from 'react'; import dateMath from '@elastic/datemath'; -import moment, { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named +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'; @@ -146,9 +146,18 @@ const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; export const useFormatTimeString = ( timeString: string, dateFormat: string, - roundUp = false, - locale: LocaleSpecifier = 'en' + options?: { + locale?: LocaleSpecifier; + roundUp?: boolean; + preferLargerRelativeUnits?: boolean; + } ): string => { + const { + locale = 'en', + roundUp = false, + preferLargerRelativeUnits = true, + } = options || {}; + // i18n'd strings const nowDisplay = useEuiI18n('euiPrettyDuration.now', 'now'); const invalidDateDisplay = useEuiI18n( @@ -171,7 +180,27 @@ export const useFormatTimeString = ( } if (moment.isMoment(tryParse)) { - return `~ ${tryParse.locale(locale).fromNow()}`; + if (preferLargerRelativeUnits) { + return `~ ${tryParse.locale(locale).fromNow()}`; + } else { + // To force a specific unit to be used, we need to skip moment.fromNow() + // entirely and write our own custom moment formatted output. + const { count, unit: _unit } = parseRelativeParts(timeString); + const isFuture = _unit.endsWith('+'); + const unit = isFuture ? _unit.slice(0, -1) : _unit; // We want just the unit letter without the trailing + + + // @see https://momentjs.com/docs/#/customization/relative-time/ + const relativeUnitKey = ( + count === 1 ? unit : unit + unit + ) as RelativeTimeKey; + + // @see https://momentjs.com/docs/#/i18n/locale-data/ + return moment.localeData().pastFuture( + isFuture ? count : count * -1, + moment.localeData().relativeTime(count, false, relativeUnitKey, false) + // Booleans don't seem to actually matter for output, .pastFuture() handles that + ); + } } return timeString; @@ -246,7 +275,7 @@ export const usePrettyDuration = ({ * If it's none of the above, display basic fallback copy */ const displayFrom = useFormatTimeString(timeFrom, dateFormat); - const displayTo = useFormatTimeString(timeTo, dateFormat, true); + const displayTo = useFormatTimeString(timeTo, dateFormat, { roundUp: true }); const fallbackDuration = useEuiI18n( 'euiPrettyDuration.fallbackDuration', '{displayFrom} to {displayTo}', From 312815e801abb84d7f39cd1ea5bbc2a497072f8f Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 1 Feb 2024 14:25:54 -0800 Subject: [PATCH 3/5] Add final tests with event/user behavior --- .../super_date_picker.test.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/components/date_picker/super_date_picker/super_date_picker.test.tsx b/src/components/date_picker/super_date_picker/super_date_picker.test.tsx index 9156c1f622c..9384d97348d 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.test.tsx +++ b/src/components/date_picker/super_date_picker/super_date_picker.test.tsx @@ -32,6 +32,12 @@ const findInternalInstance = ( }; describe('EuiSuperDatePicker', () => { + // RTL doesn't automatically clean up portals/datepicker popovers between tests + afterEach(() => { + const portals = document.querySelectorAll('[data-euiportal]'); + portals.forEach((portal) => portal.parentNode?.removeChild(portal)); + }); + shouldRenderCustomStyles(, { skip: { style: true }, }); @@ -313,5 +319,59 @@ describe('EuiSuperDatePicker', () => { expect(container.firstChild).toMatchSnapshot(); }); }); + + describe('preferLargerRelativeUnits', () => { + const props = { + onTimeChange: noop, + start: 'now-300m', + end: 'now', + }; + + it('defaults to true, which will round relative units up to the next largest unit', () => { + const { getByTestSubject } = render( + + ); + fireEvent.click(getByTestSubject('superDatePickerShowDatesButton')); + + const startButton = getByTestSubject( + 'superDatePickerstartDatePopoverButton' + ); + expect(startButton).toHaveTextContent('~ 5 hours ago'); + + const countInput = getByTestSubject( + 'superDatePickerRelativeDateInputNumber' + ); + expect(countInput).toHaveValue(5); + + const unitSelect = getByTestSubject( + 'superDatePickerRelativeDateInputUnitSelector' + ); + expect(unitSelect).toHaveValue('h'); + + fireEvent.change(countInput, { target: { value: 300 } }); + fireEvent.change(unitSelect, { target: { value: 'd' } }); + expect(startButton).toHaveTextContent('~ 10 months ago'); + }); + + it('when false, allows preserving the unit set in the start/end time timestamp', () => { + const { getByTestSubject } = render( + + ); + fireEvent.click(getByTestSubject('superDatePickerShowDatesButton')); + + const startButton = getByTestSubject( + 'superDatePickerstartDatePopoverButton' + ); + expect(startButton).toHaveTextContent('300 minutes ago'); + + const unitSelect = getByTestSubject( + 'superDatePickerRelativeDateInputUnitSelector' + ); + expect(unitSelect).toHaveValue('m'); + + fireEvent.change(unitSelect, { target: { value: 'd' } }); + expect(startButton).toHaveTextContent('300 days ago'); + }); + }); }); }); From 64b0faba1ff97d9191c9beecbb7d2df0c1abb3d0 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 1 Feb 2024 18:39:53 -0800 Subject: [PATCH 4/5] changelog --- changelogs/upcoming/7502.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/upcoming/7502.md diff --git a/changelogs/upcoming/7502.md b/changelogs/upcoming/7502.md new file mode 100644 index 00000000000..9bf2bf013e5 --- /dev/null +++ b/changelogs/upcoming/7502.md @@ -0,0 +1 @@ +- Updated `EuiSuperDatePicker` with a new `preferLargerRelativeUnits` prop, which defaults to true (current behavior). To preserve displaying the unit that users select, set this to false. From 489c100e771063c2ac296d592916ae2d636c3810 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 2 Feb 2024 09:02:30 -0800 Subject: [PATCH 5/5] [PR feedback] naming: final_FINAL_v4.docx --- changelogs/upcoming/7502.md | 2 +- src-docs/src/views/super_date_picker/playground.js | 4 ++-- .../date_popover/date_popover_button.tsx | 8 ++++---- .../date_popover/date_popover_content.tsx | 6 +++--- .../super_date_picker/pretty_duration.test.tsx | 4 ++-- .../date_picker/super_date_picker/pretty_duration.tsx | 6 +++--- .../super_date_picker/super_date_picker.test.tsx | 6 +++--- .../super_date_picker/super_date_picker.tsx | 10 +++++----- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/changelogs/upcoming/7502.md b/changelogs/upcoming/7502.md index 9bf2bf013e5..f4ff0ca7b86 100644 --- a/changelogs/upcoming/7502.md +++ b/changelogs/upcoming/7502.md @@ -1 +1 @@ -- Updated `EuiSuperDatePicker` with a new `preferLargerRelativeUnits` prop, which defaults to true (current behavior). To preserve displaying the unit that users select, set this to false. +- Updated `EuiSuperDatePicker` with a new `canRoundRelativeUnits` prop, which defaults to true (current behavior). To preserve displaying the unit that users select for relative time, set this to false. diff --git a/src-docs/src/views/super_date_picker/playground.js b/src-docs/src/views/super_date_picker/playground.js index de43f3fdbbf..31a5ab6f0ea 100644 --- a/src-docs/src/views/super_date_picker/playground.js +++ b/src-docs/src/views/super_date_picker/playground.js @@ -32,8 +32,8 @@ export const superDatePickerConfig = () => { value: true, }; - propsToUse.preferLargerRelativeUnits = { - ...propsToUse.preferLargerRelativeUnits, + propsToUse.canRoundRelativeUnits = { + ...propsToUse.canRoundRelativeUnits, type: PropTypes.Boolean, defaultValue: true, value: true, diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx index 5b1af6f28c8..e238d0df208 100644 --- a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx @@ -38,7 +38,7 @@ export interface EuiDatePopoverButtonProps { onPopoverClose: EuiPopoverProps['closePopover']; onPopoverToggle: MouseEventHandler; position: 'start' | 'end'; - preferLargerRelativeUnits?: boolean; + canRoundRelativeUnits?: boolean; roundUp?: boolean; timeFormat: string; value: string; @@ -57,7 +57,7 @@ export const EuiDatePopoverButton: FunctionComponent< needsUpdating, value, buttonProps, - preferLargerRelativeUnits, + canRoundRelativeUnits, roundUp, onChange, locale, @@ -87,7 +87,7 @@ export const EuiDatePopoverButton: FunctionComponent< const formattedValue = useFormatTimeString(value, dateFormat, { roundUp, locale, - preferLargerRelativeUnits, + canRoundRelativeUnits, }); let title = formattedValue; @@ -134,7 +134,7 @@ export const EuiDatePopoverButton: FunctionComponent< void; - preferLargerRelativeUnits?: boolean; + canRoundRelativeUnits?: boolean; roundUp?: boolean; dateFormat: string; timeFormat: string; @@ -42,7 +42,7 @@ export const EuiDatePopoverContent: FunctionComponent< EuiDatePopoverContentProps > = ({ value, - preferLargerRelativeUnits = true, + canRoundRelativeUnits = true, roundUp = false, onChange, dateFormat, @@ -111,7 +111,7 @@ export const EuiDatePopoverContent: FunctionComponent< dateFormat={dateFormat} locale={locale} value={ - preferLargerRelativeUnits ? toAbsoluteString(value, roundUp) : value + canRoundRelativeUnits ? toAbsoluteString(value, roundUp) : value } onChange={onChange} roundUp={roundUp} diff --git a/src/components/date_picker/super_date_picker/pretty_duration.test.tsx b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx index b9db7f53885..c7b5cce5bd3 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.test.tsx +++ b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx @@ -154,8 +154,8 @@ describe('useFormatTimeString', () => { ).toBe('~ 15分後'); }); - describe('preferLargerRelativeUnits', () => { - const option = { preferLargerRelativeUnits: false }; + describe('canRoundRelativeUnits', () => { + const option = { canRoundRelativeUnits: false }; it("allows skipping moment.fromNow()'s default rounding", () => { expect( diff --git a/src/components/date_picker/super_date_picker/pretty_duration.tsx b/src/components/date_picker/super_date_picker/pretty_duration.tsx index 7803c3a35fd..3f6f36d2ef0 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.tsx +++ b/src/components/date_picker/super_date_picker/pretty_duration.tsx @@ -149,13 +149,13 @@ export const useFormatTimeString = ( options?: { locale?: LocaleSpecifier; roundUp?: boolean; - preferLargerRelativeUnits?: boolean; + canRoundRelativeUnits?: boolean; } ): string => { const { locale = 'en', roundUp = false, - preferLargerRelativeUnits = true, + canRoundRelativeUnits = true, } = options || {}; // i18n'd strings @@ -180,7 +180,7 @@ export const useFormatTimeString = ( } if (moment.isMoment(tryParse)) { - if (preferLargerRelativeUnits) { + if (canRoundRelativeUnits) { return `~ ${tryParse.locale(locale).fromNow()}`; } else { // To force a specific unit to be used, we need to skip moment.fromNow() diff --git a/src/components/date_picker/super_date_picker/super_date_picker.test.tsx b/src/components/date_picker/super_date_picker/super_date_picker.test.tsx index 9384d97348d..418089185fa 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.test.tsx +++ b/src/components/date_picker/super_date_picker/super_date_picker.test.tsx @@ -320,7 +320,7 @@ describe('EuiSuperDatePicker', () => { }); }); - describe('preferLargerRelativeUnits', () => { + describe('canRoundRelativeUnits', () => { const props = { onTimeChange: noop, start: 'now-300m', @@ -329,7 +329,7 @@ describe('EuiSuperDatePicker', () => { it('defaults to true, which will round relative units up to the next largest unit', () => { const { getByTestSubject } = render( - + ); fireEvent.click(getByTestSubject('superDatePickerShowDatesButton')); @@ -355,7 +355,7 @@ describe('EuiSuperDatePicker', () => { it('when false, allows preserving the unit set in the start/end time timestamp', () => { const { getByTestSubject } = render( - + ); fireEvent.click(getByTestSubject('superDatePickerShowDatesButton')); diff --git a/src/components/date_picker/super_date_picker/super_date_picker.tsx b/src/components/date_picker/super_date_picker/super_date_picker.tsx index ceed9a315b6..4c7af4498e3 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.tsx +++ b/src/components/date_picker/super_date_picker/super_date_picker.tsx @@ -187,7 +187,7 @@ export type EuiSuperDatePickerProps = CommonProps & { * If you do not want this behavior and instead wish to keep the exact units * input by the user, set this flag to `false`. */ - preferLargerRelativeUnits?: boolean; + canRoundRelativeUnits?: boolean; }; type EuiSuperDatePickerInternalProps = EuiSuperDatePickerProps & { @@ -250,7 +250,7 @@ export class EuiSuperDatePickerInternal extends Component< recentlyUsedRanges: [], refreshInterval: 1000, showUpdateButton: true, - preferLargerRelativeUnits: true, + canRoundRelativeUnits: true, start: 'now-15m', timeFormat: 'HH:mm', width: 'restricted', @@ -478,7 +478,7 @@ export class EuiSuperDatePickerInternal extends Component< isQuickSelectOnly, showUpdateButton, commonlyUsedRanges, - preferLargerRelativeUnits, + canRoundRelativeUnits, timeOptions, dateFormat, refreshInterval, @@ -573,7 +573,7 @@ export class EuiSuperDatePickerInternal extends Component< utcOffset={utcOffset} timeFormat={timeFormat} locale={locale || contextLocale} - preferLargerRelativeUnits={preferLargerRelativeUnits} + canRoundRelativeUnits={canRoundRelativeUnits} isOpen={this.state.isStartDatePopoverOpen} onPopoverToggle={this.onStartDatePopoverToggle} onPopoverClose={this.onStartDatePopoverClose} @@ -594,7 +594,7 @@ export class EuiSuperDatePickerInternal extends Component< utcOffset={utcOffset} timeFormat={timeFormat} locale={locale || contextLocale} - preferLargerRelativeUnits={preferLargerRelativeUnits} + canRoundRelativeUnits={canRoundRelativeUnits} roundUp isOpen={this.state.isEndDatePopoverOpen} onPopoverToggle={this.onEndDatePopoverToggle}