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 (
-
+ <>
+
+ {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)}
+
+ setValue({
+ start: { date: '2022-01-02', time: '00:00:00' },
+ end: { date: '2022-02-06', time: '12:34:56' },
+ })
+ }
+ >
+ setValue({ start: { date: '', time: '' }, end: { date: '', time: '' } })}
+ >
+ >
+ );
+ 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)}
}
+