diff --git a/packages/components/src/common/styles-dictionary/css/variables.css b/packages/components/src/common/styles-dictionary/css/variables.css index 71f23aeb8..1e54371d1 100644 --- a/packages/components/src/common/styles-dictionary/css/variables.css +++ b/packages/components/src/common/styles-dictionary/css/variables.css @@ -96,16 +96,16 @@ --sds-color-semantic-base-background-secondary: #f3f3f3; --sds-color-semantic-base-background-secondary-inverse: #3b3b3b; --sds-color-semantic-base-background-tertiary: #dfdfdf; - --sds-color-semantic-base-border: #6c6c6c; - --sds-color-semantic-base-border-disabled: #c3c3c3; - --sds-color-semantic-base-border-disabled-inverse: #6c6c6c; - --sds-color-semantic-base-border-hover: #000000; - --sds-color-semantic-base-border-hover-inverse: #ffffff; - --sds-color-semantic-base-border-inverse: #c3c3c3; + --sds-color-semantic-base-border-primary: #6c6c6c; + --sds-color-semantic-base-border-primary-disabled: #c3c3c3; + --sds-color-semantic-base-border-primary-disabled-inverse: #6c6c6c; + --sds-color-semantic-base-border-primary-hover: #000000; + --sds-color-semantic-base-border-primary-hover-inverse: #ffffff; + --sds-color-semantic-base-border-primary-inverse: #c3c3c3; --sds-color-semantic-base-border-on-fill: #ffffff; - --sds-color-semantic-base-border-pressed: #000000; - --sds-color-semantic-base-border-pressed-inverse: #ffffff; - --sds-color-semantic-base-border-table: #c3c3c3; + --sds-color-semantic-base-border-primary-pressed: #000000; + --sds-color-semantic-base-border-primary-pressed-inverse: #ffffff; + --sds-color-semantic-base-border-secondary: #c3c3c3; --sds-color-semantic-base-divider: #dfdfdf; --sds-color-semantic-base-divider-inverse: #6c6c6c; --sds-color-semantic-base-fill-disabled: #dfdfdf; @@ -606,16 +606,16 @@ --sds-color-semantic-base-background-secondary: #333333; --sds-color-semantic-base-background-secondary-inverse: #ededed; --sds-color-semantic-base-background-tertiary: #494949; - --sds-color-semantic-base-border: #cdcdcd; - --sds-color-semantic-base-border-disabled: #696969; - --sds-color-semantic-base-border-disabled-inverse: #cdcdcd; - --sds-color-semantic-base-border-hover: #ffffff; - --sds-color-semantic-base-border-hover-inverse: #000000; - --sds-color-semantic-base-border-inverse: #696969; + --sds-color-semantic-base-border-primary: #cdcdcd; + --sds-color-semantic-base-border-primary-disabled: #696969; + --sds-color-semantic-base-border-primary-disabled-inverse: #cdcdcd; + --sds-color-semantic-base-border-primary-hover: #ffffff; + --sds-color-semantic-base-border-primary-hover-inverse: #000000; + --sds-color-semantic-base-border-primary-inverse: #696969; --sds-color-semantic-base-border-on-fill: #000000; - --sds-color-semantic-base-border-pressed: #ffffff; - --sds-color-semantic-base-border-pressed-inverse: #000000; - --sds-color-semantic-base-border-table: #696969; + --sds-color-semantic-base-border-primary-pressed: #ffffff; + --sds-color-semantic-base-border-primary-pressed-inverse: #000000; + --sds-color-semantic-base-border-secondary: #696969; --sds-color-semantic-base-divider: #494949; --sds-color-semantic-base-divider-inverse: #cdcdcd; --sds-color-semantic-base-fill-disabled: #494949; diff --git a/packages/components/src/common/styles-dictionary/design-tokens/colors.json b/packages/components/src/common/styles-dictionary/design-tokens/colors.json index 929323479..872f89f80 100644 --- a/packages/components/src/common/styles-dictionary/design-tokens/colors.json +++ b/packages/components/src/common/styles-dictionary/design-tokens/colors.json @@ -98,27 +98,27 @@ "value": "{sds.color.primitive.gray.200.value}", "darkValue": "{sds.color.primitive.gray.200.darkValue}" }, - "border": { + "borderPrimary": { "value": "{sds.color.primitive.gray.600.value}", "darkValue": "{sds.color.primitive.gray.600.darkValue}" }, - "border-disabled": { + "border-primary-disabled": { "value": "{sds.color.primitive.gray.300.value}", "darkValue": "{sds.color.primitive.gray.300.darkValue}" }, - "border-disabled-inverse": { + "border-primary-disabled-inverse": { "value": "{sds.color.primitive.gray.600.value}", "darkValue": "{sds.color.primitive.gray.600.darkValue}" }, - "border-hover": { + "border-primary-hover": { "value": "{sds.color.primitive.gray.900.value}", "darkValue": "{sds.color.primitive.gray.900.darkValue}" }, - "border-hover-inverse": { + "border-primary-hover-inverse": { "value": "{sds.color.primitive.gray.50.value}", "darkValue": "{sds.color.primitive.gray.50.darkValue}" }, - "border-inverse": { + "border-primary-inverse": { "value": "{sds.color.primitive.gray.300.value}", "darkValue": "{sds.color.primitive.gray.300.darkValue}" }, @@ -126,15 +126,15 @@ "value": "{sds.color.primitive.gray.50.value}", "darkValue": "{sds.color.primitive.gray.50.darkValue}" }, - "border-pressed": { + "border-primary-pressed": { "value": "{sds.color.primitive.gray.900.value}", "darkValue": "{sds.color.primitive.gray.900.darkValue}" }, - "border-pressed-inverse": { + "border-primary-pressed-inverse": { "value": "{sds.color.primitive.gray.50.value}", "darkValue": "{sds.color.primitive.gray.50.darkValue}" }, - "border-table": { + "border-secondary": { "value": "{sds.color.primitive.gray.300.value}", "darkValue": "{sds.color.primitive.gray.300.darkValue}" }, diff --git a/packages/components/src/common/styles-dictionary/json/tailwind.json b/packages/components/src/common/styles-dictionary/json/tailwind.json index 3ea037c33..dc958ef1c 100644 --- a/packages/components/src/common/styles-dictionary/json/tailwind.json +++ b/packages/components/src/common/styles-dictionary/json/tailwind.json @@ -1163,16 +1163,16 @@ "sds-color-semantic-base-background-secondary": "#333333", "sds-color-semantic-base-background-secondary-inverse": "#ededed", "sds-color-semantic-base-background-tertiary": "#494949", - "sds-color-semantic-base-border": "#cdcdcd", - "sds-color-semantic-base-border-disabled": "#696969", - "sds-color-semantic-base-border-disabled-inverse": "#cdcdcd", - "sds-color-semantic-base-border-hover": "#ffffff", - "sds-color-semantic-base-border-hover-inverse": "#000000", - "sds-color-semantic-base-border-inverse": "#696969", + "sds-color-semantic-base-border-primary": "#cdcdcd", + "sds-color-semantic-base-border-primary-disabled": "#696969", + "sds-color-semantic-base-border-primary-disabled-inverse": "#cdcdcd", + "sds-color-semantic-base-border-primary-hover": "#ffffff", + "sds-color-semantic-base-border-primary-hover-inverse": "#000000", + "sds-color-semantic-base-border-primary-inverse": "#696969", "sds-color-semantic-base-border-on-fill": "#000000", - "sds-color-semantic-base-border-pressed": "#ffffff", - "sds-color-semantic-base-border-pressed-inverse": "#000000", - "sds-color-semantic-base-border-table": "#696969", + "sds-color-semantic-base-border-primary-pressed": "#ffffff", + "sds-color-semantic-base-border-primary-pressed-inverse": "#000000", + "sds-color-semantic-base-border-secondary": "#696969", "sds-color-semantic-base-divider": "#494949", "sds-color-semantic-base-divider-inverse": "#cdcdcd", "sds-color-semantic-base-fill-disabled": "#494949", @@ -1332,16 +1332,16 @@ "sds-color-semantic-base-background-secondary": "#f3f3f3", "sds-color-semantic-base-background-secondary-inverse": "#3b3b3b", "sds-color-semantic-base-background-tertiary": "#dfdfdf", - "sds-color-semantic-base-border": "#6c6c6c", - "sds-color-semantic-base-border-disabled": "#c3c3c3", - "sds-color-semantic-base-border-disabled-inverse": "#6c6c6c", - "sds-color-semantic-base-border-hover": "#000000", - "sds-color-semantic-base-border-hover-inverse": "#ffffff", - "sds-color-semantic-base-border-inverse": "#c3c3c3", + "sds-color-semantic-base-border-primary": "#6c6c6c", + "sds-color-semantic-base-border-primary-disabled": "#c3c3c3", + "sds-color-semantic-base-border-primary-disabled-inverse": "#6c6c6c", + "sds-color-semantic-base-border-primary-hover": "#000000", + "sds-color-semantic-base-border-primary-hover-inverse": "#ffffff", + "sds-color-semantic-base-border-primary-inverse": "#c3c3c3", "sds-color-semantic-base-border-on-fill": "#ffffff", - "sds-color-semantic-base-border-pressed": "#000000", - "sds-color-semantic-base-border-pressed-inverse": "#ffffff", - "sds-color-semantic-base-border-table": "#c3c3c3", + "sds-color-semantic-base-border-primary-pressed": "#000000", + "sds-color-semantic-base-border-primary-pressed-inverse": "#ffffff", + "sds-color-semantic-base-border-secondary": "#c3c3c3", "sds-color-semantic-base-divider": "#dfdfdf", "sds-color-semantic-base-divider-inverse": "#6c6c6c", "sds-color-semantic-base-fill-disabled": "#dfdfdf", diff --git a/packages/components/src/common/styles-dictionary/scss/_variables.scss b/packages/components/src/common/styles-dictionary/scss/_variables.scss index 843b7ee5d..19c85eaf3 100644 --- a/packages/components/src/common/styles-dictionary/scss/_variables.scss +++ b/packages/components/src/common/styles-dictionary/scss/_variables.scss @@ -187,26 +187,26 @@ $sds-color-semantic-base-background-secondary-inverse: #3b3b3b; $sds-color-semantic-base-background-secondary-inverse-dark: #ededed; $sds-color-semantic-base-background-tertiary: #dfdfdf; $sds-color-semantic-base-background-tertiary-dark: #494949; -$sds-color-semantic-base-border: #6c6c6c; -$sds-color-semantic-base-border-dark: #cdcdcd; -$sds-color-semantic-base-border-disabled: #c3c3c3; -$sds-color-semantic-base-border-disabled-dark: #696969; -$sds-color-semantic-base-border-disabled-inverse: #6c6c6c; -$sds-color-semantic-base-border-disabled-inverse-dark: #cdcdcd; -$sds-color-semantic-base-border-hover: #000000; -$sds-color-semantic-base-border-hover-dark: #ffffff; -$sds-color-semantic-base-border-hover-inverse: #ffffff; -$sds-color-semantic-base-border-hover-inverse-dark: #000000; -$sds-color-semantic-base-border-inverse: #c3c3c3; -$sds-color-semantic-base-border-inverse-dark: #696969; +$sds-color-semantic-base-border-primary: #6c6c6c; +$sds-color-semantic-base-border-primary-dark: #cdcdcd; +$sds-color-semantic-base-border-primary-disabled: #c3c3c3; +$sds-color-semantic-base-border-primary-disabled-dark: #696969; +$sds-color-semantic-base-border-primary-disabled-inverse: #6c6c6c; +$sds-color-semantic-base-border-primary-disabled-inverse-dark: #cdcdcd; +$sds-color-semantic-base-border-primary-hover: #000000; +$sds-color-semantic-base-border-primary-hover-dark: #ffffff; +$sds-color-semantic-base-border-primary-hover-inverse: #ffffff; +$sds-color-semantic-base-border-primary-hover-inverse-dark: #000000; +$sds-color-semantic-base-border-primary-inverse: #c3c3c3; +$sds-color-semantic-base-border-primary-inverse-dark: #696969; $sds-color-semantic-base-border-on-fill: #ffffff; $sds-color-semantic-base-border-on-fill-dark: #000000; -$sds-color-semantic-base-border-pressed: #000000; -$sds-color-semantic-base-border-pressed-dark: #ffffff; -$sds-color-semantic-base-border-pressed-inverse: #ffffff; -$sds-color-semantic-base-border-pressed-inverse-dark: #000000; -$sds-color-semantic-base-border-table: #c3c3c3; -$sds-color-semantic-base-border-table-dark: #696969; +$sds-color-semantic-base-border-primary-pressed: #000000; +$sds-color-semantic-base-border-primary-pressed-dark: #ffffff; +$sds-color-semantic-base-border-primary-pressed-inverse: #ffffff; +$sds-color-semantic-base-border-primary-pressed-inverse-dark: #000000; +$sds-color-semantic-base-border-secondary: #c3c3c3; +$sds-color-semantic-base-border-secondary-dark: #696969; $sds-color-semantic-base-divider: #dfdfdf; $sds-color-semantic-base-divider-dark: #494949; $sds-color-semantic-base-divider-inverse: #6c6c6c; diff --git a/packages/components/src/common/utils.ts b/packages/components/src/common/utils.ts index bd39a9ee3..d7e49d7a3 100644 --- a/packages/components/src/common/utils.ts +++ b/packages/components/src/common/utils.ts @@ -22,3 +22,17 @@ export function filterProps( return result; } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const mergeRefs = (refs: any[]) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (element: any) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(element); + } else if (ref != null) { + ref.current = element; + } + }); + }; +}; diff --git a/packages/components/src/common/warnings.ts b/packages/components/src/common/warnings.ts index ad3b69332..2a615c4a5 100644 --- a/packages/components/src/common/warnings.ts +++ b/packages/components/src/common/warnings.ts @@ -9,6 +9,8 @@ export enum SDSWarningTypes { TooltipSubtitle = "tooltipSubtitle", TooltipWidth = "tooltipWidth", TooltipInvertStyle = "tooltipInvertStyle", + ContentCardActionsOnlyButtons = "contentCardActionsOnlyButtons", + ClickableContentCardNumberOfButtons = "clickableContentCardNumberOfButtons", } export const SDS_WARNINGS = { @@ -58,6 +60,16 @@ export const SDS_WARNINGS = { message: "Warning: Tooltips using the inverted or sdsStyle prop will be deprecated. Please use hasInvertedStyle instead!", }, + [SDSWarningTypes.ContentCardActionsOnlyButtons]: { + hasWarned: false, + message: + "Warning: Only SDS buttons could be used within ContentCard Actions component slot!", + }, + [SDSWarningTypes.ClickableContentCardNumberOfButtons]: { + hasWarned: false, + message: + "Warning: Clickable Content Cards can only have one or no buttons!", + }, }; export const showWarningIfFirstOccurence = (warningType: SDSWarningTypes) => { diff --git a/packages/components/src/core/Autocomplete/components/AutocompleteBase/style.ts b/packages/components/src/core/Autocomplete/components/AutocompleteBase/style.ts index 09f48823b..fe040e810 100644 --- a/packages/components/src/core/Autocomplete/components/AutocompleteBase/style.ts +++ b/packages/components/src/core/Autocomplete/components/AutocompleteBase/style.ts @@ -51,7 +51,7 @@ export const StyledAutocompleteBase = styled(Autocomplete, { // look at the useDetectUserTabbing hook. &[data-user-is-tabbing="true"]:focus-within { border-radius: 4px; - outline: 2px solid ${semanticColors?.base?.borderHover}; + outline: 2px solid ${semanticColors?.base?.borderPrimaryHover}; outline-offset: 1px; } diff --git a/packages/components/src/core/Button/style.ts b/packages/components/src/core/Button/style.ts index 74e69350c..203729352 100644 --- a/packages/components/src/core/Button/style.ts +++ b/packages/components/src/core/Button/style.ts @@ -12,6 +12,7 @@ import { getShadows, getSpaces, fontBodySemiboldXxs, + fontBody, } from "src/core/styles"; import { focusVisibleA11yStyle } from "src/core/styles/common/mixins/a11y"; import { ButtonProps } from "."; @@ -65,7 +66,7 @@ const ButtonStyles = (props: ButtonExtraProps): SerializedStyles => { const disabledBorder = variant === "outlined" - ? `inset 0 0 0 1px ${semanticColors?.base?.borderDisabled}` + ? `inset 0 0 0 1px ${semanticColors?.base?.borderPrimaryDisabled}` : "none"; const boxshadow = @@ -73,6 +74,8 @@ const ButtonStyles = (props: ButtonExtraProps): SerializedStyles => { ? `inset 0 0 0 1px ${semanticColors?.accent?.border}` : "none"; + fontBody("xs", "semibold"); + return css` background-color: ${backgroundColor}; border: none; diff --git a/packages/components/src/core/ContentCard/ContentCard.types.ts b/packages/components/src/core/ContentCard/ContentCard.types.ts new file mode 100644 index 000000000..162a24769 --- /dev/null +++ b/packages/components/src/core/ContentCard/ContentCard.types.ts @@ -0,0 +1,50 @@ +import { CardProps } from "@mui/material"; + +interface BaseContentCardProps extends CardProps { + sdsType?: "wide" | "narrow"; + overlineText?: string; + titleText?: string; + subtitleText?: string; + metadataText?: string; + boundingBox?: boolean; + decorativeBorder?: boolean; + children?: React.ReactNode; + clickableCard?: boolean; + buttonsPosition?: "left" | "right"; +} + +export interface ImageContentCardProps extends BaseContentCardProps { + visualElementType: "image"; + image?: React.ReactNode; + imagePosition?: "left" | "right"; + imagePadding?: boolean; + imageSize?: number; + icon?: never; +} + +export interface IconContentCardProps extends BaseContentCardProps { + visualElementType: "icon"; + icon?: React.ReactNode; + image?: never; + imageSize?: never; + imagePosition?: never; + imagePadding?: never; +} + +export interface NoneContentCardProps extends BaseContentCardProps { + visualElementType: "none"; + icon?: never; + image?: never; + imagePosition?: never; + imagePadding?: never; + imageSize?: never; +} + +export type ContentCardProps = + | ImageContentCardProps + | IconContentCardProps + | NoneContentCardProps; + +export const CONTENT_CARD_DEFAULT_IMAGE_MEDIA_SIZE = 300; +export const CONTENT_CARD_WIDE_TYPE_MIN_WIDTH_LOW_BOUNDARY = 595; +export const CONTENT_CARD_WIDE_TYPE_MIN_WIDTH_HIGH_BOUNDARY = 605; diff --git a/packages/components/src/core/ContentCard/__storybook__/constants.tsx b/packages/components/src/core/ContentCard/__storybook__/constants.tsx new file mode 100644 index 000000000..9dbde2487 --- /dev/null +++ b/packages/components/src/core/ContentCard/__storybook__/constants.tsx @@ -0,0 +1,111 @@ +import { CardMedia } from "@mui/material"; +import Icon from "src/core/Icon"; +import Button from "src/core/Button"; + +export const CONTENT_CARD_EXCLUDED_CONTROLS = [ + "visualElement", + "sdsType", + "imagePosition", + "imagePadding", + "overlineText", + "titleText", + "subtitleText", + "metadataText", + "contentBlock", + "decorativeBorder", + "boundingBox", + "buttons", + "visualElementType", + "image", + "icon", + "buttonsPosition", + "clickableCard", + "imageSize", + "imagePadding", + "imagePosition", +]; + +// Buttons + +export const CONTENT_CARD_BUTTONS_LABELS = [ + "Rounded Primary", + "Rounded Secondary", + "Square Primary", + "Square Secondary", + "Minimal", + "Minimal with Icon", +]; + +export const CONTENT_CARD_BUTTONS_OPTIONS = [ + // Rounded Primary + , + // Rounded Secondary + , + // Square Primary + , + // Square Secondary + , + // Minimal + , + // Minimal with Icon + , +]; + +// Images + +export const CONTENT_CARD_IMAGE_LABELS = [ + "None", + "Placeholder ContentCard", + "Image ContentCard", + "Placeholder Image", + "Image", +]; + +export const CONTENT_CARD_IMAGE_OPTIONS = [ + null, + , + , + "https://placehold.co/300?text=Placeholder Image", + "https://picsum.photos/1000", +]; + +// Icons + +export const CONTENT_CARD_ICON_LABELS = [ + "None", + "Compass Icon", + "Speech Bubbles Icon", +]; + +export const CONTENT_CARD_ICON_OPTIONS = [ + null, + , + , +]; diff --git a/packages/components/src/core/ContentCard/__storybook__/index.stories.tsx b/packages/components/src/core/ContentCard/__storybook__/index.stories.tsx new file mode 100644 index 000000000..185ef24cc --- /dev/null +++ b/packages/components/src/core/ContentCard/__storybook__/index.stories.tsx @@ -0,0 +1,221 @@ +import { Args, Meta } from "@storybook/react"; +import { BADGE } from "@geometricpanda/storybook-addon-badges"; +import { ContentCard } from "./stories/default"; +import { + CONTENT_CARD_BUTTONS_LABELS, + CONTENT_CARD_BUTTONS_OPTIONS, + CONTENT_CARD_EXCLUDED_CONTROLS, + CONTENT_CARD_ICON_LABELS, + CONTENT_CARD_ICON_OPTIONS, + CONTENT_CARD_IMAGE_LABELS, + CONTENT_CARD_IMAGE_OPTIONS, +} from "./constants"; +import { TestDemo } from "./stories/test"; + +export default { + argTypes: { + boundingBox: { + control: { + type: "boolean", + }, + description: + "If true, the card will include a bounding box; otherwise, it will not.", + table: { + defaultValue: { summary: "true" }, + }, + }, + buttons: { + control: { + labels: CONTENT_CARD_BUTTONS_LABELS, + type: "multi-select", + }, + description: + "Select the buttons to display. This is a multi-select field, and you can hold the Cmd (Mac) or Ctrl (Windows) key to select multiple options.", + mapping: CONTENT_CARD_BUTTONS_OPTIONS, + options: Object.keys(CONTENT_CARD_BUTTONS_OPTIONS), + table: { + type: { summary: "ReactNode" }, + }, + }, + buttonsPosition: { + control: { + type: "radio", + }, + description: "Defines the position of the buttons.", + options: ["left", "right"], + table: { + defaultValue: { summary: "left" }, + }, + }, + clickableCard: { + control: { + type: "boolean", + }, + description: + "If set to true, the entire card becomes a clickable button. Note that enabling this prop automatically sets boundingBox to true, and only the first button in the action area will be visible.", + table: { + defaultValue: { summary: "false" }, + }, + }, + decorativeBorder: { + control: { + type: "boolean", + }, + description: + "If true, and imagePadding is also true, the card will display a decorative border on the left for wide cards and on the top for narrow cards.", + table: { + defaultValue: { summary: "false" }, + }, + }, + icon: { + control: { + labels: CONTENT_CARD_ICON_LABELS, + type: "select", + }, + description: + "If visualElementType is set to `icon`, this prop accepts an icon to be displayed in the card’s media section.", + mapping: CONTENT_CARD_ICON_OPTIONS, + options: Object.keys(CONTENT_CARD_ICON_OPTIONS), + table: { + type: { summary: "ReactNode" }, + }, + }, + image: { + control: { + labels: CONTENT_CARD_IMAGE_LABELS, + type: "select", + }, + description: + "If visualElementType is set to `image`, this prop accepts an image to be displayed in the card’s media section.", + mapping: CONTENT_CARD_IMAGE_OPTIONS, + options: Object.keys(CONTENT_CARD_IMAGE_OPTIONS), + table: { + type: { summary: "ReactNode" }, + }, + }, + imagePadding: { + control: { + type: "boolean", + }, + description: + "If true, the card will include a padding to the image; otherwise, it will not.", + table: { + defaultValue: { summary: "false" }, + }, + }, + imagePosition: { + control: { + type: "radio", + }, + description: + "Defines the position of the image. If the image is not set, this prop will have no effect.", + options: ["left", "right"], + table: { + defaultValue: { summary: "left" }, + }, + }, + imageSize: { + control: { + type: "number", + }, + description: + "Defines the size of the image in pixels, serving as the maximum and minimum boundary for the visual element.", + table: { + defaultValue: { summary: "300" }, + }, + }, + metadataText: { + control: { + type: "text", + }, + description: + "The text to display in the metadata section of the card title.", + }, + overlineText: { + control: { + type: "text", + }, + description: + "The text to display in the overline section of the card title.", + }, + sdsType: { + control: { + type: "radio", + }, + description: "Defines the type of card to display.", + options: ["wide", "narrow"], + table: { + defaultValue: { summary: "wide" }, + }, + }, + subtitleText: { + control: { + type: "text", + }, + description: + "The text to display in the subtitle section of the card title.", + }, + titleText: { + control: { + type: "text", + }, + description: "The text to display in the title section of the card.", + }, + visualElementType: { + control: { + type: "select", + }, + description: "The type of media to display in the card.", + options: ["image", "icon", "none"], + table: { + defaultValue: { summary: "none" }, + }, + }, + }, + component: ContentCard, + parameters: { + badges: [BADGE.BETA], + }, + title: "Components/ContentCard [beta]", +} as Meta; + +// Default + +export const Default = { + args: { + boundingBox: true, + buttons: [CONTENT_CARD_BUTTONS_OPTIONS[2], CONTENT_CARD_BUTTONS_OPTIONS[5]], + buttonsPosition: "left", + clickableCard: false, + decorativeBorder: false, + icon: CONTENT_CARD_ICON_OPTIONS[1], + image: CONTENT_CARD_IMAGE_OPTIONS[4], + imagePadding: false, + imagePosition: "left", + metadataText: "Metadata Text", + overlineText: "Overline Text", + sdsType: "wide", + subtitleText: "Subtitle Text", + titleText: "Title Text", + visualElementType: "image", + }, + parameters: { + controls: { + expanded: true, + }, + }, +}; + +// Test + +export const Test = { + parameters: { + controls: { + exclude: CONTENT_CARD_EXCLUDED_CONTROLS, + }, + snapshot: { + skip: true, + }, + }, + render: (args: Args) => , +}; diff --git a/packages/components/src/core/ContentCard/__storybook__/stories/default.tsx b/packages/components/src/core/ContentCard/__storybook__/stories/default.tsx new file mode 100644 index 000000000..0151956db --- /dev/null +++ b/packages/components/src/core/ContentCard/__storybook__/stories/default.tsx @@ -0,0 +1,21 @@ +import { Args } from "@storybook/react"; +import { MEDIUM_LOREM_IPSUM } from "src/common/storybook/loremIpsum"; +import Button from "src/core/Button"; +import RawContentCard, { + ContentCardActions, + ContentCardBody, +} from "src/core/ContentCard"; + +export const ContentCard = (props: Args): JSX.Element => { + const { buttons, visualElementType, ...rest } = props; + + return ( + + {MEDIUM_LOREM_IPSUM} + + + {buttons && buttons.map((button: typeof Button) => button)} + + + ); +}; diff --git a/packages/components/src/core/ContentCard/__storybook__/stories/test.tsx b/packages/components/src/core/ContentCard/__storybook__/stories/test.tsx new file mode 100644 index 000000000..d1fb490a9 --- /dev/null +++ b/packages/components/src/core/ContentCard/__storybook__/stories/test.tsx @@ -0,0 +1,29 @@ +import { Args } from "@storybook/react"; +import { MEDIUM_LOREM_IPSUM } from "src/common/storybook/loremIpsum"; +import Button from "src/core/Button"; +import ContentCard, { + ContentCardActions, + ContentCardBody, +} from "src/core/ContentCard"; +import Icon from "src/core/Icon"; + +export const TestDemo = (props: Args): JSX.Element => { + return ( + } + titleText="Content Card Title" + subtitleText="Content Card Subtitle" + metadataText="Content Card Metadata" + {...props} + > + {MEDIUM_LOREM_IPSUM} + + + + + ); +}; diff --git a/packages/components/src/core/ContentCard/__tests__/ContentCard.namespace-test.tsx b/packages/components/src/core/ContentCard/__tests__/ContentCard.namespace-test.tsx new file mode 100644 index 000000000..31eb136ed --- /dev/null +++ b/packages/components/src/core/ContentCard/__tests__/ContentCard.namespace-test.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { + ContentCard, + ContentCardActions, + ContentCardBody, + ContentCardProps, +} from "@czi-sds/components"; +import Button from "src/core/Button"; + +export const ContentCardNameSpaceTest = (props: ContentCardProps) => { + return ( + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea + commodo consequat. Duis aute irure dolor in reprehenderit in voluptate + velit esse cillum dolore eu fugiat nulla pariatur. + + + + + + + ); +}; diff --git a/packages/components/src/core/ContentCard/__tests__/__snapshots__/index.test.tsx.snap b/packages/components/src/core/ContentCard/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..40e40c5af --- /dev/null +++ b/packages/components/src/core/ContentCard/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContentCard Default story renders snapshot 1`] = ` +
+
+ Content Card Media +
+
+
+
+

+ Overline Text +

+

+ Title Text +

+

+ Subtitle Text +

+
+

+ Metadata Text +

+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Imperdiet massa tincidunt nunc pulvinar sapien et ligula ullamcorper. Urna duis convallis convallis tellus id interdum velit laoreet id. Donec ultrices tincidunt arcu non sodales. Aliquam eleifend mi in nulla posuere. +
+
+ + +
+
+
+
+`; diff --git a/packages/components/src/core/ContentCard/__tests__/index.test.tsx b/packages/components/src/core/ContentCard/__tests__/index.test.tsx new file mode 100644 index 000000000..565845bda --- /dev/null +++ b/packages/components/src/core/ContentCard/__tests__/index.test.tsx @@ -0,0 +1,131 @@ +import { generateSnapshots } from "@chanzuckerberg/story-utils"; +import { composeStories } from "@storybook/react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import * as stories from "../__storybook__/index.stories"; +import Button from "src/core/Button"; +import ContentCardActions from "../components/ContentCardActions"; +import { ContentCardBody } from ".."; + +global.ResizeObserver = class { + observe() {} + + unobserve() {} + + disconnect() {} +}; + +// Returns a component that already contain all decorators from story level, meta level and global level. +const { Test } = composeStories(stories); +const CONTENT_CARD_TEST_ID = "content-card"; + +describe("ContentCard", () => { + generateSnapshots(stories); + + it("renders content card with default props", () => { + render(); + expect(screen.getByTestId(CONTENT_CARD_TEST_ID)).toBeInTheDocument(); + }); + + it("renders content card with image", () => { + render( + + ); + const image = screen.getByRole("img"); + expect(image).toBeInTheDocument(); + }); + + it("renders content card with icon", () => { + render( + Icon} + /> + ); + expect(screen.getByTestId("test-icon")).toBeInTheDocument(); + }); + + it("renders content card with text content", () => { + render( + + ); + expect(screen.getByText("Overline")).toBeInTheDocument(); + expect(screen.getByText("Title")).toBeInTheDocument(); + expect(screen.getByText("Subtitle")).toBeInTheDocument(); + expect(screen.getByText("Metadata")).toBeInTheDocument(); + }); + + it("renders clickable card", () => { + const onClick = jest.fn(); + render(); + const card = screen.getByTestId(CONTENT_CARD_TEST_ID); + fireEvent.click(card); + expect(onClick).toHaveBeenCalled(); + }); + + it("renders content card with buttons", () => { + render( + + +
Content Card Body
+
+ + + + + +
+ ); + expect(screen.getByText("Button")).toBeInTheDocument(); + }); + + it("renders content card with different button positions", () => { + render( + + +
Content Card Body
+
+ + + + + +
+ ); + const button = screen.getByText("Button"); + expect(button).toBeInTheDocument(); + }); + + it("renders content card with different image positions", () => { + render( + + ); + const image = screen.getByRole("img"); + expect(image).toBeInTheDocument(); + }); + + it("renders content card with different types", () => { + render(); + expect(screen.getByTestId(CONTENT_CARD_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/packages/components/src/core/ContentCard/components/ContentCardActions/index.tsx b/packages/components/src/core/ContentCard/components/ContentCardActions/index.tsx new file mode 100644 index 000000000..2645708b3 --- /dev/null +++ b/packages/components/src/core/ContentCard/components/ContentCardActions/index.tsx @@ -0,0 +1,97 @@ +import React, { forwardRef, ReactElement, ReactNode } from "react"; +import { StyledCardActions } from "./style"; +import Button, { ButtonProps } from "src/core/Button"; +import { + SDSWarningTypes, + showWarningIfFirstOccurence, +} from "src/common/warnings"; +import { ContentCardProps } from "../.."; + +export interface ContentCardActionsProps { + buttonsPosition?: "left" | "right"; + clickableCard?: ContentCardProps["clickableCard"]; + children: + | React.ReactElement + | Array>; +} + +const isButtonElement = ( + child: ReactNode +): child is ReactElement => { + if (React.isValidElement(child) && child.type === Button) { + return true; + } else { + showWarningIfFirstOccurence(SDSWarningTypes.ContentCardActionsOnlyButtons); + return false; + } +}; + +/** + * @see https://mui.com/material-ui/api/card-actions/ + */ +const ContentCardActions = forwardRef( + function ContentCardActions( + props: ContentCardActionsProps, + ref + ): JSX.Element | null { + const { buttonsPosition, clickableCard, children } = props; + + /** + * (masoudmanson): + * We need to ensure that only SDS buttons are used within the + * ContentCardActions component slot. This is to prevent any potential + * issues with using other components within the ContentCardActions component slot. + */ + const validChildren = React.Children.toArray( + Array.isArray(children) ? children : [children] + ).filter(isButtonElement); + + /** + * If the card is clickable, it acts as a `