diff --git a/pages/attribute-editor/form-field-label.page.tsx b/pages/attribute-editor/form-field-label.page.tsx new file mode 100644 index 0000000000..3d172b76e9 --- /dev/null +++ b/pages/attribute-editor/form-field-label.page.tsx @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState, useCallback, useMemo } from 'react'; +import AttributeEditor, { AttributeEditorProps } from '~components/attribute-editor'; +import { Box, FormField, Input, InputProps, NonCancelableCustomEvent } from '~components'; + +interface Tag { + key?: string; + value?: string; +} + +interface ControlProps extends InputProps { + index: number; + setItems?: any; + prop: keyof Tag; +} + +const i18nStrings = { + addButtonText: 'Add new user', + removeButtonText: 'Remove', + empty: 'No secondary owners assigned to this resource.', +}; + +const Control = React.memo(({ value, index, setItems, prop }: ControlProps) => { + return ( + { + setItems((items: any) => { + const updatedItems = [...items]; + updatedItems[index] = { ...updatedItems[index], [prop]: detail.value }; + return updatedItems; + }); + }} + /> + ); +}); + +export default function AttributeEditorPage() { + const [items, setItems] = useState([{ key: '' }]); + + const definition: AttributeEditorProps.FieldDefinition[] = useMemo( + () => [ + { + control: ({ key = '' }, itemIndex) => , + }, + ], + [] + ); + + const onAddButtonClick = useCallback(() => { + setItems(items => [...items, {}]); + }, []); + + const onRemoveButtonClick = useCallback( + ({ detail: { itemIndex } }: NonCancelableCustomEvent) => { + setItems(items => { + const newItems = items.slice(); + newItems.splice(itemIndex, 1); + return newItems; + }); + }, + [] + ); + + return ( + +

Attribute Editor - Using a form field label

+ + + +
+ ); +} diff --git a/pages/breadcrumb-group/events.page.tsx b/pages/breadcrumb-group/events.page.tsx index 8a9a3481c6..e6973a9645 100644 --- a/pages/breadcrumb-group/events.page.tsx +++ b/pages/breadcrumb-group/events.page.tsx @@ -1,10 +1,20 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; +import { SpaceBetween } from '~components'; import BreadcrumbGroup, { BreadcrumbGroupProps } from '~components/breadcrumb-group'; import ScreenshotArea from '../utils/screenshot-area'; -const items = ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth']; +const items = [ + 'First that is very very very very very very long long long text', + 'Second', + 'Third', + 'Fourth', + 'Fifth', + 'Sixth that is very very very very very very long long long text', +]; + +const shortItems = ['1', '2', '3', '4']; export default function ButtonDropdownPage() { const [onFollowMessage, setOnFollowMessage] = useState(''); @@ -20,16 +30,32 @@ export default function ButtonDropdownPage() {

BreadcrumbGroup variations

- ({ text, href: `#` }))} - onFollow={onFollowCallback} - onClick={onClickCallback} - /> -
-
{onFollowMessage}
-
{onClickMessage}
+ +
+ + ({ text, href: `#` }))} + onFollow={onFollowCallback} + onClick={onClickCallback} + /> +
{onFollowMessage}
+
{onClickMessage}
+
+
+ + ({ text, href: `#` }))} + /> +
+
); diff --git a/pages/date-range-picker/calendar-permutations.page.tsx b/pages/date-range-picker/calendar-permutations.page.tsx index f18a12abf0..b8d580c49d 100644 --- a/pages/date-range-picker/calendar-permutations.page.tsx +++ b/pages/date-range-picker/calendar-permutations.page.tsx @@ -21,19 +21,26 @@ const intervals = [ ['2021-05-10', '2021-05-30'], ]; -const permutations = createPermutations( - intervals.map(([startDate, endDate]) => ({ +const permutations = createPermutations([ + ...intervals.map(([startDate, endDate]) => ({ value: [{ start: { date: startDate, time: '' }, end: { date: endDate, time: '' } }], setValue: [() => {}], locale: ['en-GB'], startOfWeek: [1], isDateEnabled: [() => true], onChange: [() => {}], - timeInputFormat: ['hh:mm:ss'], + timeInputFormat: ['hh:mm:ss'] as const, i18nStrings: [i18nStrings], dateOnly: [false, true], - })) -); + customAbsoluteRangeControl: [undefined], + })), + { + value: [{ start: { date: '', time: '' }, end: { date: '', time: '' } }], + setValue: [() => {}], + i18nStrings: [i18nStrings], + customAbsoluteRangeControl: [() => 'Custom control'], + }, +]); export default function DateRangePickerCalendarPage() { let i = -1; diff --git a/pages/date-range-picker/custom-control.page.tsx b/pages/date-range-picker/custom-control.page.tsx new file mode 100644 index 0000000000..712ff041fd --- /dev/null +++ b/pages/date-range-picker/custom-control.page.tsx @@ -0,0 +1,86 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; +import startOfWeek from 'date-fns/startOfWeek'; +import endOfWeek from 'date-fns/endOfWeek'; +import startOfMonth from 'date-fns/startOfMonth'; +import endOfMonth from 'date-fns/endOfMonth'; +import enLocale from 'date-fns/locale/en-GB'; +import { Box, DateRangePicker, DateRangePickerProps, Link, FormField } from '~components'; +import { i18nStrings, isValid } from './common'; +import { formatDate } from '~components/internal/utils/date-time'; + +export default function DatePickerScenario() { + const [value, setValue] = useState(null); + + return ( + +

Date range picker with custom control

+ + setValue(e.detail.value)} + locale={enLocale.code} + i18nStrings={i18nStrings} + relativeOptions={[]} + placeholder="Filter by a date and time range" + isValidRange={isValid} + rangeSelectorMode="absolute-only" + customAbsoluteRangeControl={(selectedDate, setSelectedDate) => ( + <> + Auto-select:{' '} + { + const today = formatDate(new Date()); + return setSelectedDate({ + start: { date: today, time: '' }, + end: { date: today, time: '' }, + }); + }} + > + 1D + {' '} + + setSelectedDate({ + start: { + date: formatDate(startOfWeek(new Date(), { locale: enLocale })), + time: '', + }, + end: { + date: formatDate(endOfWeek(new Date(), { locale: enLocale })), + time: '', + }, + }) + } + > + 7D + {' '} + + setSelectedDate({ + start: { date: formatDate(startOfMonth(new Date())), time: '' }, + end: { date: formatDate(endOfMonth(new Date())), time: '' }, + }) + } + > + 1M + {' '} + + setSelectedDate({ + start: { date: '', time: '' }, + end: { date: '', time: '' }, + }) + } + > + None + + + )} + /> + +
+ ); +} diff --git a/pages/date-range-picker/range-calendar.page.tsx b/pages/date-range-picker/range-calendar.page.tsx index af4e596942..7652aae99e 100644 --- a/pages/date-range-picker/range-calendar.page.tsx +++ b/pages/date-range-picker/range-calendar.page.tsx @@ -31,6 +31,7 @@ export default function RangeCalendarScenario() { dateOnly={dateOnly} timeInputFormat="hh:mm" isDateEnabled={date => date.getDate() !== 15} + customAbsoluteRangeControl={undefined} /> Focusable element after the range calendar diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 985ca00357..1782083f18 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -4843,6 +4843,27 @@ is provided by its parent form field component. "optional": true, "type": "string", }, + Object { + "description": "Specifies an additional control displayed in the dropdown, located below the range calendar.", + "inlineType": Object { + "name": "DateRangePickerProps.AbsoluteRangeControl", + "parameters": Array [ + Object { + "name": "selectedRange", + "type": "DateRangePickerProps.PendingAbsoluteValue", + }, + Object { + "name": "setSelectedRange", + "type": "React.Dispatch>", + }, + ], + "returnType": "React.ReactNode", + "type": "function", + }, + "name": "customAbsoluteRangeControl", + "optional": true, + "type": "DateRangePickerProps.AbsoluteRangeControl", + }, Object { "defaultValue": "false", "description": "Hides time inputs and changes the input format to date-only, e.g. 2021-04-06. diff --git a/src/attribute-editor/row.tsx b/src/attribute-editor/row.tsx index 19a26c9f17..ef18dfea69 100644 --- a/src/attribute-editor/row.tsx +++ b/src/attribute-editor/row.tsx @@ -88,7 +88,11 @@ export const Row = React.memo( ))} {removable && ( - + row.label)} + > ( +const ButtonContainer = ({ index, children, isNarrowViewport, hasLabel }: ButtonContainer) => (
diff --git a/src/attribute-editor/styles.scss b/src/attribute-editor/styles.scss index dfb8960620..8b2f097c59 100644 --- a/src/attribute-editor/styles.scss +++ b/src/attribute-editor/styles.scss @@ -37,7 +37,7 @@ /* used in test-utils */ } -.button-container { +.button-container-haslabel { // We only support vertical alignment of the remove button for labels with exactly one line. // The value is calculated as follows: // padding-top = awsui-form-field-controls: 4px + @@ -45,6 +45,10 @@ padding-top: calc(#{awsui.$space-xxs} + #{awsui.$font-body-m-line-height}); } +.button-container-nolabel { + padding-top: #{awsui.$space-xxs}; +} + .divider { border-bottom: awsui.$border-divider-section-width solid awsui.$color-border-divider-default; } diff --git a/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts b/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts index 437892b994..d3fe9ce8bc 100644 --- a/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts +++ b/src/breadcrumb-group/__integ__/breadcrumb-group.test.ts @@ -3,6 +3,7 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; import createWrapper from '../../../lib/components/test-utils/selectors'; import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; +import styles from '../../../lib/components/breadcrumb-group/item/styles.selectors.js'; const breadcrumbGroupWrapper = createWrapper().findBreadcrumbGroup(); const dropdownWrapper = breadcrumbGroupWrapper.findDropdown(); @@ -73,4 +74,37 @@ describe('BreadcrumbGroup', () => { await expect(page.getText('#onClickMessage')).resolves.toEqual('OnClick: Second item was selected'); }) ); + + test( + 'Item popover should not show on large screen', + setupTest(async page => { + await page.setWindowSize({ width: 1200, height: 800 }); + await page.click('#focus-target-long-text'); + await page.keys('Tab'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + false + ); + }) + ); + + test( + 'Item popover should show on small screen when text get truncated, and should close pressing Escape', + setupTest(async page => { + await page.setMobileViewport(); + await page.click('#focus-target-long-text'); + await page.keys('Tab'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + true + ); + await page.keys('Escape'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + false + ); + await page.click('#focus-target-short-text'); + await page.keys('Tab'); + await expect(page.isExisting(createWrapper().find(`.${styles['item-popover']}`).toSelector())).resolves.toBe( + false + ); + }) + ); }); diff --git a/src/breadcrumb-group/interfaces.ts b/src/breadcrumb-group/interfaces.ts index f87dfe59cc..b3c83bfbef 100644 --- a/src/breadcrumb-group/interfaces.ts +++ b/src/breadcrumb-group/interfaces.ts @@ -55,6 +55,7 @@ export namespace BreadcrumbGroupProps { export interface BreadcrumbItemProps { item: T; + isDisplayed: boolean; isLast?: boolean; isCompressed?: boolean; onClick?: CancelableEventHandler>; diff --git a/src/breadcrumb-group/internal.tsx b/src/breadcrumb-group/internal.tsx index 826000a376..850d6c17c4 100644 --- a/src/breadcrumb-group/internal.tsx +++ b/src/breadcrumb-group/internal.tsx @@ -101,6 +101,7 @@ export default function InternalBreadcrumbGroup ); diff --git a/src/breadcrumb-group/item/item.tsx b/src/breadcrumb-group/item/item.tsx index 64713b390c..094ccbe592 100644 --- a/src/breadcrumb-group/item/item.tsx +++ b/src/breadcrumb-group/item/item.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { BreadcrumbGroupProps, BreadcrumbItemProps } from '../interfaces'; import InternalIcon from '../../icon/internal'; import styles from './styles.css.js'; @@ -8,11 +8,110 @@ import clsx from 'clsx'; import useFocusVisible from '../../internal/hooks/focus-visible'; import { fireCancelableEvent, isPlainLeftClick } from '../../internal/events'; import { getEventDetail } from '../internal'; +import { Transition } from '../../internal/components/transition'; +import PopoverContainer from '../../popover/container'; +import PopoverBody from '../../popover/body'; +import Portal from '../../internal/components/portal'; +import popoverStyles from '../../popover/styles.css.js'; + +type BreadcrumbItemWithPopoverProps = + React.AnchorHTMLAttributes & { + item: T; + }; + +const BreadcrumbItemWithPopover = ({ + item, + ...anchorAttributes +}: BreadcrumbItemWithPopoverProps) => { + const focusVisible = useFocusVisible(); + const [showPopover, setShowPopover] = useState(false); + const textRef = useRef(null); + const virtualTextRef = useRef(null); + + const isTruncated = (textRef: React.RefObject, virtualTextRef: React.RefObject) => { + if (!textRef || !virtualTextRef || !textRef.current || !virtualTextRef.current) { + return false; + } + const virtualTextWidth = virtualTextRef.current.getBoundingClientRect().width; + const textWidth = textRef.current.getBoundingClientRect().width; + if (virtualTextWidth > textWidth) { + return true; + } + return false; + }; + + const popoverContent = ( + +
+ + {() => ( + ( +
+
+
+
+ )} + > + {}} header={undefined}> + {item.text} + + + )} + +
+ + ); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setShowPopover(false); + } + }; + if (showPopover) { + document.addEventListener('keydown', onKeyDown); + } + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [showPopover]); + + return ( + <> + { + isTruncated(textRef, virtualTextRef) && setShowPopover(true); + }} + onBlur={() => setShowPopover(false)} + onMouseEnter={() => { + isTruncated(textRef, virtualTextRef) && setShowPopover(true); + }} + onMouseLeave={() => setShowPopover(false)} + > + + {item.text} + + + {item.text} + + + {showPopover && popoverContent} + + ); +}; export function BreadcrumbItem({ item, onClick, onFollow, + isDisplayed, isLast = false, isCompressed = false, }: BreadcrumbItemProps) { @@ -24,24 +123,32 @@ export function BreadcrumbItem({ } fireCancelableEvent(onClick, getEventDetail(item), event); }; + + const anchorAttributes: React.AnchorHTMLAttributes = { + href: isLast ? undefined : item.href || '#', + className: clsx(styles.anchor, { [styles.compressed]: isCompressed }), + 'aria-current': isLast ? 'page' : undefined, // Active breadcrumb item is implemented according to WAI-ARIA 1.1 + 'aria-disabled': isLast && 'true', + onClick: isLast ? preventDefault : onClickHandler, + tabIndex: isLast ? 0 : undefined, // tabIndex is added to the last crumb to keep it in the index without an href + }; + return ( -
- - {item.text} - - {!isLast ? ( - - - - ) : null} -
+ <> +
+ {isDisplayed && isCompressed ? ( + + ) : ( + + {item.text} + + )} + {!isLast ? ( + + + + ) : null} +
+ ); } diff --git a/src/breadcrumb-group/item/styles.scss b/src/breadcrumb-group/item/styles.scss index 61b9373631..5561acdce6 100644 --- a/src/breadcrumb-group/item/styles.scss +++ b/src/breadcrumb-group/item/styles.scss @@ -36,7 +36,6 @@ font-weight: styles.$font-weight-bold; text-decoration: none; cursor: default; - pointer-events: none; } } } @@ -50,3 +49,10 @@ display: block; } } +.virtual-item { + @include styles.awsui-util-hide; + visibility: hidden; +} +.item-popover { + /* used in tests */ +} diff --git a/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx b/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx index 1db7aab0ed..e58bcbf87e 100644 --- a/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx +++ b/src/date-range-picker/__tests__/date-range-picker-absolute.test.tsx @@ -469,5 +469,72 @@ describe('Date range picker', () => { ); }); }); + + describe('custom control', () => { + const customControl: DateRangePickerProps.AbsoluteRangeControl = (value, setValue) => ( + <> +
{JSON.stringify(value)}
+ + + + ); + test('renders current value from calendar', () => { + const { wrapper, getByTestId } = renderDateRangePicker({ + ...defaultProps, + value: { type: 'absolute', startDate: '2022-11-24T12:55:00', endDate: '2022-11-28T11:14:00' }, + customAbsoluteRangeControl: customControl, + }); + act(() => wrapper.findTrigger().click()); + expect(getByTestId('display')).toHaveTextContent( + '{"start":{"date":"2022-11-24","time":"12:55:00"},"end":{"date":"2022-11-28","time":"11:14:00"}}' + ); + }); + + test('can update value in calendar', () => { + const { wrapper, getByTestId } = renderDateRangePicker({ + ...defaultProps, + customAbsoluteRangeControl: customControl, + }); + act(() => wrapper.findTrigger().click()); + getByTestId('set-date').click(); + expect(getByTestId('display')).toHaveTextContent( + '{"start":{"date":"2022-01-02","time":"00:00:00"},"end":{"date":"2022-02-06","time":"12:34:56"}}' + ); + expect(wrapper.findDropdown()!.findSelectedStartDate()!.getElement()).toHaveTextContent('2'); + expect(wrapper.findDropdown()!.findStartDateInput()!.findNativeInput().getElement()).toHaveValue('2022/01/02'); + expect(wrapper.findDropdown()!.findStartTimeInput()!.findNativeInput().getElement()).toHaveValue('00:00:00'); + expect(wrapper.findDropdown()!.findSelectedEndDate()!.getElement()).toHaveTextContent('6'); + expect(wrapper.findDropdown()!.findEndDateInput()!.findNativeInput().getElement()).toHaveValue('2022/02/06'); + expect(wrapper.findDropdown()!.findEndTimeInput()!.findNativeInput().getElement()).toHaveValue('12:34:56'); + }); + + test('can clear value in calendar', () => { + const { wrapper, getByTestId } = renderDateRangePicker({ + ...defaultProps, + value: { type: 'absolute', startDate: '2022-11-24T12:55:00', endDate: '2022-11-28T11:14:00' }, + customAbsoluteRangeControl: customControl, + }); + act(() => wrapper.findTrigger().click()); + getByTestId('clear-date').click(); + expect(getByTestId('display')).toHaveTextContent('{"start":{"date":"","time":""},"end":{"date":"","time":""}}'); + expect(wrapper.findDropdown()!.findSelectedStartDate()).toBeNull(); + expect(wrapper.findDropdown()!.findStartDateInput()!.findNativeInput().getElement()).toHaveValue(''); + expect(wrapper.findDropdown()!.findStartTimeInput()!.findNativeInput().getElement()).toHaveValue(''); + expect(wrapper.findDropdown()!.findSelectedEndDate()).toBeNull(); + expect(wrapper.findDropdown()!.findEndDateInput()!.findNativeInput().getElement()).toHaveValue(''); + expect(wrapper.findDropdown()!.findEndTimeInput()!.findNativeInput().getElement()).toHaveValue(''); + }); + }); }); }); diff --git a/src/date-range-picker/calendar/index.tsx b/src/date-range-picker/calendar/index.tsx index 1a9823039e..320a984f34 100644 --- a/src/date-range-picker/calendar/index.tsx +++ b/src/date-range-picker/calendar/index.tsx @@ -4,8 +4,9 @@ import React, { useState } from 'react'; import { addMonths, endOfDay, isAfter, isBefore, isSameMonth, startOfDay, startOfMonth } from 'date-fns'; import styles from '../styles.css.js'; +import SpaceBetween from '../../space-between/internal'; import { BaseComponentProps } from '../../internal/base-component'; -import { DateTimeStrings, PendingAbsoluteValue, RangeCalendarI18nStrings } from '../interfaces'; +import { DateRangePickerProps, RangeCalendarI18nStrings } from '../interfaces'; import CalendarHeader from './header'; import { Grids } from './grids'; import { TimeInputProps } from '../../time-input/interfaces'; @@ -21,14 +22,15 @@ import RangeInputs from './range-inputs.js'; import { findDateToFocus, findMonthToDisplay } from './utils'; export interface DateRangePickerCalendarProps extends BaseComponentProps { - value: PendingAbsoluteValue; - setValue: React.Dispatch>; + value: DateRangePickerProps.PendingAbsoluteValue; + setValue: React.Dispatch>; locale?: string; startOfWeek?: number; isDateEnabled?: (date: Date) => boolean; i18nStrings: RangeCalendarI18nStrings; dateOnly?: boolean; timeInputFormat?: TimeInputProps.Format; + customAbsoluteRangeControl: DateRangePickerProps.AbsoluteRangeControl | undefined; } export default function DateRangePickerCalendar({ @@ -40,6 +42,7 @@ export default function DateRangePickerCalendar({ i18nStrings, dateOnly = false, timeInputFormat = 'hh:mm:ss', + customAbsoluteRangeControl, }: DateRangePickerCalendarProps) { const isSingleGrid = useMobile(); const normalizedLocale = normalizeLocale('DateRangePicker', locale); @@ -60,6 +63,13 @@ export default function DateRangePickerCalendar({ return findDateToFocus(parseDate(value.start.date), currentMonth, isDateEnabled); }); + const updateCurrentMonth = (startDate: string) => { + if (startDate.length >= 8) { + const newCurrentMonth = startOfMonth(parseDate(startDate)); + setCurrentMonth(isSingleGrid ? newCurrentMonth : addMonths(newCurrentMonth, 1)); + } + }; + // recommended to include the start/end time announced with the selection // because the user is not aware of the fact that a start/end time is also set as soon as they select a date const announceStart = (startDate: Date) => { @@ -144,7 +154,10 @@ export default function DateRangePickerCalendar({ } } - const formatValue = (date: Date | null | undefined, previous: DateTimeStrings): DateTimeStrings => { + const formatValue = ( + date: Date | null | undefined, + previous: DateRangePickerProps.DateTimeStrings + ): DateRangePickerProps.DateTimeStrings => { if (date === null) { // explicitly reset to empty return { date: '', time: '' }; @@ -171,12 +184,19 @@ export default function DateRangePickerCalendar({ }; const onChangeStartDate = (value: string) => { - setValue((oldValue: PendingAbsoluteValue) => ({ ...oldValue, start: { ...oldValue.start, date: value } })); + setValue((oldValue: DateRangePickerProps.PendingAbsoluteValue) => ({ + ...oldValue, + start: { ...oldValue.start, date: value }, + })); + updateCurrentMonth(value); + }; - if (value.length >= 8) { - const newCurrentMonth = startOfMonth(parseDate(value)); - setCurrentMonth(isSingleGrid ? newCurrentMonth : addMonths(newCurrentMonth, 1)); - } + const interceptedSetValue: DateRangePickerCalendarProps['setValue'] = newValue => { + setValue(oldValue => { + const updated = typeof newValue === 'function' ? newValue(oldValue) : newValue; + updateCurrentMonth(updated.start.date); + return updated; + }); }; const headingIdPrefix = useUniqueId('date-range-picker-calendar-heading'); @@ -187,53 +207,56 @@ export default function DateRangePickerCalendar({ [styles['one-grid']]: isSingleGrid, })} > -
- + +
+ + + +
- + setValue(oldValue => ({ ...oldValue, start: { ...oldValue.start, time: value } })) + } + endDate={value.end.date} + onChangeEndDate={value => setValue(oldValue => ({ ...oldValue, end: { ...oldValue.end, date: value } }))} + endTime={value.end.time} + onChangeEndTime={value => setValue(oldValue => ({ ...oldValue, end: { ...oldValue.end, time: value } }))} + i18nStrings={i18nStrings} + dateOnly={dateOnly} + timeInputFormat={timeInputFormat} /> -
- - - setValue(oldValue => ({ ...oldValue, start: { ...oldValue.start, time: value } })) - } - endDate={value.end.date} - onChangeEndDate={value => setValue(oldValue => ({ ...oldValue, end: { ...oldValue.end, date: value } }))} - endTime={value.end.time} - onChangeEndTime={value => setValue(oldValue => ({ ...oldValue, end: { ...oldValue.end, time: value } }))} - i18nStrings={i18nStrings} - dateOnly={dateOnly} - timeInputFormat={timeInputFormat} - /> + {customAbsoluteRangeControl &&
{customAbsoluteRangeControl(value, interceptedSetValue)}
} +
{announcement} diff --git a/src/date-range-picker/calendar/utils.ts b/src/date-range-picker/calendar/utils.ts index d18c103f4d..9c1f0d5a85 100644 --- a/src/date-range-picker/calendar/utils.ts +++ b/src/date-range-picker/calendar/utils.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { addMonths, isSameMonth, startOfMonth } from 'date-fns'; -import { DateRangePickerProps, PendingAbsoluteValue } from '../interfaces'; +import { DateRangePickerProps } from '../interfaces'; import { parseDate } from '../../internal/utils/date-time'; export function findDateToFocus( @@ -22,7 +22,7 @@ export function findDateToFocus( return null; } -export function findMonthToDisplay(value: PendingAbsoluteValue, isSingleGrid: boolean) { +export function findMonthToDisplay(value: DateRangePickerProps.PendingAbsoluteValue, isSingleGrid: boolean) { if (value.start.date) { const startDate = parseDate(value.start.date); if (isSingleGrid) { diff --git a/src/date-range-picker/dropdown.tsx b/src/date-range-picker/dropdown.tsx index 5818570ef5..2671efe4fd 100644 --- a/src/date-range-picker/dropdown.tsx +++ b/src/date-range-picker/dropdown.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useRef, useState } from 'react'; -import { DateRangePickerProps, PendingAbsoluteValue } from './interfaces'; +import { DateRangePickerProps } from './interfaces'; import Calendar from './calendar'; import { ButtonProps } from '../button/interfaces'; import { InternalButton } from '../button/internal'; @@ -43,6 +43,7 @@ export interface DateRangePickerDropdownProps ariaLabelledby?: string; ariaDescribedby?: string; + customAbsoluteRangeControl: DateRangePickerProps.AbsoluteRangeControl | undefined; } export function DateRangePickerDropdown({ @@ -63,12 +64,13 @@ export function DateRangePickerDropdown({ rangeSelectorMode, ariaLabelledby, ariaDescribedby, + customAbsoluteRangeControl, }: DateRangePickerDropdownProps) { const [rangeSelectionMode, setRangeSelectionMode] = useState<'absolute' | 'relative'>( getDefaultMode(value, relativeOptions, rangeSelectorMode) ); - const [selectedAbsoluteRange, setSelectedAbsoluteRange] = useState(() => + const [selectedAbsoluteRange, setSelectedAbsoluteRange] = useState(() => splitAbsoluteValue(value?.type === 'absolute' ? value : null) ); @@ -172,6 +174,7 @@ export function DateRangePickerDropdown({ i18nStrings={i18nStrings} dateOnly={dateOnly} timeInputFormat={timeInputFormat} + customAbsoluteRangeControl={customAbsoluteRangeControl} /> )} diff --git a/src/date-range-picker/index.tsx b/src/date-range-picker/index.tsx index 5f4a9ec8e6..33c763ac74 100644 --- a/src/date-range-picker/index.tsx +++ b/src/date-range-picker/index.tsx @@ -103,6 +103,7 @@ const DateRangePicker = React.forwardRef( timeInputFormat = 'hh:mm:ss', expandToViewport = false, rangeSelectorMode = 'default', + customAbsoluteRangeControl, ...rest }: DateRangePickerProps, ref: Ref @@ -273,6 +274,7 @@ const DateRangePicker = React.forwardRef( rangeSelectorMode={rangeSelectorMode} ariaLabelledby={ariaLabelledby} ariaDescribedby={ariaDescribedby} + customAbsoluteRangeControl={customAbsoluteRangeControl} /> )} diff --git a/src/date-range-picker/interfaces.ts b/src/date-range-picker/interfaces.ts index 672954a4f7..c457929ce5 100644 --- a/src/date-range-picker/interfaces.ts +++ b/src/date-range-picker/interfaces.ts @@ -5,6 +5,7 @@ import { FormFieldValidationControlProps } from '../internal/context/form-field- import { NonCancelableEventHandler } from '../internal/events'; import { TimeInputProps } from '../time-input/interfaces'; import { ExpandToViewport } from '../internal/components/dropdown/interfaces'; +import React from 'react'; export interface DateRangePickerBaseProps { /** @@ -152,6 +153,11 @@ export interface DateRangePickerProps * allows the user to clear the selected value. */ showClearButton?: boolean; + + /** + * Specifies an additional control displayed in the dropdown, located below the range calendar. + */ + customAbsoluteRangeControl?: DateRangePickerProps.AbsoluteRangeControl; } export namespace DateRangePickerProps { @@ -218,6 +224,21 @@ export namespace DateRangePickerProps { (date: Date): number; } + export interface DateTimeStrings { + date: string; + time: string; + } + + export interface PendingAbsoluteValue { + start: DateTimeStrings; + end: DateTimeStrings; + } + + export type AbsoluteRangeControl = ( + selectedRange: PendingAbsoluteValue, + setSelectedRange: React.Dispatch> + ) => React.ReactNode; + export type RangeSelectorMode = 'default' | 'absolute-only' | 'relative-only'; export interface Ref { @@ -368,16 +389,6 @@ export namespace DateRangePickerProps { export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; -export interface DateTimeStrings { - date: string; - time: string; -} - -export interface PendingAbsoluteValue { - start: DateTimeStrings; - end: DateTimeStrings; -} - export type RangeCalendarI18nStrings = Pick< DateRangePickerProps.I18nStrings, | 'todayAriaLabel' diff --git a/src/date-range-picker/styles.scss b/src/date-range-picker/styles.scss index 34996abd30..1ba3f571dc 100644 --- a/src/date-range-picker/styles.scss +++ b/src/date-range-picker/styles.scss @@ -38,10 +38,6 @@ $calendar-header-color: awsui.$color-text-body-default; } .calendar { - display: block; - width: 100%; - margin-bottom: awsui.$space-scaled-s; - &-header { display: flex; justify-content: space-between; diff --git a/src/date-range-picker/utils.tsx b/src/date-range-picker/utils.tsx index c2c27bf630..1a2467614c 100644 --- a/src/date-range-picker/utils.tsx +++ b/src/date-range-picker/utils.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { DateRangePickerProps, PendingAbsoluteValue } from './interfaces'; +import { DateRangePickerProps } from './interfaces'; import { setTimeOffset } from './time-offset'; import { joinDateTime, splitDateTime } from '../internal/utils/date-time'; @@ -38,7 +38,9 @@ export function getDefaultMode( return relativeOptions.length > 0 ? 'relative' : 'absolute'; } -export function splitAbsoluteValue(value: null | DateRangePickerProps.AbsoluteValue): PendingAbsoluteValue { +export function splitAbsoluteValue( + value: null | DateRangePickerProps.AbsoluteValue +): DateRangePickerProps.PendingAbsoluteValue { if (!value) { return { start: { date: '', time: '' }, @@ -48,7 +50,9 @@ export function splitAbsoluteValue(value: null | DateRangePickerProps.AbsoluteVa return { start: splitDateTime(value.startDate), end: splitDateTime(value.endDate) }; } -export function joinAbsoluteValue(value: PendingAbsoluteValue): DateRangePickerProps.AbsoluteValue { +export function joinAbsoluteValue( + value: DateRangePickerProps.PendingAbsoluteValue +): DateRangePickerProps.AbsoluteValue { return { type: 'absolute', startDate: joinDateTime(value.start.date, value.start.time || '00:00:00'), diff --git a/src/internal/components/option/index.tsx b/src/internal/components/option/index.tsx index 752ab03919..b64b740930 100644 --- a/src/internal/components/option/index.tsx +++ b/src/internal/components/option/index.tsx @@ -59,7 +59,7 @@ const Option = ({ return (