diff --git a/src/cards/index.tsx b/src/cards/index.tsx index 93207b5624..d31e7887f0 100644 --- a/src/cards/index.tsx +++ b/src/cards/index.tsx @@ -12,6 +12,7 @@ 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 Card from '../internal/card'; 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'; @@ -269,7 +270,6 @@ const CardsList = ({ }) => { const selectable = !!selectionType; const canClickEntireCard = selectable && entireCardClickable; - const isRefresh = useVisualRefresh(); const { moveFocusDown, moveFocusUp } = useSelectionFocusMove(selectionType, items.length); @@ -315,60 +315,62 @@ const CardsList = ({ }, }; return ( -
  • + + + ) + } + active={selectable && selected} + className={styles.card} + header={ +
    + {cardDefinition.header ? cardDefinition.header(item) : ''} +
    + } + innerMetadataAttributes={ + entireCardClickable && !disabled ? getAnalyticsMetadataAttribute(selectionAnalyticsMetadata) : {} + } + key={index} + metadataAttributes={{ + ...getAnalyticsMetadataAttribute({ + component: { + innerContext: { + position: `${index + 1}`, + item: `${key}`, + }, + }, + }), + ...focusMarkers.item, + role: listItemRole, + }} + onClick={ + canClickEntireCard + ? event => { + selectionProps?.onChange(); + // Manually move focus to the native input (checkbox or radio button) + event.currentTarget.querySelector('input')?.focus(); + } + : undefined + } onFocus={onFocus} - {...(focusMarkers && focusMarkers.item)} role={listItemRole} - {...getAnalyticsMetadataAttribute({ - component: { - innerContext: { - position: `${index + 1}`, - item: `${key}`, - }, - }, - })} + TagName="li" > -
    { - selectionProps?.onChange(); - // Manually move focus to the native input (checkbox or radio button) - event.currentTarget.querySelector('input')?.focus(); - } - : undefined - } - > -
    -
    - {cardDefinition.header ? cardDefinition.header(item) : ''} -
    - {selectionProps && ( -
    - -
    - )} + {visibleSectionsDefinition.map(({ width = 100, header, content, id }, index) => ( +
    + {header ?
    {header}
    : ''} + {content ?
    {content(item)}
    : ''}
    - {visibleSectionsDefinition.map(({ width = 100, header, content, id }, index) => ( -
    - {header ?
    {header}
    : ''} - {content ?
    {content(item)}
    : ''} -
    - ))} -
    -
  • + ))} + ); })} diff --git a/src/cards/styles.scss b/src/cards/styles.scss index b2075f281b..52f98ba971 100644 --- a/src/cards/styles.scss +++ b/src/cards/styles.scss @@ -7,47 +7,9 @@ @use '../internal/styles' as styles; @use '../internal/styles/tokens' as awsui; -@use '../container/shared' as container; -@use './motion'; - -@mixin card-style { - border-start-start-radius: awsui.$border-radius-container; - border-start-end-radius: awsui.$border-radius-container; - border-end-start-radius: awsui.$border-radius-container; - border-end-end-radius: awsui.$border-radius-container; - box-sizing: border-box; - - &::before { - @include styles.base-pseudo-element; - // Reset border color to prevent it from flashing black during card selection animation - border-color: transparent; - border-block-start: awsui.$border-container-top-width solid awsui.$color-border-container-top; - border-start-start-radius: awsui.$border-radius-container; - border-start-end-radius: awsui.$border-radius-container; - border-end-start-radius: awsui.$border-radius-container; - border-end-end-radius: awsui.$border-radius-container; - z-index: 1; - } - - &::after { - @include styles.base-pseudo-element; - border-start-start-radius: awsui.$border-radius-container; - border-start-end-radius: awsui.$border-radius-container; - border-end-start-radius: awsui.$border-radius-container; - border-end-end-radius: awsui.$border-radius-container; - } - &:not(.refresh)::after { - box-shadow: awsui.$shadow-container; - } - &.refresh::after { - border-block: solid awsui.$border-divider-section-width awsui.$color-border-divider-default; - border-inline: solid awsui.$border-divider-section-width awsui.$color-border-divider-default; - } -} .root { @include styles.styles-reset(); - @include styles.default-text-style; } .header { @@ -111,49 +73,8 @@ } } -.card { - display: flex; - overflow-wrap: break-word; - word-wrap: break-word; - margin-block: 0; - margin-inline: 0; - padding-block: 0; - padding-inline: 0; - list-style: none; - &-inner { - position: relative; - background-color: awsui.$color-background-container-content; - margin-block-start: 0; - margin-block-end: awsui.$space-grid-gutter; - margin-inline-start: awsui.$space-grid-gutter; - margin-inline-end: 0; - padding-block: awsui.$space-card-vertical; - padding-inline: awsui.$space-card-horizontal; - inline-size: 100%; - min-inline-size: 0; - @include card-style; - } - &-header { - @include styles.font-heading-m; - &-inner { - inline-size: 100%; - display: inline-block; - } - } - &-selectable { - > .card-inner > .card-header { - inline-size: 90%; - } - } - &-selected { - > .card-inner { - background-color: awsui.$color-background-item-selected; - &::before { - border-block: awsui.$border-item-width solid awsui.$color-border-item-selected; - border-inline: awsui.$border-item-width solid awsui.$color-border-item-selected; - } - } - } +.card-header { + @include styles.font-heading-m; } .section { diff --git a/src/internal/card/index.tsx b/src/internal/card/index.tsx new file mode 100644 index 0000000000..a912566a6d --- /dev/null +++ b/src/internal/card/index.tsx @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import clsx from 'clsx'; + +import { useVisualRefresh } from '../hooks/use-visual-mode'; +import { InternalCardProps } from './interfaces'; + +import styles from './styles.css.js'; + +export default function Card({ + action, + active, + children, + className, + header, + innerMetadataAttributes, + metadataAttributes, + onClick, + onFocus, + role, + TagName = 'div', +}: InternalCardProps) { + const isRefresh = useVisualRefresh(); + + return ( + +
    +
    + {header} + {action &&
    {action}
    } +
    + {children} +
    +
    + ); +} diff --git a/src/internal/card/interfaces.ts b/src/internal/card/interfaces.ts new file mode 100644 index 0000000000..28bcb06ee4 --- /dev/null +++ b/src/internal/card/interfaces.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { FocusEventHandler } from 'react'; + +import { BaseComponentProps } from '../base-component'; + +export interface InternalCardProps extends BaseComponentProps { + /** + * Specifies an action for the card. + * It is recommended to use a button with inline-icon variant. + */ + action?: React.ReactNode; + + /** + * Specifies whether the card is in active state. + */ + active?: boolean; + + /** + * Optional URL for an image which will be displayed cropped as a background of the card. + * When this property is used, a dark gradient is overlayed and the text above defaults to bright colors. + * Make sure that any content you place on the card has sufficient contrast with the overlayed image behind. + */ + imageUrl?: string; + + /** + * Primary content displayed in the card. + */ + children?: React.ReactNode; + + /** + * Heading text. + */ + header?: React.ReactNode; + + /** + * Icon which will be displayed at the top of the card, + * inline at the start of the content. + */ + icon?: React.ReactNode; + + /** + * Called when the user clicks on the card. + */ + onClick?: React.MouseEventHandler; + + onFocus?: FocusEventHandler; + + role?: string; + + TagName?: 'li' | 'div'; + + metadataAttributes: Record; + + innerMetadataAttributes: Record; +} diff --git a/src/cards/motion.scss b/src/internal/card/motion.scss similarity index 89% rename from src/cards/motion.scss rename to src/internal/card/motion.scss index 385aa1d4b2..c7bb058576 100644 --- a/src/cards/motion.scss +++ b/src/internal/card/motion.scss @@ -3,8 +3,8 @@ SPDX-License-Identifier: Apache-2.0 */ -@use '../internal/styles' as styles; -@use '../internal/styles/tokens' as awsui; +@use '../styles' as styles; +@use '../styles/tokens' as awsui; .card-inner { @include styles.with-motion { diff --git a/src/internal/card/styles.scss b/src/internal/card/styles.scss new file mode 100644 index 0000000000..2b4822b7f1 --- /dev/null +++ b/src/internal/card/styles.scss @@ -0,0 +1,94 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use 'sass:math'; + +@use '../styles' as styles; +@use '../styles/tokens' as awsui; +@use './motion'; + +@mixin card-style { + border-start-start-radius: awsui.$border-radius-container; + border-start-end-radius: awsui.$border-radius-container; + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + box-sizing: border-box; + + &::before { + @include styles.base-pseudo-element; + // Reset border color to prevent it from flashing black during card selection animation + border-color: transparent; + border-block-start: awsui.$border-container-top-width solid awsui.$color-border-container-top; + border-start-start-radius: awsui.$border-radius-container; + border-start-end-radius: awsui.$border-radius-container; + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + z-index: 1; + } + + &::after { + @include styles.base-pseudo-element; + border-start-start-radius: awsui.$border-radius-container; + border-start-end-radius: awsui.$border-radius-container; + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + } + &:not(.refresh)::after { + box-shadow: awsui.$shadow-container; + } + &.refresh::after { + border-block: solid awsui.$border-divider-section-width awsui.$color-border-divider-default; + border-inline: solid awsui.$border-divider-section-width awsui.$color-border-divider-default; + } +} + +.root { + @include styles.styles-reset(); +} + +.card { + display: flex; + overflow-wrap: break-word; + word-wrap: break-word; + margin-block: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; + list-style: none; + &-inner { + position: relative; + background-color: awsui.$color-background-container-content; + margin-block-start: 0; + margin-block-end: awsui.$space-grid-gutter; + margin-inline-start: awsui.$space-grid-gutter; + margin-inline-end: 0; + padding-block: awsui.$space-card-vertical; + padding-inline: awsui.$space-card-horizontal; + inline-size: 100%; + min-inline-size: 0; + @include card-style; + } + &-header { + @include styles.font-heading-m; + &-inner { + inline-size: 100%; + display: inline-block; + } + } + &-with-action { + > .card-inner > .card-header { + inline-size: 90%; + } + } + &-selected { + > .card-inner { + background-color: awsui.$color-background-item-selected; + &::before { + border-block: awsui.$border-item-width solid awsui.$color-border-item-selected; + border-inline: awsui.$border-item-width solid awsui.$color-border-item-selected; + } + } + } +}