diff --git a/build-tools/tasks/package-json.js b/build-tools/tasks/package-json.js index c834d9e57c..eed2511ee9 100644 --- a/build-tools/tasks/package-json.js +++ b/build-tools/tasks/package-json.js @@ -23,9 +23,16 @@ function getComponentsExports() { './contexts/form-field': './contexts/form-field.js', // Public internal components './internal/tooltip-do-not-use': './internal/tooltip-do-not-use/index.js', + './internal/do-not-use/chart-filter': './internal/do-not-use/chart-filter.js', + './internal/do-not-use/chart-tooltip': './internal/do-not-use/chart-tooltip.js', + './internal/do-not-use/expandable-section': './internal/do-not-use/expandable-section.js', + './internal/do-not-use/i18n': './internal/do-not-use/i18n.js', + './internal/do-not-use/tooltip': './internal/do-not-use/tooltip.js', './internal/widget-exports': './internal/widget-exports.js', './test-utils/dom/internal/tooltip': './test-utils/dom/internal/tooltip.js', './test-utils/selectors/internal/tooltip': './test-utils/selectors/internal/tooltip.js', + './test-utils/dom/internal/chart-tooltip': './test-utils/dom/internal/chart-tooltip.js', + './test-utils/selectors/internal/chart-tooltip': './test-utils/selectors/internal/chart-tooltip.js', }; let components = listPublicItems('src'); diff --git a/package.json b/package.json index b70d3f38f7..fa8037096d 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ { "path": "lib/components/internal/widget-exports.js", "brotli": false, - "limit": "755 kB", + "limit": "764 kB", "ignore": "react-dom" } ], diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index d613e38f58..c47fe6939d 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -350,6 +350,7 @@ exports[`test-utils selectors 1`] = ` "awsui_application_1fcus", "awsui_axis-label--x_f0fot", "awsui_axis-label--y_f0fot", + "awsui_body_dgs8z", "awsui_button-trigger_18eso", "awsui_button_m5h9f", "awsui_chart-filter_1px7g", @@ -360,8 +361,10 @@ exports[`test-utils selectors 1`] = ` "awsui_dropdown_qwoo0", "awsui_filter-container_z5mul", "awsui_filtering-match-highlight_1p2cx", + "awsui_footer_dgs8z", "awsui_handle_sdha6", "awsui_has-background_15o6u", + "awsui_header_dgs8z", "awsui_highlighted_15o6u", "awsui_inner-list-item_10ipo", "awsui_key_10ipo", diff --git a/src/area-chart/elements/area-chart-filter.tsx b/src/area-chart/elements/area-chart-filter.tsx index 4adae745fe..e40814f90a 100644 --- a/src/area-chart/elements/area-chart-filter.tsx +++ b/src/area-chart/elements/area-chart-filter.tsx @@ -3,6 +3,7 @@ import React, { memo } from 'react'; import ChartFilter from '../../internal/components/chart-filter'; +import ChartSeriesMarker from '../../internal/components/chart-series-marker'; import { AreaChartProps } from '../interfaces'; import { ChartModel } from '../model'; @@ -21,7 +22,11 @@ function AreaChartFilter({ }) { const filterItems = model.allSeries.map(s => { const { title, color, markerType } = model.getInternalSeries(s); - return { label: title, color, type: markerType, datum: s }; + return { + label: title, + marker: , + datum: s, + }; }); return ( diff --git a/src/area-chart/elements/area-chart-legend.tsx b/src/area-chart/elements/area-chart-legend.tsx index c386f8bdcb..a15aa4727a 100644 --- a/src/area-chart/elements/area-chart-legend.tsx +++ b/src/area-chart/elements/area-chart-legend.tsx @@ -3,6 +3,7 @@ import React, { memo, useMemo } from 'react'; import ChartLegend from '../../internal/components/chart-legend'; +import ChartSeriesMarker from '../../internal/components/chart-series-marker'; import { useSelector } from '../async-store'; import { AreaChartProps } from '../interfaces'; import { ChartModel } from '../model'; @@ -24,7 +25,7 @@ function AreaChartLegend({ () => model.series.map(s => { const { title, color, markerType } = model.getInternalSeries(s); - return { label: title, color, type: markerType, datum: s }; + return { label: title, marker: , datum: s }; }), [model] ); diff --git a/src/area-chart/elements/chart-popover.tsx b/src/area-chart/elements/chart-popover.tsx index 87ba0a4d8d..9d31d4fa24 100644 --- a/src/area-chart/elements/chart-popover.tsx +++ b/src/area-chart/elements/chart-popover.tsx @@ -3,7 +3,6 @@ import React from 'react'; import ChartPopover from '../../internal/components/chart-popover'; -import ChartPopoverFooter from '../../internal/components/chart-popover-footer'; import ChartSeriesDetails from '../../internal/components/chart-series-details'; import { AreaChartProps } from '../interfaces'; import { ChartModel } from '../model'; @@ -47,11 +46,11 @@ export default function AreaChartPopover({ dismissAriaLabel={dismissAriaLabel} size={size} onBlur={onBlur} + footer={footer} >
- {footer && {footer}} ); } diff --git a/src/expandable-section/internal.tsx b/src/expandable-section/internal.tsx index 6c7bbdfff8..1b1dab3103 100644 --- a/src/expandable-section/internal.tsx +++ b/src/expandable-section/internal.tsx @@ -5,6 +5,7 @@ import { CSSTransition } from 'react-transition-group'; import clsx from 'clsx'; import { getBaseProps } from '../internal/base-component'; +import { screenReaderTextClass } from '../internal/components/chart-series-details/series-details-text'; import { fireNonCancelableEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { useControllable } from '../internal/hooks/use-controllable'; @@ -18,7 +19,7 @@ import { variantSupportsDescription } from './utils'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; -type InternalExpandableSectionProps = Omit & +export type InternalExpandableSectionProps = Omit & InternalBaseComponentProps & { variant?: InternalVariant; __injectAnalyticsComponentMetadata?: boolean; @@ -109,7 +110,7 @@ export default function InternalExpandableSection({ ['series'] = [ + { label: 'Chocolate', marker:
, datum: datum0 }, + { label: 'Apples', marker:
, datum: datum1 }, +]; const i18nStrings = { filterLabel: 'Filter displayed data', diff --git a/src/internal/components/chart-filter/__tests__/i18n.test.tsx b/src/internal/components/chart-filter/__tests__/i18n.test.tsx index 6cb2e12bdc..5fbf411b84 100644 --- a/src/internal/components/chart-filter/__tests__/i18n.test.tsx +++ b/src/internal/components/chart-filter/__tests__/i18n.test.tsx @@ -4,16 +4,16 @@ import React from 'react'; import { render } from '@testing-library/react'; import TestI18nProvider from '../../../../../lib/components/i18n/testing'; -import ChartFilter from '../../../../../lib/components/internal/components/chart-filter'; +import ChartFilter, { ChartFilterProps } from '../../../../../lib/components/internal/components/chart-filter'; import createWrapper from '../../../../../lib/components/test-utils/dom'; const datum0 = {}; const datum1 = {}; -const series = [ - { label: 'Chocolate', color: 'chocolate', type: 'line', datum: datum0 }, - { label: 'Apples', color: 'red', type: 'rectangle', datum: datum1 }, -] as const; +const series: ChartFilterProps['series'] = [ + { label: 'Chocolate', marker:
, datum: datum0 }, + { label: 'Apples', marker:
, datum: datum1 }, +]; const defaultProps = { series, diff --git a/src/internal/components/chart-filter/index.tsx b/src/internal/components/chart-filter/index.tsx index 4ffa4d2ce7..0cc4fb045c 100644 --- a/src/internal/components/chart-filter/index.tsx +++ b/src/internal/components/chart-filter/index.tsx @@ -9,7 +9,6 @@ import { MultiselectProps } from '../../../multiselect/interfaces'; import InternalMultiselect from '../../../multiselect/internal'; import { BaseComponentProps, getBaseProps } from '../../base-component'; import { NonCancelableEventHandler } from '../../events'; -import SeriesMarker, { ChartSeriesMarkerType } from '../chart-series-marker'; import styles from './styles.css.js'; @@ -21,8 +20,7 @@ interface I18nStrings { interface ChartFilterItem { label: string; - color: string; - type: ChartSeriesMarkerType; + marker: React.ReactNode; datum: T; } @@ -44,11 +42,7 @@ function ChartFilter({ series, i18nStrings, selectedSeries, onChange, ...rest label: d.label, value: '' + i, datum: d.datum, - __customIcon: ( - - - - ), + __customIcon: {d.marker}, })); const selectedOptions = defaultOptions.filter(option => selectedSeries?.indexOf(option.datum) !== -1); diff --git a/src/internal/components/chart-legend/__tests__/chart-legend.test.tsx b/src/internal/components/chart-legend/__tests__/chart-legend.test.tsx index 436707c84a..590661f808 100644 --- a/src/internal/components/chart-legend/__tests__/chart-legend.test.tsx +++ b/src/internal/components/chart-legend/__tests__/chart-legend.test.tsx @@ -12,9 +12,9 @@ import createWrapper from '../../../../../lib/components/test-utils/dom'; import styles from '../../../../../lib/components/internal/components/chart-legend/styles.selectors.js'; const series = [ - { label: 'Chocolate', color: 'chocolate', type: 'line', datum: { title: 's1' } }, - { label: 'Apples', color: 'red', type: 'rectangle', datum: { title: 's2' } }, - { label: 'Oranges', color: 'orange', type: 'rectangle', datum: { title: 's3' } }, + { label: 'Chocolate', marker:
, datum: { title: 's1' } }, + { label: 'Apples', marker:
, datum: { title: 's2' } }, + { label: 'Oranges', marker:
, datum: { title: 's3' } }, ] as const; function getTextContent(wrapper: ElementWrapper) { diff --git a/src/internal/components/chart-legend/index.tsx b/src/internal/components/chart-legend/index.tsx index 11bb14aa3a..45608c1a88 100644 --- a/src/internal/components/chart-legend/index.tsx +++ b/src/internal/components/chart-legend/index.tsx @@ -7,14 +7,12 @@ import InternalBox from '../../../box/internal'; import { useInternalI18n } from '../../../i18n/context'; import { KeyCode } from '../../keycode'; import handleKey from '../../utils/handle-key'; -import SeriesMarker, { ChartSeriesMarkerType } from '../chart-series-marker'; import styles from './styles.css.js'; interface ChartLegendItem { label: string; - color: string; - type: ChartSeriesMarkerType; + marker: React.ReactNode; datum: T; } @@ -142,7 +140,7 @@ function ChartLegend({ onMouseOver={() => handleMouseOver(s.datum)} onMouseLeave={handleMouseLeave} > - {s.label} + {s.marker} {s.label}
); })} diff --git a/src/internal/components/chart-popover-footer/index.tsx b/src/internal/components/chart-popover-footer/index.tsx deleted file mode 100644 index e21ba72656..0000000000 --- a/src/internal/components/chart-popover-footer/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React from 'react'; - -import styles from './styles.css.js'; - -export default function ChartPopoverFooter({ children }: { children: React.ReactNode }) { - return
{children}
; -} diff --git a/src/internal/components/chart-popover-footer/styles.scss b/src/internal/components/chart-popover-footer/styles.scss deleted file mode 100644 index 8aa66c38b4..0000000000 --- a/src/internal/components/chart-popover-footer/styles.scss +++ /dev/null @@ -1,17 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ -@use '../../styles/tokens' as awsui; - -.root { - margin-block-start: awsui.$space-xs; - /* stylelint-disable-next-line selector-max-type */ - > hr { - border-block: none; - border-inline: none; - border-block-start: awsui.$border-divider-section-width solid awsui.$color-border-divider-default; - margin-block: awsui.$space-xs; - margin-inline: 0; - } -} diff --git a/src/internal/components/chart-popover/index.tsx b/src/internal/components/chart-popover/index.tsx index 7497ca5316..3cbbc68821 100644 --- a/src/internal/components/chart-popover/index.tsx +++ b/src/internal/components/chart-popover/index.tsx @@ -14,13 +14,15 @@ import { nodeBelongs } from '../../utils/node-belongs'; import popoverStyles from '../../../popover/styles.css.js'; import styles from './styles.css.js'; +import testClasses from './test-classes/styles.css.js'; -interface ChartPopoverProps extends PopoverProps { +export interface ChartPopoverProps extends PopoverProps { /** Title of the popover */ title?: React.ReactNode; /** References the element the container is positioned against. */ - trackRef: React.RefObject; + trackRef?: React.RefObject; + getTrack?: () => null | HTMLElement | SVGElement; /** Used to update the container position in case track or track position changes: @@ -31,6 +33,7 @@ interface ChartPopoverProps extends PopoverProps { ) */ trackKey?: string | number; + minVisibleBlockSize?: number; /** Optional container element that prevents any clicks in there from dismissing the popover */ container: Element | null; @@ -48,6 +51,9 @@ interface ChartPopoverProps extends PopoverProps { /** Popover content */ children?: React.ReactNode; + + /** Popover footer */ + footer?: React.ReactNode; } export default React.forwardRef(ChartPopover); @@ -61,12 +67,15 @@ function ChartPopover( dismissAriaLabel, children, + footer, title, trackRef, + getTrack, trackKey, onDismiss, container, + minVisibleBlockSize, onMouseEnter, onMouseLeave, @@ -118,7 +127,9 @@ function ChartPopover( fixedWidth={fixedWidth} position={position} trackRef={trackRef} + getTrack={getTrack} trackKey={trackKey} + minVisibleBlockSize={minVisibleBlockSize} arrow={position => (
@@ -128,19 +139,20 @@ function ChartPopover( keepPosition={true} allowVerticalOverflow={true} allowScrollToFit={isPinned} + hoverArea={true} > -
- - {children} - -
+ {title}} + onDismiss={onDismiss} + overflowVisible="content" + className={styles['popover-body']} + variant="chart" + > +
{children}
+ {footer &&
{footer}
} +
); diff --git a/src/internal/components/chart-popover/styles.scss b/src/internal/components/chart-popover/styles.scss index 706ce5f97f..4bc5f69daf 100644 --- a/src/internal/components/chart-popover/styles.scss +++ b/src/internal/components/chart-popover/styles.scss @@ -10,14 +10,20 @@ @include styles.styles-reset; position: absolute; } -.hover-area { - pointer-events: none; - padding-block: awsui.$space-static-s; - padding-inline: awsui.$space-static-s; - margin-block: calc(-1 * #{awsui.$space-static-s}); - margin-inline: calc(-1 * #{awsui.$space-static-s}); -} .popover-body { pointer-events: auto; } + +.footer { + margin-block-start: awsui.$space-static-xs; + + /* stylelint-disable-next-line selector-max-type */ + > hr { + border-block: none; + border-inline: none; + border-block-start: 1px solid awsui.$color-border-divider-default; + margin-block: awsui.$space-static-xs; + margin-inline: 0; + } +} diff --git a/src/internal/components/chart-popover/test-classes/styles.scss b/src/internal/components/chart-popover/test-classes/styles.scss new file mode 100644 index 0000000000..333c7f7e45 --- /dev/null +++ b/src/internal/components/chart-popover/test-classes/styles.scss @@ -0,0 +1,10 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +.header, +.body, +.footer { + /* used in test-utils */ +} \ No newline at end of file diff --git a/src/internal/components/chart-series-details/index.tsx b/src/internal/components/chart-series-details/index.tsx index b384616dbc..16f5126dda 100644 --- a/src/internal/components/chart-series-details/index.tsx +++ b/src/internal/components/chart-series-details/index.tsx @@ -7,7 +7,7 @@ import InternalExpandableSection from '../../../expandable-section/internal'; import { BaseComponentProps, getBaseProps } from '../../base-component'; import { useMergeRefs } from '../../hooks/use-merge-refs'; import ChartSeriesMarker, { ChartSeriesMarkerType } from '../chart-series-marker'; -import getSeriesDetailsText from './series-details-text'; +import getSeriesDetailsText, { screenReaderTextClass } from './series-details-text'; import styles from './styles.css.js'; @@ -123,7 +123,7 @@ function SubItems({ className={clsx( styles['inner-list-item'], styles['key-value-pair'], - (expanded || !expandable) && styles.announced + (expanded || !expandable) && screenReaderTextClass )} > {key} @@ -168,7 +168,7 @@ function ExpandableSeries({ function NonExpandableSeries({ itemKey, value, subItems, markerType, color }: ListItemProps) { return ( <> -
+
{markerType && color && } {itemKey} diff --git a/src/internal/components/chart-series-details/series-details-text.ts b/src/internal/components/chart-series-details/series-details-text.ts index 56cb9fb021..b0e2af4e68 100644 --- a/src/internal/components/chart-series-details/series-details-text.ts +++ b/src/internal/components/chart-series-details/series-details-text.ts @@ -1,12 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import expandabeleSectionHeaderStyles from '../../../expandable-section/styles.css.js'; -import styles from './styles.css.js'; + +export const screenReaderTextClass = 'awsui-screenreader-text'; export default function getSeriesDetailsText(element: HTMLElement) { - const elementsWithText = Array.from( - element.querySelectorAll(`.${styles.announced},.${expandabeleSectionHeaderStyles.header}`) - ); + const elementsWithText = Array.from(element.querySelectorAll(`.${screenReaderTextClass}`)); return elementsWithText .map(element => { if (element instanceof HTMLElement) { diff --git a/src/internal/components/chart-series-details/styles.scss b/src/internal/components/chart-series-details/styles.scss index d46b432876..98bf38db5c 100644 --- a/src/internal/components/chart-series-details/styles.scss +++ b/src/internal/components/chart-series-details/styles.scss @@ -88,7 +88,3 @@ $font-weight-bold: awsui.$font-weight-heading-s; .full-width { inline-size: 100%; } - -.announced { - /* Used by getSeriesDetailText to select text to be announced by screen readers */ -} diff --git a/src/internal/do-not-use/chart-filter.ts b/src/internal/do-not-use/chart-filter.ts new file mode 100644 index 0000000000..7f74b1424b --- /dev/null +++ b/src/internal/do-not-use/chart-filter.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import InternalChartFilter, { ChartFilterProps } from '../components/chart-filter/index.js'; +export type InternalChartFilterProps = ChartFilterProps; +export { InternalChartFilter }; diff --git a/src/internal/do-not-use/chart-tooltip.ts b/src/internal/do-not-use/chart-tooltip.ts new file mode 100644 index 0000000000..6515a9ece0 --- /dev/null +++ b/src/internal/do-not-use/chart-tooltip.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import InternalChartTooltip, { ChartPopoverProps } from '../components/chart-popover/index.js'; +export type InternalChartTooltipProps = ChartPopoverProps; +export { InternalChartTooltip }; diff --git a/src/internal/do-not-use/expandable-section.ts b/src/internal/do-not-use/expandable-section.ts new file mode 100644 index 0000000000..fab69486aa --- /dev/null +++ b/src/internal/do-not-use/expandable-section.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import InternalExpandableSection from '../../expandable-section/internal'; +export type { InternalExpandableSectionProps } from '../../expandable-section/internal'; +export { InternalExpandableSection }; diff --git a/src/internal/do-not-use/i18n.ts b/src/internal/do-not-use/i18n.ts new file mode 100644 index 0000000000..b24e71c9a9 --- /dev/null +++ b/src/internal/do-not-use/i18n.ts @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { useInternalI18n } from '../../i18n/context'; diff --git a/src/internal/do-not-use/tooltip.ts b/src/internal/do-not-use/tooltip.ts new file mode 100644 index 0000000000..7fa0ae38d4 --- /dev/null +++ b/src/internal/do-not-use/tooltip.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import InternalTooltip, { TooltipProps } from '../components/tooltip/index.js'; +export type InternalTooltipProps = TooltipProps; +export { InternalTooltip }; diff --git a/src/internal/tooltip-do-not-use/index.ts b/src/internal/tooltip-do-not-use/index.ts index 5255539171..3748281802 100644 --- a/src/internal/tooltip-do-not-use/index.ts +++ b/src/internal/tooltip-do-not-use/index.ts @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import InternalTooltip_DO_NOT_USE, { TooltipProps } from '../components/tooltip/index.js'; export type InternalTooltipProps = TooltipProps; diff --git a/src/mixed-line-bar-chart/chart-legend.tsx b/src/mixed-line-bar-chart/chart-legend.tsx index 4ee88cd0ce..7a24a8cc87 100644 --- a/src/mixed-line-bar-chart/chart-legend.tsx +++ b/src/mixed-line-bar-chart/chart-legend.tsx @@ -4,6 +4,7 @@ import React, { useMemo } from 'react'; import { ChartFilterProps } from '../internal/components/chart-filter'; import ChartLegend from '../internal/components/chart-legend'; +import ChartSeriesMarker from '../internal/components/chart-series-marker'; import { ChartDataTypes, InternalChartSeries, MixedLineBarChartProps } from './interfaces'; import { chartLegendMap } from './utils'; @@ -31,8 +32,7 @@ export default function InternalChartLegend({ .filter(s => visibleSeries.indexOf(s.series) !== -1) .map(({ series, color }) => ({ label: series.title, - type: chartLegendMap[series.type], - color, + marker: , datum: series, })); }, [series, visibleSeries]); diff --git a/src/mixed-line-bar-chart/chart-popover.tsx b/src/mixed-line-bar-chart/chart-popover.tsx index 541fa319ae..776af505af 100644 --- a/src/mixed-line-bar-chart/chart-popover.tsx +++ b/src/mixed-line-bar-chart/chart-popover.tsx @@ -4,7 +4,6 @@ import React, { useState } from 'react'; import clsx from 'clsx'; import ChartPopover from '../internal/components/chart-popover'; -import ChartPopoverFooter from '../internal/components/chart-popover-footer'; import ChartSeriesDetails, { ExpandedSeries } from '../internal/components/chart-series-details'; import { Transition } from '../internal/components/transition'; import { HighlightDetails } from './format-highlighted'; @@ -67,6 +66,7 @@ function MixedChartPopover( onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onBlur={onBlur} + footer={footer} > ( }) } /> - {footer && {footer}} )}
diff --git a/src/mixed-line-bar-chart/internal.tsx b/src/mixed-line-bar-chart/internal.tsx index a5f01df802..56fdea1810 100644 --- a/src/mixed-line-bar-chart/internal.tsx +++ b/src/mixed-line-bar-chart/internal.tsx @@ -7,6 +7,7 @@ import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; import { getBaseProps } from '../internal/base-component'; import Filter from '../internal/components/chart-filter'; +import ChartSeriesMarker from '../internal/components/chart-series-marker'; import ChartStatusContainer, { getChartStatus } from '../internal/components/chart-status-container'; import { ChartWrapper } from '../internal/components/chart-wrapper'; import { fireNonCancelableEvent } from '../internal/events'; @@ -210,8 +211,7 @@ export default function InternalMixedLineBarChart ({ label: series.title, - type: chartLegendMap[series.type], - color, + marker: , datum: series, })); diff --git a/src/pie-chart/index.tsx b/src/pie-chart/index.tsx index 843cfd05ac..4d99472f4b 100644 --- a/src/pie-chart/index.tsx +++ b/src/pie-chart/index.tsx @@ -7,6 +7,7 @@ import { pie } from 'd3-shape'; import { getBaseProps } from '../internal/base-component'; import Filter, { ChartFilterProps } from '../internal/components/chart-filter'; import Legend, { ChartLegendProps } from '../internal/components/chart-legend'; +import ChartSeriesMarker from '../internal/components/chart-series-marker'; import ChartStatusContainer, { getChartStatus } from '../internal/components/chart-status-container'; import { ChartWrapper } from '../internal/components/chart-wrapper'; import { fireNonCancelableEvent } from '../internal/events'; @@ -105,8 +106,7 @@ const PieChart = function PieChart['series'] = data?.map(data => ({ label: data.datum.title, - color: data.color, - type: 'rectangle', + marker: , datum: data.datum, })); diff --git a/src/pie-chart/pie-chart.tsx b/src/pie-chart/pie-chart.tsx index 4459f754bc..3003723d5d 100644 --- a/src/pie-chart/pie-chart.tsx +++ b/src/pie-chart/pie-chart.tsx @@ -10,7 +10,6 @@ import InternalBox from '../box/internal'; import { useInternalI18n } from '../i18n/context'; import ChartPlot, { ChartPlotRef } from '../internal/components/chart-plot'; import ChartPopover from '../internal/components/chart-popover'; -import ChartPopoverFooter from '../internal/components/chart-popover-footer'; import SeriesDetails from '../internal/components/chart-series-details'; import SeriesMarker from '../internal/components/chart-series-marker'; import { useHeightMeasure } from '../internal/hooks/container-queries/use-height-measure'; @@ -391,9 +390,9 @@ export default ({ size={detailPopoverSize} onMouseLeave={checkMouseLeave} onBlur={onApplicationBlur} + footer={detailPopoverFooterContent} > {popoverContent} - {detailPopoverFooterContent && {detailPopoverFooterContent}} )} diff --git a/src/popover/__tests__/popover-container.test.tsx b/src/popover/__tests__/popover-container.test.tsx new file mode 100644 index 0000000000..5f3995241b --- /dev/null +++ b/src/popover/__tests__/popover-container.test.tsx @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import '../../__a11y__/to-validate-a11y'; +import PopoverContainer from '../../../lib/components/popover/container'; +import * as usePopoverPosition from '../../../lib/components/popover/use-popover-position'; + +const usePopoverPositionSpy = jest.spyOn(usePopoverPosition, 'default'); + +const defaultProps = { position: 'bottom', arrow: () => null, size: 'small', fixedWidth: false } as const; + +afterEach(() => { + usePopoverPositionSpy.mockClear(); +}); + +test('throws when neither trackRef nor getTrack is provided', () => { + expect(() => render(content)).toThrow( + 'Invariant violation: must provide either trackRef or getTrack.' + ); +}); + +test.each([null, document.createElement('div')])('accepts track element with trackRef, track = %s', track => { + render( + + content + + ); + + const getTrack = usePopoverPositionSpy.mock.calls[0][0].getTrack; + expect(getTrack()).toBe(track); +}); + +test.each([null, document.createElement('div')])('accepts track element with getTrack, track = %s', track => { + render( + track}> + content + + ); + + const getTrack = usePopoverPositionSpy.mock.calls[0][0].getTrack; + expect(getTrack()).toBe(track); +}); diff --git a/src/popover/__tests__/positions.test.ts b/src/popover/__tests__/positions.test.ts index 1b3793699e..27592c33bd 100644 --- a/src/popover/__tests__/positions.test.ts +++ b/src/popover/__tests__/positions.test.ts @@ -33,6 +33,27 @@ describe('calculatePosition', () => { }); }); + ( + [ + ['top', { insetBlockStart: 500, insetInlineStart: 500, blockSize: 25, inlineSize: 25 }], + ['bottom', { insetBlockStart: 500, insetInlineStart: 500, blockSize: 25, inlineSize: 25 }], + ] as const + ).forEach(([preferredPosition, trigger]) => { + test(`takes first vertical position for preferredPosition="${preferredPosition}" from priority mapping when matching min height`, () => { + const position = calculatePosition({ + preferredPosition, + trigger, + arrow, + body: { inlineSize: 250, blockSize: 1000 }, + minVisibleBlockSize: 250, + container: viewport, + viewport, + }); + expect(position.internalPosition).toBe(PRIORITY_MAPPING[preferredPosition][0]); + expect(position.scrollable).toBe(true); + }); + }); + ( [ ['top', { insetBlockStart: 500, insetInlineStart: 100, blockSize: 25, inlineSize: 25 }], diff --git a/src/popover/body.scss b/src/popover/body.scss index e4e876b03b..00a296f155 100644 --- a/src/popover/body.scss +++ b/src/popover/body.scss @@ -18,6 +18,11 @@ $header-row-margin-block-end: awsui.$space-xs; &-overflow-visible { overflow: visible; } + + &-variant-chart { + padding-block: awsui.$space-static-s; + padding-inline: awsui.$space-static-s; + } } .has-dismiss { diff --git a/src/popover/body.tsx b/src/popover/body.tsx index 4bb6c647af..345184e7ff 100644 --- a/src/popover/body.tsx +++ b/src/popover/body.tsx @@ -21,7 +21,7 @@ export interface PopoverBodyProps { header: React.ReactNode | undefined; children: React.ReactNode; - variant?: 'annotation'; + variant?: 'annotation' | 'chart'; overflowVisible?: 'content' | 'both'; className?: string; @@ -96,7 +96,7 @@ export default function PopoverBody({ return (
; + trackRef?: React.RefObject; + getTrack?: () => null | HTMLElement | SVGElement; /** Used to update the container position in case track or track position changes: @@ -25,6 +26,7 @@ interface PopoverContainerProps { ) */ trackKey?: string | number; + minVisibleBlockSize?: number; position: PopoverProps.Position; zIndex?: React.CSSProperties['zIndex']; arrow: (position: InternalPosition | null) => React.ReactNode; @@ -41,13 +43,16 @@ interface PopoverContainerProps { allowVerticalOverflow?: boolean; // Whether the popover should be hidden when the trigger is scrolled away. hideOnOverscroll?: boolean; + hoverArea?: boolean; className?: string; } export default function PopoverContainer({ position, trackRef, + getTrack: externalGetTrack, trackKey, + minVisibleBlockSize, arrow, children, zIndex, @@ -59,6 +64,7 @@ export default function PopoverContainer({ allowScrollToFit, allowVerticalOverflow, hideOnOverscroll, + hoverArea, className, }: PopoverContainerProps) { const bodyRef = useRef(null); @@ -68,13 +74,23 @@ export default function PopoverContainer({ const isRefresh = useVisualRefresh(); + const getTrack = useRef(() => { + if (trackRef) { + return trackRef.current; + } + if (externalGetTrack) { + return externalGetTrack(); + } + throw new Error('Invariant violation: must provide either trackRef or getTrack.'); + }); + // Updates the position handler. const { updatePositionHandler, popoverStyle, internalPosition, positionHandlerRef, isOverscrolling } = usePopoverPosition({ popoverRef, bodyRef, arrowRef, - trackRef, + getTrack: getTrack.current, contentRef, allowScrollToFit, allowVerticalOverflow, @@ -82,6 +98,7 @@ export default function PopoverContainer({ renderWithPortal, keepPosition, hideOnOverscroll, + minVisibleBlockSize, }); // Recalculate position when properties change. @@ -109,7 +126,7 @@ export default function PopoverContainer({ keepPosition || // If the click was on the trigger, this will make the popover appear or disappear, // so no need to update its position either in this case. - nodeContains(trackRef.current, event.target) + nodeContains(getTrack.current(), event.target) ) { return; } @@ -153,7 +170,13 @@ export default function PopoverContainer({ [styles[`container-body-variant-${variant}`]]: variant, })} > -
{children}
+ {hoverArea ? ( +
+
{children}
+
+ ) : ( +
{children}
+ )}
); diff --git a/src/popover/styles.scss b/src/popover/styles.scss index 65f236634c..497601353b 100644 --- a/src/popover/styles.scss +++ b/src/popover/styles.scss @@ -89,3 +89,11 @@ $trigger-underline-offset: 0.25em; .popover-inline-content { display: inline; } + +.hover-area { + pointer-events: none; + padding-block: awsui.$space-static-s; + padding-inline: awsui.$space-static-s; + margin-block: calc(-1 * #{awsui.$space-static-s}); + margin-inline: calc(-1 * #{awsui.$space-static-s}); +} diff --git a/src/popover/use-popover-position.ts b/src/popover/use-popover-position.ts index 5b49c316ad..6b6c17a160 100644 --- a/src/popover/use-popover-position.ts +++ b/src/popover/use-popover-position.ts @@ -19,7 +19,7 @@ export default function usePopoverPosition({ popoverRef, bodyRef, arrowRef, - trackRef, + getTrack, contentRef, allowScrollToFit, allowVerticalOverflow, @@ -27,11 +27,12 @@ export default function usePopoverPosition({ renderWithPortal, keepPosition, hideOnOverscroll, + minVisibleBlockSize, }: { popoverRef: React.RefObject; bodyRef: React.RefObject; arrowRef: React.RefObject; - trackRef: React.RefObject; + getTrack: () => null | HTMLElement | SVGElement; contentRef: React.RefObject; allowScrollToFit?: boolean; allowVerticalOverflow?: boolean; @@ -39,6 +40,7 @@ export default function usePopoverPosition({ renderWithPortal?: boolean; keepPosition?: boolean; hideOnOverscroll?: boolean; + minVisibleBlockSize?: number; }) { const previousInternalPositionRef = useRef(null); const [popoverStyle, setPopoverStyle] = useState>({}); @@ -52,7 +54,8 @@ export default function usePopoverPosition({ const updatePositionHandler = useCallback( (onContentResize = false) => { - if (!trackRef.current || !popoverRef.current || !bodyRef.current || !contentRef.current || !arrowRef.current) { + const track = getTrack(); + if (!track || !popoverRef.current || !bodyRef.current || !contentRef.current || !arrowRef.current) { return; } @@ -61,7 +64,6 @@ export default function usePopoverPosition({ const body = bodyRef.current; const arrow = arrowRef.current; const document = popover.ownerDocument; - const track = trackRef.current; // If the popover body isn't being rendered for whatever reason (e.g. "display: none" or JSDOM), // or track does not belong to the document - bail on calculating dimensions. @@ -130,6 +132,7 @@ export default function usePopoverPosition({ viewport: viewportRect, renderWithPortal, allowVerticalOverflow, + minVisibleBlockSize, }); // Get the position of the popover relative to the containing block. @@ -168,14 +171,18 @@ export default function usePopoverPosition({ scrollRectangleIntoView(rect, scrollableParent); } - if (hideOnOverscroll && trackRef.current instanceof HTMLElement) { - const scrollableContainer = getFirstScrollableParent(trackRef.current); + if (hideOnOverscroll && track instanceof HTMLElement) { + const scrollableContainer = getFirstScrollableParent(track); if (scrollableContainer) { scrollableContainerRectRef.current = getLogicalBoundingClientRect(scrollableContainer); } } positionHandlerRef.current = () => { + const track = getTrack(); + if (!track) { + return; + } const trackRect = getLogicalBoundingClientRect(track); const newTrackOffset = toRelativePosition( @@ -197,7 +204,7 @@ export default function usePopoverPosition({ }; }, [ - trackRef, + getTrack, popoverRef, bodyRef, contentRef, @@ -208,6 +215,7 @@ export default function usePopoverPosition({ allowVerticalOverflow, allowScrollToFit, hideOnOverscroll, + minVisibleBlockSize, ] ); return { updatePositionHandler, popoverStyle, internalPosition, positionHandlerRef, isOverscrolling }; diff --git a/src/popover/utils/positions.ts b/src/popover/utils/positions.ts index 82dbd4d3d3..770102336e 100644 --- a/src/popover/utils/positions.ts +++ b/src/popover/utils/positions.ts @@ -239,6 +239,7 @@ export function calculatePosition({ // the popover is only bound by the viewport if it is rendered in a portal renderWithPortal, allowVerticalOverflow, + minVisibleBlockSize, }: { preferredPosition: PopoverProps.Position; fixedInternalPosition?: InternalPosition; @@ -250,6 +251,7 @@ export function calculatePosition({ // the popover is only bound by the viewport if it is rendered in a portal renderWithPortal?: boolean; allowVerticalOverflow?: boolean; + minVisibleBlockSize?: number; }): CalculatedPosition { let bestOption: CandidatePosition | null = null; @@ -265,11 +267,17 @@ export function calculatePosition({ ? getIntersection([rect, viewport]) : getIntersection([rect, viewport, container]); - const fitsWithoutOverflow = - visibleArea && visibleArea.inlineSize === body.inlineSize && visibleArea.blockSize === body.blockSize; + // When min visible block size is set, the popover is considered fitting the container if the available space + // is the same or larger than min allowed, even if it means the scrollbar is needed. + const fitsBlockSize = + minVisibleBlockSize === undefined + ? visibleArea && visibleArea.blockSize === body.blockSize + : visibleArea && visibleArea.blockSize >= Math.min(body.blockSize, minVisibleBlockSize); + const fitsInlineSize = visibleArea && visibleArea.inlineSize === body.inlineSize; - if (fitsWithoutOverflow) { - return { internalPosition, rect }; + if (fitsBlockSize && fitsInlineSize) { + const scrollable = visibleArea && visibleArea.blockSize < body.blockSize; + return { internalPosition, rect: scrollable ? fitIntoContainer(rect, viewport) : rect, scrollable }; } const newOption = { rect, internalPosition, visibleArea }; diff --git a/src/test-utils/dom/internal/chart-tooltip.ts b/src/test-utils/dom/internal/chart-tooltip.ts new file mode 100644 index 0000000000..d669235de4 --- /dev/null +++ b/src/test-utils/dom/internal/chart-tooltip.ts @@ -0,0 +1,29 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; + +import ButtonWrapper from '../button'; + +import styles from '../../../internal/components/chart-popover/styles.selectors.js'; +import testClasses from '../../../internal/components/chart-popover/test-classes/styles.selectors.js'; +import popoverStyles from '../../../popover/styles.selectors.js'; + +export default class ChartTooltipWrapper extends ComponentWrapper { + static rootSelector: string = styles.root; + + findHeader(): ElementWrapper | null { + return this.findByClassName(testClasses.header); + } + + findBody(): ElementWrapper | null { + return this.findByClassName(testClasses.body); + } + + findFooter(): ElementWrapper | null { + return this.findByClassName(testClasses.footer); + } + + findDismissButton(): ButtonWrapper | null { + return this.findComponent(`.${popoverStyles['dismiss-control']}`, ButtonWrapper); + } +}