@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
201 lines • 12.4 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import clsx from 'clsx';
import { nodeContains } from '@awsui/component-toolkit/dom';
import { useUniqueId } from '@awsui/component-toolkit/internal';
import InternalBox from '../box/internal';
import { useInternalI18n } from '../i18n/context';
import ChartPlot from '../internal/components/chart-plot';
import ChartPopover from '../internal/components/chart-popover';
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';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { KeyCode } from '../internal/keycode';
import { nodeBelongs } from '../internal/utils/node-belongs';
import InternalLiveRegion from '../live-region/internal';
import Labels from './labels';
import Segments from './segments';
import { defaultDetails, getDimensionsBySize } from './utils';
import styles from './styles.css.js';
export default ({ fitHeight, height: explicitHeight, variant, size, width, i18nStrings, ariaLabel, ariaLabelledby, ariaDescription, innerMetricValue, innerMetricDescription, hideTitles, hideDescriptions, detailPopoverContent, detailPopoverSize, detailPopoverFooter, segmentDescription, highlightedSegment, onHighlightChange, legendSegment, pieData, dataSum, }) => {
var _a, _b;
const [pinnedSegment, setPinnedSegment] = useState(null);
const plotRef = useRef(null);
const containerRef = useRef(null);
const focusedSegmentRef = useRef(null);
const popoverTrackRef = useRef(null);
const popoverRef = useRef(null);
const hasLabels = !(hideTitles && hideDescriptions);
const isRefresh = useVisualRefresh();
const height = (_a = useHeightMeasure(() => { var _a, _b; return (_b = (_a = plotRef.current) === null || _a === void 0 ? void 0 : _a.svg) !== null && _b !== void 0 ? _b : null; }, !fitHeight)) !== null && _a !== void 0 ? _a : explicitHeight;
const dimensions = useMemo(() => getDimensionsBySize({ size: fitHeight ? Math.min(height, width) : size, hasLabels, visualRefresh: isRefresh }), [fitHeight, height, width, size, hasLabels, isRefresh]);
// Inner content is only available for donut charts and the inner description is not displayed for small charts
const hasInnerContent = variant === 'donut' && (innerMetricValue || (innerMetricDescription && dimensions.size !== 'small'));
const innerMetricId = useUniqueId('awsui-pie-chart__inner');
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [popoverData, setPopoverData] = useState();
const highlightedSegmentIndex = useMemo(() => {
for (let index = 0; index < pieData.length; index++) {
if (pieData[index].data.datum === highlightedSegment) {
return index;
}
}
return null;
}, [pieData, highlightedSegment]);
const detailPopoverFooterContent = useMemo(() => (detailPopoverFooter && highlightedSegment ? detailPopoverFooter(highlightedSegment) : null), [detailPopoverFooter, highlightedSegment]);
const i18n = useInternalI18n('pie-chart');
const detailFunction = detailPopoverContent || defaultDetails(i18n, i18nStrings);
const details = popoverData ? detailFunction(popoverData.datum, dataSum) : [];
const popoverContentRef = useRef(null);
const popoverContent = popoverData && React.createElement(SeriesDetails, { details: details, compactList: true, ref: popoverContentRef });
const popoverDismissedRecently = useRef(false);
const escapePressed = useRef(false);
const duringTouch = useRef(false);
const highlightSegment = useCallback((internalDatum) => {
const segment = internalDatum.datum;
if (segment !== highlightedSegment) {
onHighlightChange(segment);
}
if (popoverTrackRef.current) {
setPopoverData({
datum: internalDatum.datum,
series: {
color: internalDatum.color,
index: internalDatum.index,
label: internalDatum.datum.title,
markerType: 'rectangle',
},
trackRef: popoverTrackRef,
});
setPopoverOpen(true);
}
}, [highlightedSegment, setPopoverOpen, onHighlightChange]);
const clearHighlightedSegment = useCallback(() => {
setPopoverOpen(false);
onHighlightChange(null);
}, [onHighlightChange, setPopoverOpen]);
const checkMouseLeave = (event) => {
if (pinnedSegment !== null) {
return;
}
if (nodeContains(popoverRef.current, event.relatedTarget) ||
nodeContains(focusedSegmentRef.current, event.relatedTarget)) {
return;
}
clearHighlightedSegment();
};
useEffect(() => {
const onKeyDown = (event) => {
if (event.key === 'Escape') {
clearHighlightedSegment();
escapePressed.current = true;
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [clearHighlightedSegment]);
const onMouseDown = useCallback((internalDatum) => {
if (pinnedSegment === internalDatum.datum) {
setPinnedSegment(null);
clearHighlightedSegment();
}
else {
setPinnedSegment(internalDatum.datum);
highlightSegment(internalDatum);
}
}, [pinnedSegment, clearHighlightedSegment, setPinnedSegment, highlightSegment]);
const onMouseOver = useCallback((internalDatum) => {
if (escapePressed.current) {
escapePressed.current = false;
return;
}
if (duringTouch.current) {
duringTouch.current = false;
return;
}
if (pinnedSegment !== null) {
return;
}
highlightSegment(internalDatum);
}, [pinnedSegment, highlightSegment]);
const onTouchStart = useCallback(() => {
duringTouch.current = true;
}, []);
const onKeyDown = useCallback((event) => {
const keyCode = event.keyCode;
if (keyCode !== KeyCode.right &&
keyCode !== KeyCode.left &&
keyCode !== KeyCode.enter &&
keyCode !== KeyCode.space) {
return;
}
event.preventDefault();
let nextIndex = highlightedSegmentIndex || 0;
const MAX = pieData.length - 1;
if (keyCode === KeyCode.right) {
nextIndex++;
if (nextIndex > MAX) {
nextIndex = 0;
}
}
else if (keyCode === KeyCode.left) {
nextIndex--;
if (nextIndex < 0) {
nextIndex = MAX;
}
}
if (keyCode === KeyCode.enter || keyCode === KeyCode.space) {
setPinnedSegment(pieData[nextIndex].data.datum);
}
highlightSegment(pieData[nextIndex].data);
}, [setPinnedSegment, highlightSegment, pieData, highlightedSegmentIndex]);
const onApplicationFocus = useCallback((_event, target) => {
if (pinnedSegment !== null || popoverDismissedRecently.current || target === 'mouse') {
return;
}
const segment = highlightedSegment || legendSegment || pieData[0].data.datum;
const matched = pieData.filter(d => d.data.datum === segment);
highlightSegment(matched[0].data);
}, [pinnedSegment, pieData, highlightSegment, highlightedSegment, legendSegment]);
const onApplicationBlur = useCallback((event) => {
const blurTarget = event.relatedTarget || event.target;
if (blurTarget === null || !(blurTarget instanceof Element) || !nodeBelongs(containerRef.current, blurTarget)) {
// We only need to close the popover and remove the pinned segment so that we keep track of the current
// highlighted legendSeries. using clearHighlightedSegment() would set the legendSeries to null, in that case
// using Keyboard Tab will always highlight the first legend item in the legend component.
setPopoverOpen(false);
setPinnedSegment(null);
}
}, [setPinnedSegment]);
const onPopoverDismiss = (outsideClick) => {
setPopoverOpen(false);
setPinnedSegment(null);
if (!outsideClick) {
// The delay is needed to bypass focus events caused by click or keypress needed to unpin the popover.
setTimeout(() => {
popoverDismissedRecently.current = true;
plotRef.current.focusApplication();
popoverDismissedRecently.current = false;
}, 0);
}
else {
onHighlightChange(null);
}
};
return (React.createElement("div", { className: clsx(styles['chart-container'], fitHeight && styles['chart-container--fit-height']), ref: containerRef },
React.createElement("div", { className: clsx(styles['chart-container-chart-plot'], fitHeight && styles['chart-container-chart-plot--fit-height']) },
React.createElement(ChartPlot, { ref: plotRef, width: "100%", height: fitHeight ? '100%' : height, transform: `translate(${width / 2} ${height / 2})`, isPrecise: true, isClickable: !isPopoverOpen, ariaLabel: ariaLabel, ariaLabelledby: ariaLabelledby, ariaDescription: ariaDescription, ariaDescribedby: hasInnerContent ? innerMetricId : undefined, ariaRoleDescription: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.chartAriaRoleDescription, activeElementRef: focusedSegmentRef, activeElementKey: highlightedSegmentIndex === null || highlightedSegmentIndex === void 0 ? void 0 : highlightedSegmentIndex.toString(), onApplicationFocus: onApplicationFocus, onApplicationBlur: onApplicationBlur, onKeyDown: onKeyDown, onMouseOut: checkMouseLeave },
React.createElement(Segments, { pieData: pieData, dimensions: dimensions, variant: variant, focusedSegmentRef: focusedSegmentRef, popoverTrackRef: popoverTrackRef, highlightedSegment: highlightedSegment, segmentAriaRoleDescription: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.segmentAriaRoleDescription, onMouseDown: onMouseDown, onMouseOver: onMouseOver, onTouchStart: onTouchStart }),
hasLabels && (React.createElement(Labels, { pieData: pieData, dimensions: dimensions, segmentDescription: segmentDescription, visibleDataSum: dataSum, hideTitles: hideTitles, hideDescriptions: hideDescriptions, highlightedSegment: highlightedSegment, containerRef: containerRef })))),
hasInnerContent && (React.createElement("div", { className: styles['inner-content'], id: innerMetricId },
innerMetricValue && (React.createElement(InternalBox, { variant: dimensions.size === 'small' ? 'h3' : 'h1', tagOverride: "div", color: "inherit", padding: "n" }, innerMetricValue)),
innerMetricDescription && dimensions.size !== 'small' && (React.createElement(InternalBox, { variant: "h3", color: "text-body-secondary", tagOverride: "div", padding: "n" }, innerMetricDescription)))),
isPopoverOpen && popoverData && (React.createElement(ChartPopover, { ref: popoverRef, title: popoverData.series && (React.createElement(InternalBox, { className: styles['popover-header'], variant: "strong" },
React.createElement(SeriesMarker, { color: popoverData.series.color, type: popoverData.series.markerType }),
' ',
popoverData.series.label)), trackRef: popoverData.trackRef, trackKey: popoverData.series.index, dismissButton: pinnedSegment !== null, dismissAriaLabel: i18nStrings.detailPopoverDismissAriaLabel, onDismiss: onPopoverDismiss, container: ((_b = plotRef.current) === null || _b === void 0 ? void 0 : _b.svg) || null, size: detailPopoverSize, onMouseLeave: checkMouseLeave, onBlur: onApplicationBlur, footer: detailPopoverFooterContent }, popoverContent)),
React.createElement(InternalLiveRegion, { sources: [popoverContentRef] })));
};
//# sourceMappingURL=pie-chart.js.map