From f75a7aeeda4327aeba340907ad83d5da20ab1a24 Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Thu, 16 Nov 2023 13:10:45 -0500 Subject: [PATCH 1/9] docs(Tile): scaffold slug story, add demo styles --- .../react/src/components/Tile/Tile.stories.js | 75 ++++++++++++++++++- .../react/src/components/Tile/tile-story.scss | 54 +++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/Tile/Tile.stories.js b/packages/react/src/components/Tile/Tile.stories.js index dd7de3a3d573..08cf8870ac29 100644 --- a/packages/react/src/components/Tile/Tile.stories.js +++ b/packages/react/src/components/Tile/Tile.stories.js @@ -23,7 +23,7 @@ import { TileBelowTheFoldContent, } from './'; import TileGroup from '../TileGroup/TileGroup'; -import { Download } from '@carbon/icons-react'; +import { Download, ArrowRight } from '@carbon/icons-react'; export default { title: 'Components/Tile', @@ -57,6 +57,79 @@ export const Default = () => { ); }; +export const SlugTest = () => { + return ( +
+ +

Title

+

+ Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit at + consectetur turpis mauris gravida penatibus. +

+
+
+

Data Quality

+

85%

+
+
+

Label text

+

16%

+
+
+ +
+ +

Title

+

+ Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit at + consectetur turpis mauris gravida penatibus. +

+
+
+

Data Quality

+

85%

+
+
+

Label text

+

16%

+
+
+
+ + +

Title

+

+ Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit + at consectetur turpis mauris gravida penatibus. +

+
+
+

Data Quality

+

85%

+
+
+

Label text

+

16%

+
+
+
+ +
Expanded Section
+

+ Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit + at consectetur turpis mauris. +

+
+
+
+ ); +}; + export const DefaultWithLayer = () => ( {(layer) => ( diff --git a/packages/react/src/components/Tile/tile-story.scss b/packages/react/src/components/Tile/tile-story.scss index 58b453a26748..7c7df8a90d8f 100644 --- a/packages/react/src/components/Tile/tile-story.scss +++ b/packages/react/src/components/Tile/tile-story.scss @@ -1,4 +1,5 @@ @use '@carbon/react/scss/components/tile'; +@use '@carbon/react/scss/type'; .experimental-tile { @include tile.tile($enable-experimental-tile-contrast: true); @@ -7,3 +8,56 @@ div .bx--tile--selectable:not(:last-child) { margin-block-end: 1rem; } + +.slug-tile-container { + display: flex; + flex-wrap: wrap; +} + +.slug-tile-container .cds--tile { + height: 320px; + max-width: 320px; + margin-right: 3rem; + margin-bottom: 3rem; +} + +.slug-tile-container h4 { + margin-bottom: 1rem; +} + +.slug-tile-container .ai-data { + margin-top: 16px; + padding: 16px 0; + display: flex; +} + +.slug-tile-container .data-container { + width: 126px; +} + +.slug-tile-container .data-container:first-of-type { + margin-right: 16px; +} + +.slug-tile-container p { + @include type.type-style('body-01'); +} + +.slug-tile-container .data-container p { + @include type.type-style('label-02'); +} + +.slug-tile-container .arrow-right { + position: absolute; + bottom: 16px; + right: 16px; +} + +.slug-tile-container .cds--tile-content__below-the-fold { + padding-top: 16px; +} + +.slug-tile-container .cds--tile-content__below-the-fold p { + @include type.type-style('label-01'); + margin: 8px 0 50px; +} From ca0ba1096ea0201b8c66be8c072d297642ddf468 Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Thu, 16 Nov 2023 14:46:27 -0500 Subject: [PATCH 2/9] feat(Tile): add slug to SelectableTile, ExpandableTile --- .../react/src/components/Tile/Tile.stories.js | 64 +++++++++++------ packages/react/src/components/Tile/Tile.tsx | 70 ++++++++++++++++--- .../react/src/components/Tile/tile-story.scss | 3 +- .../styles/scss/components/slug/_slug.scss | 15 +--- .../styles/scss/components/tile/_tile.scss | 25 +++++++ .../styles/scss/utilities/_ai-gradient.scss | 23 +++++- 6 files changed, 151 insertions(+), 49 deletions(-) diff --git a/packages/react/src/components/Tile/Tile.stories.js b/packages/react/src/components/Tile/Tile.stories.js index 08cf8870ac29..f11b776e28f9 100644 --- a/packages/react/src/components/Tile/Tile.stories.js +++ b/packages/react/src/components/Tile/Tile.stories.js @@ -23,7 +23,9 @@ import { TileBelowTheFoldContent, } from './'; import TileGroup from '../TileGroup/TileGroup'; -import { Download, ArrowRight } from '@carbon/icons-react'; +import { IconButton } from '../IconButton'; +import { Slug, SlugContent, SlugActions } from '../Slug'; +import { Download, View, FolderOpen, Folders } from '@carbon/icons-react'; export default { title: 'Components/Tile', @@ -58,29 +60,44 @@ export const Default = () => { }; export const SlugTest = () => { + const slug = ( + + +
+

AI Explained

+

84%

+

Confidence score

+

+ Lorem ipsum dolor sit amet, di os consectetur adipiscing elit, sed + do eiusmod tempor incididunt ut fsil labore et dolore magna aliqua. +

+
+

Model type

+

Foundation model

+
+ + + + + + + + + + + + +
+
+ ); + return (
- -

Title

-

- Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit at - consectetur turpis mauris gravida penatibus. -

-
-
-

Data Quality

-

85%

-
-
-

Label text

-

16%

-
-
- -
- +

Title

Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit at @@ -100,7 +117,8 @@ export const SlugTest = () => { + tileExpandedIconText="Interact to Collapse tile" + slug={slug}>

Title

diff --git a/packages/react/src/components/Tile/Tile.tsx b/packages/react/src/components/Tile/Tile.tsx index 3f84d76105d4..28617e5e3d35 100644 --- a/packages/react/src/components/Tile/Tile.tsx +++ b/packages/react/src/components/Tile/Tile.tsx @@ -9,7 +9,7 @@ import React, { type ChangeEvent, type ComponentType, } from 'react'; -import PropTypes from 'prop-types'; +import PropTypes, { ReactNodeLike } from 'prop-types'; import cx from 'classnames'; import { Checkbox, @@ -147,8 +147,10 @@ export const ClickableTile = React.forwardRef< const classes = cx( `${prefix}--tile`, `${prefix}--tile--clickable`, - clicked && `${prefix}--tile--is-clicked`, - light && `${prefix}--tile--light`, + { + [`${prefix}--tile--is-clicked`]: clicked, + [`${prefix}--tile--light`]: light, + }, className ); @@ -303,6 +305,11 @@ export interface SelectableTileProps extends HTMLAttributes { */ selected?: boolean; + /** + * Provide a `Slug` component to be rendered inside the `SelectableTile` component + */ + slug?: ReactNodeLike; + /** * Specify the tab index of the wrapper element */ @@ -336,6 +343,7 @@ export const SelectableTile = React.forwardRef< selected = false, tabIndex = 0, title = 'title', + slug, ...rest }, ref @@ -351,9 +359,12 @@ export const SelectableTile = React.forwardRef< const classes = cx( `${prefix}--tile`, `${prefix}--tile--selectable`, - isSelected && `${prefix}--tile--is-selected`, - light && `${prefix}--tile--light`, - disabled && `${prefix}--tile--disabled`, + { + [`${prefix}--tile--is-selected`]: isSelected, + [`${prefix}--tile--light`]: light, + [`${prefix}--tile--disabled`]: disabled, + [`${prefix}--tile--slug`]: slug, + }, className ); @@ -361,6 +372,9 @@ export const SelectableTile = React.forwardRef< function handleOnClick(evt) { evt.preventDefault(); evt?.persist?.(); + if (slug && slugRef.current && slugRef.current.contains(evt.target)) { + return; + } setIsSelected(!isSelected); clickHandler(evt); onChange(evt); @@ -387,6 +401,16 @@ export const SelectableTile = React.forwardRef< setPrevSelected(selected); } + // Slug is always size `xs` + const slugRef = useRef(null); + let normalizedSlug; + if (slug) { + normalizedSlug = React.cloneElement(slug as React.ReactElement, { + size: 'xs', + ref: slugRef, + }); + } + return ( // eslint-disable-next-line jsx-a11y/interactive-supports-focus

{children} + {normalizedSlug}
); }); -SelectableTile.displayName = 'SelectableTile'; SelectableTile.propTypes = { children: PropTypes.node, className: PropTypes.string, @@ -463,6 +487,11 @@ SelectableTile.propTypes = { */ selected: PropTypes.bool, + /** + * Provide a `Slug` component to be rendered inside the `SelectableTile` component + */ + slug: PropTypes.node, + /** * Specify the tab index of the wrapper element */ @@ -507,6 +536,11 @@ export interface ExpandableTileProps extends HTMLAttributes { */ onKeyUp?(event: KeyboardEvent): void; + /** + * Provide a `Slug` component to be rendered inside the `ExpandableTile` component + */ + slug?: ReactNodeLike; + /** * The `tabindex` attribute. */ @@ -555,6 +589,7 @@ export const ExpandableTile = React.forwardRef< tileCollapsedLabel, tileExpandedLabel, light, + slug, ...rest }, forwardRef @@ -632,8 +667,11 @@ export const ExpandableTile = React.forwardRef< `${prefix}--tile`, `${prefix}--tile--expandable`, `${prefix}--tile--expandable--interactive`, - isExpanded && `${prefix}--tile--is-expanded`, - light && `${prefix}--tile--light`, + { + [`${prefix}--tile--is-expanded`]: isExpanded, + [`${prefix}--tile--light`]: light, + [`${prefix}--tile--slug`]: slug, + }, className ); @@ -673,11 +711,12 @@ export const ExpandableTile = React.forwardRef< !getInteractiveContent(belowTheFold.current) && !getRoleContent(belowTheFold.current) && !getInteractiveContent(aboveTheFold.current) && - !getRoleContent(aboveTheFold.current) + !getRoleContent(aboveTheFold.current) && + !slug ) { setInteractive(false); } - }, []); + }, [slug]); useIsomorphicEffect(() => { if (!tile.current) { @@ -707,6 +746,14 @@ export const ExpandableTile = React.forwardRef< const belowTheFoldId = useId('expandable-tile-interactive'); + // Slug is always size `xs` + let normalizedSlug; + if (slug) { + normalizedSlug = React.cloneElement(slug as React.ReactElement, { + size: 'xs', + }); + } + return interactive ? (
+ {normalizedSlug}
{childrenAsArray[0]}
diff --git a/packages/react/src/components/Tile/tile-story.scss b/packages/react/src/components/Tile/tile-story.scss index 7c7df8a90d8f..efd3f17cebc2 100644 --- a/packages/react/src/components/Tile/tile-story.scss +++ b/packages/react/src/components/Tile/tile-story.scss @@ -12,13 +12,14 @@ div .bx--tile--selectable:not(:last-child) { .slug-tile-container { display: flex; flex-wrap: wrap; + align-items: flex-start; } .slug-tile-container .cds--tile { - height: 320px; max-width: 320px; margin-right: 3rem; margin-bottom: 3rem; + padding-bottom: 90px; } .slug-tile-container h4 { diff --git a/packages/styles/scss/components/slug/_slug.scss b/packages/styles/scss/components/slug/_slug.scss index 1e65431b014d..30f9a436c39a 100644 --- a/packages/styles/scss/components/slug/_slug.scss +++ b/packages/styles/scss/components/slug/_slug.scss @@ -4,6 +4,7 @@ @use '../../spacing' as *; @use '../../theme' as *; @use '../../type' as *; +@use '../../utilities/ai-gradient' as *; @use '../../utilities/convert'; $sizes: ( @@ -324,22 +325,12 @@ $sizes: ( // Slug callout styles .#{$prefix}--slug.#{$prefix}--slug--enabled .#{$prefix}--slug-content { + @include callout-gradient(); + border: 1px solid $border-subtle; border-radius: 16px; // 84px seems to make this fully opaque? backdrop-filter: blur(25px); - background: linear-gradient( - 0deg, - $slug-callout-aura-start 0%, - $slug-callout-aura-end 33%, - transparent 100% - ), - linear-gradient( - 180deg, - $slug-callout-gradient-top 0%, - $slug-callout-gradient-bottom 100% - ) - rgba(0, 0, 0, 0.01); // box-shadow seems to match the spec better // than the same values plugged into `drop-shadow` // filter: drop-shadow(-45px 45px 100px rgba(0, 0, 0, 0.2)); diff --git a/packages/styles/scss/components/tile/_tile.scss b/packages/styles/scss/components/tile/_tile.scss index c85eb0f499d2..a5f07093ff20 100644 --- a/packages/styles/scss/components/tile/_tile.scss +++ b/packages/styles/scss/components/tile/_tile.scss @@ -13,6 +13,7 @@ @use '../../spacing' as *; @use '../../theme' as *; @use '../../type' as *; +@use '../../utilities/ai-gradient' as *; @use '../../utilities/button-reset'; @use '../../utilities/component-reset'; @use '../../utilities/focus-outline' as *; @@ -368,6 +369,30 @@ $-icon-container-size: calc(#{layout.density('padding-inline')} * 2 + 1rem); fill: $icon-disabled; } + // Slug styles + .#{$prefix}--tile > .#{$prefix}--slug, + .#{$prefix}--tile--expandable > div > .#{$prefix}--slug { + position: absolute; + inset-block-start: $spacing-05; + inset-inline-end: $spacing-05; + } + + .#{$prefix}--tile.#{$prefix}--tile--selectable > .#{$prefix}--slug { + inset-inline-end: $spacing-08; + } + + .#{$prefix}--tile--slug.#{$prefix}--tile { + @include callout-gradient(); + + border: 1px solid $border-tile; + } + + .#{$prefix}--tile--expandable:has( + .#{$prefix}--slug > .#{$prefix}--popover--open + ) { + overflow: visible; + } + // Windows HCM fix .#{$prefix}--tile__chevron svg, .#{$prefix}--tile__checkmark svg, diff --git a/packages/styles/scss/utilities/_ai-gradient.scss b/packages/styles/scss/utilities/_ai-gradient.scss index ddf1beb042fe..77757256d2f7 100644 --- a/packages/styles/scss/utilities/_ai-gradient.scss +++ b/packages/styles/scss/utilities/_ai-gradient.scss @@ -9,12 +9,11 @@ /// Experimental - name and structure subject to change. Use at your own risk! /// Adds AI gradient styles to a component -/// @access public +/// @access private /// @param {String} $direction - Direction of gradient from: `left`, `right`, `top`, and `bottom` /// @param {Number} $width - Percentage width of gradient with regards to parent component /// @example @include ai-gradient('right', '33%'); /// @group utilities - @mixin ai-gradient($direction: 'right', $width: 33%) { $deg: 0; @if $direction == 'bottom' { @@ -40,3 +39,23 @@ transparent 100% ); } + +/// Experimental - name and structure subject to change. Use at your own risk! +/// Adds callout gradient styles to a component +/// @access private +/// @example @include callout-gradient(); +/// @group utilities +@mixin callout-gradient() { + background: linear-gradient( + 0deg, + theme.$slug-callout-aura-start 0%, + theme.$slug-callout-aura-end 33%, + transparent 100% + ), + linear-gradient( + 180deg, + theme.$slug-callout-gradient-top 0%, + theme.$slug-callout-gradient-bottom 100% + ) + rgba(0, 0, 0, 0.01); +} From 4f24f695db82378a86bea81193faaf04a6f59fa3 Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Fri, 17 Nov 2023 08:50:58 -0500 Subject: [PATCH 3/9] test(snapshot): update snapshots --- packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 1ca356b34d0d..56835681f587 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -6595,6 +6595,9 @@ Map { "selected": Object { "type": "bool", }, + "slug": Object { + "type": "node", + }, "tabIndex": Object { "type": "number", }, From e5484df9fc089e40eb2ac7b88377f1bb1c25521a Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Fri, 17 Nov 2023 09:11:44 -0500 Subject: [PATCH 4/9] feat(Tile): add slug prop to Tile --- .../react/src/components/Tile/Tile.stories.js | 17 +++++++++++++++++ packages/react/src/components/Tile/Tile.tsx | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/Tile/Tile.stories.js b/packages/react/src/components/Tile/Tile.stories.js index f11b776e28f9..2e635b3f55c5 100644 --- a/packages/react/src/components/Tile/Tile.stories.js +++ b/packages/react/src/components/Tile/Tile.stories.js @@ -93,6 +93,23 @@ export const SlugTest = () => { return (
+ +

Title

+

+ Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit at + consectetur turpis mauris gravida penatibus. +

+
+
+

Data Quality

+

85%

+
+
+

Label text

+

16%

+
+
+
{ className?: string; /** @deprecated */ light?: boolean; + + /** + * Provide a `Slug` component to be rendered inside the `SelectableTile` component + */ + slug?: ReactNodeLike; } export const Tile = React.forwardRef(function Tile( - { children, className, light = false, ...rest }, + { children, className, light = false, slug, ...rest }, ref ) { const prefix = usePrefix(); const tileClasses = cx( `${prefix}--tile`, - light && `${prefix}--tile--light`, + { + [`${prefix}--tile--light`]: light, + [`${prefix}--tile--slug`]: slug, + }, className ); return (
{children} + {slug}
); }); @@ -80,6 +89,11 @@ Tile.propTypes = { PropTypes.bool, 'The `light` prop for `Tile` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.' ), + + /** + * Provide a `Slug` component to be rendered inside the `Tile` component + */ + slug: PropTypes.node, }; export interface ClickableTileProps extends HTMLAttributes { From 156b2dbc9d0c127edf47066274de1530812f10a0 Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Fri, 17 Nov 2023 09:56:36 -0500 Subject: [PATCH 5/9] test(snapshot): update snapshots --- packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 56835681f587..819c169661ab 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -8525,6 +8525,9 @@ Map { "type": "string", }, "light": [Function], + "slug": Object { + "type": "node", + }, }, "render": [Function], }, From 311229b0f1e3aec37d63aeb4085a8d795d4eefc3 Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Mon, 20 Nov 2023 15:05:47 -0500 Subject: [PATCH 6/9] feat(Tile): add new tokens, add hover, selected styles --- .../react/src/components/Tile/Tile.stories.js | 29 +++++++- packages/react/src/components/Tile/Tile.tsx | 32 ++++++++- .../styles/scss/components/tile/_tile.scss | 71 ++++++++++++++++++- .../styles/scss/utilities/_ai-gradient.scss | 61 ++++++++++++---- packages/themes/src/g10.js | 10 +++ packages/themes/src/g100.js | 11 +++ packages/themes/src/g90.js | 11 +++ packages/themes/src/tokens/v11TokenGroup.js | 10 +++ packages/themes/src/white.js | 10 +++ 9 files changed, 229 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/Tile/Tile.stories.js b/packages/react/src/components/Tile/Tile.stories.js index 2e635b3f55c5..84cff4b80e6c 100644 --- a/packages/react/src/components/Tile/Tile.stories.js +++ b/packages/react/src/components/Tile/Tile.stories.js @@ -25,7 +25,13 @@ import { import TileGroup from '../TileGroup/TileGroup'; import { IconButton } from '../IconButton'; import { Slug, SlugContent, SlugActions } from '../Slug'; -import { Download, View, FolderOpen, Folders } from '@carbon/icons-react'; +import { + Download, + View, + FolderOpen, + Folders, + ArrowRight, +} from '@carbon/icons-react'; export default { title: 'Components/Tile', @@ -110,6 +116,27 @@ export const SlugTest = () => {
+ +

Title

+

+ Lorem ipsum dolor sit amet consectetur. Posuere duis fermentum sit at + consectetur turpis mauris gravida penatibus. +

+
+
+

Data Quality

+

85%

+
+
+

Label text

+

16%

+
+
+
{ * The rel property for the link. */ rel?: string; + + /** + * Specify if a `Slug` icon should be rendered inside the `ClickableTile` + */ + slug?: boolean; } export const ClickableTile = React.forwardRef< @@ -153,6 +158,7 @@ export const ClickableTile = React.forwardRef< onClick = () => {}, onKeyDown = () => {}, renderIcon: Icon, + slug, ...rest }, ref @@ -164,6 +170,7 @@ export const ClickableTile = React.forwardRef< { [`${prefix}--tile--is-clicked`]: clicked, [`${prefix}--tile--light`]: light, + [`${prefix}--tile--slug`]: slug, }, className ); @@ -184,6 +191,24 @@ export const ClickableTile = React.forwardRef< onKeyDown(evt); } + // To Do: Replace with an an icon from `@carbon/react` + // since the hollow slug in `ClickableTile` is not interactive + const hollowSlugIcon = ( + + + + + ); + const v12DefaultIcons = useFeatureFlag('enable-v12-tile-default-icons'); if (v12DefaultIcons) { if (!Icon) { @@ -211,7 +236,12 @@ export const ClickableTile = React.forwardRef< ref={ref} disabled={disabled} {...rest}> - {children} + {slug ? ( +
{children}
+ ) : ( + children + )} + {slug && hollowSlugIcon} {Icon &&