@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
175 lines • 12.3 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
'use client';
import React, { useCallback, useImperativeHandle, useRef } from 'react';
import clsx from 'clsx';
import { useContainerQuery } from '@awsui/component-toolkit';
import { useMergeRefs } from '@awsui/component-toolkit/internal';
import { getAnalyticsMetadataAttribute } from '@awsui/component-toolkit/internal/analytics-metadata';
import { InternalContainerAsSubstep } from '../container/internal';
import { useInternalI18n } from '../i18n/context';
import { AnalyticsFunnelSubStep } from '../internal/analytics/components/analytics-funnel';
import { getBaseProps } from '../internal/base-component';
import { CollectionLabelContext } from '../internal/context/collection-label-context';
import { LinkDefaultVariantContext } from '../internal/context/link-default-variant-context';
import useBaseComponent from '../internal/hooks/use-base-component';
import { useMobile } from '../internal/hooks/use-mobile';
import useMouseDownTarget from '../internal/hooks/use-mouse-down-target';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import InternalLiveRegion from '../live-region/internal';
import InternalStatusIndicator from '../status-indicator/internal';
import { focusMarkers, SelectionControl, useSelection, useSelectionFocusMove, } from '../table/selection';
import stickyScrolling from '../table/sticky-scrolling';
import ToolsHeader from '../table/tools-header';
import { getItemKey } from '../table/utils';
import { getCardsPerRow } from './cards-layout-helper';
import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
const Cards = React.forwardRef(function ({ items = [], cardDefinition, cardsPerRow = [], header, filter, pagination, preferences, empty, loading, loadingText, trackBy, selectedItems, selectionType, isItemDisabled, onSelectionChange, ariaLabels, visibleSections, stickyHeader, stickyHeaderVerticalOffset, variant = 'container', renderAriaLive, firstIndex = 1, totalItemsCount, entireCardClickable, ...rest }, ref) {
const { __internalRootRef } = useBaseComponent('Cards', {
props: { entireCardClickable, selectionType, stickyHeader, variant },
metadata: { usesVisibleSections: !!visibleSections },
});
const baseProps = getBaseProps(rest);
const isRefresh = useVisualRefresh();
const isMobile = useMobile();
const computedVariant = isRefresh ? variant : 'container';
const headerIdRef = useRef(undefined);
const setHeaderRef = useCallback((id) => {
headerIdRef.current = id;
}, []);
const isLabelledByHeader = !(ariaLabels === null || ariaLabels === void 0 ? void 0 : ariaLabels.cardsLabel) && !!header;
const [columns, measureRef] = useContainerQuery(({ contentBoxWidth }) => getCardsPerRow(contentBoxWidth, cardsPerRow), [cardsPerRow]);
const refObject = useRef(null);
const mergedRef = useMergeRefs(measureRef, refObject, __internalRootRef);
const getMouseDownTarget = useMouseDownTarget();
const i18n = useInternalI18n('cards');
const { isItemSelected, getItemSelectionProps } = useSelection({
items,
trackBy,
selectedItems,
selectionType,
isItemDisabled,
onSelectionChange,
ariaLabels: {
itemSelectionLabel: ariaLabels === null || ariaLabels === void 0 ? void 0 : ariaLabels.itemSelectionLabel,
selectionGroupLabel: i18n('ariaLabels.selectionGroupLabel', ariaLabels === null || ariaLabels === void 0 ? void 0 : ariaLabels.selectionGroupLabel),
},
});
const hasToolsHeader = header || filter || pagination || preferences;
const hasFooterPagination = isMobile && variant === 'full-page' && !!pagination;
const headerRef = useRef(null);
const { scrollToTop, scrollToItem } = stickyScrolling(refObject, headerRef);
stickyHeader = !isMobile && stickyHeader;
const onCardFocus = event => {
// When an element inside card receives focus we want to adjust the scroll.
// However, that behavior is unwanted when the focus is received as result of a click
// as it causes the click to never reach the target element.
if (stickyHeader && !event.currentTarget.contains(getMouseDownTarget())) {
scrollToItem(event.currentTarget);
}
};
useImperativeHandle(ref, () => ({
scrollToTop: () => {
if (stickyHeader) {
scrollToTop();
}
},
}), [stickyHeader, scrollToTop]);
let status;
if (loading) {
status = (React.createElement("div", { className: styles.loading },
React.createElement(InternalStatusIndicator, { type: "loading" },
React.createElement(InternalLiveRegion, { tagName: "span" }, loadingText))));
}
else if (empty && !items.length) {
status = React.createElement("div", { className: styles.empty }, empty);
}
const analyticsComponentMetadata = {
name: 'awsui.Cards',
label: `.${analyticsSelectors.container}`,
properties: {
selectionType: selectionType || 'none',
itemsCount: `${items.length}`,
selectedItemsCount: `${(selectedItems || []).length}`,
variant,
},
};
if (trackBy) {
analyticsComponentMetadata.properties.selectedItems = (selectedItems || []).map((item, index) => `${getItemKey(trackBy, item, index)}`);
}
return (React.createElement(LinkDefaultVariantContext.Provider, { value: { defaultVariant: 'primary' } },
React.createElement(AnalyticsFunnelSubStep, null,
React.createElement("div", { ...baseProps, className: clsx(baseProps.className, styles.root), ref: mergedRef, ...getAnalyticsMetadataAttribute({ component: analyticsComponentMetadata }) },
React.createElement(InternalContainerAsSubstep, { header: hasToolsHeader && (React.createElement("div", { className: clsx(styles.header, isRefresh && styles['header-refresh'], styles[`header-variant-${computedVariant}`]) },
React.createElement(CollectionLabelContext.Provider, { value: { assignId: setHeaderRef } },
React.createElement(ToolsHeader, { header: header, filter: filter, pagination: pagination, preferences: preferences })))), footer: hasFooterPagination && React.createElement("div", { className: styles['footer-pagination'] }, pagination), disableContentPaddings: true, disableHeaderPaddings: computedVariant === 'full-page', variant: computedVariant === 'container' ? 'cards' : computedVariant, __stickyHeader: stickyHeader, __stickyOffset: stickyHeaderVerticalOffset, __headerRef: headerRef, __fullPage: computedVariant === 'full-page', __disableFooterDivider: true, className: analyticsSelectors.container },
React.createElement("div", { className: clsx(hasToolsHeader && styles['has-header'], isRefresh && styles.refresh, styles[`header-variant-${computedVariant}`]) },
!!renderAriaLive && !!firstIndex && (React.createElement(InternalLiveRegion, { hidden: true, tagName: "span" },
React.createElement("span", null, renderAriaLive({ totalItemsCount, firstIndex, lastIndex: firstIndex + items.length - 1 })))), status !== null && status !== void 0 ? status : (React.createElement(CardsList, { items: items, cardDefinition: cardDefinition, trackBy: trackBy, selectionType: selectionType, columns: columns, isItemSelected: isItemSelected, getItemSelectionProps: getItemSelectionProps, visibleSections: visibleSections, onFocus: onCardFocus, ariaLabel: ariaLabels === null || ariaLabels === void 0 ? void 0 : ariaLabels.cardsLabel, ariaLabelledby: isLabelledByHeader && headerIdRef.current ? headerIdRef.current : undefined, entireCardClickable: entireCardClickable }))))))));
});
export default Cards;
const CardsList = ({ items, cardDefinition, trackBy, selectionType, columns, isItemSelected, getItemSelectionProps, visibleSections, onFocus, ariaLabelledby, ariaLabel, entireCardClickable, }) => {
const selectable = !!selectionType;
const canClickEntireCard = selectable && entireCardClickable;
const isRefresh = useVisualRefresh();
const { moveFocusDown, moveFocusUp } = useSelectionFocusMove(selectionType, items.length);
let visibleSectionsDefinition = cardDefinition.sections || [];
visibleSectionsDefinition = visibleSections
? visibleSectionsDefinition.filter((section) => section.id && visibleSections.indexOf(section.id) !== -1)
: visibleSectionsDefinition;
let listRole = undefined;
let listItemRole = undefined;
if (selectable) {
listRole = 'group';
listItemRole = 'presentation';
}
return (React.createElement("ol", { className: clsx(styles.list, styles[`list-grid-${columns || 1}`], analyticsSelectors['cards-list']), role: listRole, "aria-labelledby": ariaLabelledby, "aria-label": ariaLabel, ...(focusMarkers && focusMarkers.root) }, items.map((item, index) => {
const key = getItemKey(trackBy, item, index);
const selectionProps = getItemSelectionProps ? getItemSelectionProps(item) : null;
const selected = isItemSelected(item);
const disabled = selectionProps && selectionProps.disabled;
const selectionAnalyticsMetadata = {
action: selected ? 'deselect' : 'select',
detail: {
label: {
selector: `.${analyticsSelectors['cards-list']} li:nth-child(${index + 1}) .${analyticsSelectors['card-header']}`,
root: 'component',
},
position: `${index + 1}`,
item: `${key}`,
},
};
return (React.createElement("li", { className: clsx(styles.card, {
[styles['card-selectable']]: selectable,
[styles['card-selected']]: selectable && selected,
}), key: key, onFocus: onFocus, ...(focusMarkers && focusMarkers.item), role: listItemRole, ...getAnalyticsMetadataAttribute({
component: {
innerContext: {
position: `${index + 1}`,
item: `${key}`,
},
},
}) },
React.createElement("div", { className: clsx(styles['card-inner'], isRefresh && styles.refresh), ...(canClickEntireCard && !disabled ? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata) : {}), onClick: canClickEntireCard
? event => {
var _a;
selectionProps === null || selectionProps === void 0 ? void 0 : selectionProps.onChange();
// Manually move focus to the native input (checkbox or radio button)
(_a = event.currentTarget.querySelector('input')) === null || _a === void 0 ? void 0 : _a.focus();
}
: undefined },
React.createElement("div", { className: styles['card-header'] },
React.createElement("div", { className: clsx(styles['card-header-inner'], analyticsSelectors['card-header']) }, cardDefinition.header ? cardDefinition.header(item) : ''),
selectionProps && (React.createElement("div", { className: styles['selection-control'], ...(!canClickEntireCard && !disabled
? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata)
: {}) },
React.createElement(SelectionControl, { onFocusDown: moveFocusDown, onFocusUp: moveFocusUp, ...selectionProps })))),
visibleSectionsDefinition.map(({ width = 100, header, content, id }, index) => (React.createElement("div", { key: id || index, className: styles.section, style: { width: `${width}%` } },
header ? React.createElement("div", { className: styles['section-header'] }, header) : '',
content ? React.createElement("div", { className: styles['section-content'] }, content(item)) : ''))))));
})));
};
applyDisplayName(Cards, 'Cards');
//# sourceMappingURL=index.js.map