From 7dcb02e2feb6b50d2c5cc7451c28f00ea754282c Mon Sep 17 00:00:00 2001 From: TJ Egan Date: Tue, 31 Oct 2023 16:36:24 -0400 Subject: [PATCH] refactor(Slug): refactor Slug callout into composable components (#15036) * feat(Slug): scaffold out AI Slug component * feat(slug): add gradient tokens to themes, hover styles * feat(slug): add focus styles, cleanup storybook * fix(Theme): adjust small slug hover tokens * feat(Slug): add hollow slug * feat(Slug): initial inline styles * chore(snapshot): update snapshots * feat(Slug): add inline styles * fix(Slug): refactor inline variant * style(Slug): tweak inline styles * style(Slug): add hover styles to inline variant * style(Slug): add initial callout styles * style(Slug): tokenize gradients * style(Slug): adjust padding * chore: udpate snapshots * style(Slug): adjust padding with hollow dot, fix text colors * fix(Slug): fix hover styles when focused * refactor(Slug): remove ai from prefix * refactor(Slug): break Slug callout into composable components * style(Slug): add styles for action bar * chore(storybook): update stories to use SlugContent, SlugToolbar * refactor(Slug): rename SlugToolbar to SlugActions * chore(storybook): remove console.log * chore(storybook): add storybook toggle to show / hide action bar --------- Co-authored-by: Andrea N. Cardona Co-authored-by: Taylor Jones --- .../react/src/components/Slug/Slug.stories.js | 357 ++++++++++++++---- packages/react/src/components/Slug/index.js | 190 +++++++++- packages/react/src/index.js | 6 +- .../styles/scss/components/slug/_slug.scss | 35 +- 4 files changed, 508 insertions(+), 80 deletions(-) diff --git a/packages/react/src/components/Slug/Slug.stories.js b/packages/react/src/components/Slug/Slug.stories.js index 5c2c7c4722b4..7a677b3a6dca 100644 --- a/packages/react/src/components/Slug/Slug.stories.js +++ b/packages/react/src/components/Slug/Slug.stories.js @@ -9,8 +9,10 @@ import React from 'react'; -import Slug from '.'; +import { Slug, SlugContent, SlugActions } from '.'; +import { View, FolderOpen, Folders } from '@carbon/icons-react'; import Button from '../Button'; +import { IconButton } from '../IconButton'; import mdx from './Slug.mdx'; import './slug-story.scss'; @@ -44,69 +46,225 @@ const content = AI was used to generate this content; export const Default = () => ( <>
- - - - - - - + + {aiContent} + + + {aiContent} + + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + +
- - - + + {content} + + + {content} + + + {content} +
- - - + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + +
- - - + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + + + + + {aiContent} + + + + + + + + + + + + + +
- - - + + {content} + + + {content} + + + {content} +
( kind="inline" dotType="hollow" size="sm" - aiTextLabel="Text goes here" - slugContent={content} - /> + aiTextLabel="Text goes here"> + {content} + + aiTextLabel="Text goes here"> + {content} + + aiTextLabel="Text goes here"> + {content} +
); -export const Playground = (args) => ( - <> -
- -
- - - -); +export const Playground = (args) => { + const { kind, dotType, showSlugActions = true } = args; + + let renderedContent; + if (kind === 'hollow' || dotType === 'hollow') { + renderedContent = content; + } else { + renderedContent = ( + <> +
+

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

+
+ {showSlugActions && ( + + + + + + + + + + + + + )} + + ); + } + + return ( + <> +
+ + {renderedContent} + +
+ + + + ); +}; + +Playground.argTypes = { + showSlugActions: { + control: { + type: 'boolean', + }, + description: 'Playground only - toggle to show the callout toolbar', + }, +}; diff --git a/packages/react/src/components/Slug/index.js b/packages/react/src/components/Slug/index.js index b93fe751c330..8e4be8cfbb37 100644 --- a/packages/react/src/components/Slug/index.js +++ b/packages/react/src/components/Slug/index.js @@ -4,7 +4,191 @@ * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ -import Slug from './Slug'; -export default Slug; -export { Slug }; +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import { usePrefix } from '../../internal/usePrefix'; +import { + Toggletip, + ToggletipButton, + ToggletipContent, + ToggletipActions, +} from '../Toggletip'; + +export const SlugContent = React.forwardRef(function SlugContent( + { children, className }, + ref +) { + const prefix = usePrefix(); + + const slugContentClasses = cx(className, { + [`${prefix}--slug-content`]: true, + }); + + return ( + + {children} + + ); +}); + +SlugContent.propTypes = { + /** + * Specify the content you want rendered inside the slug ToggleTip + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to the AI slug callout + */ + className: PropTypes.string, +}; + +export const SlugActions = React.forwardRef(function SlugActions( + { children, className }, + ref +) { + const prefix = usePrefix(); + + const slugActionBarClasses = cx(className, { + [`${prefix}--slug-actions`]: true, + }); + + return ( + + {children} + + ); +}); + +SlugActions.propTypes = { + /** + * Specify the content you want rendered inside the slug callout toolbar + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to the AI slug toolbar + */ + className: PropTypes.string, +}; + +export const Slug = React.forwardRef(function Slug( + { + aiText = 'AI', + aiTextLabel, + align, + autoAlign = false, + className, + dotType, + kind, + size = 'xs', + children, + }, + ref +) { + const prefix = usePrefix(); + + const slugClasses = cx(className, { + [`${prefix}--slug`]: true, + [`${prefix}--slug--hollow`]: kind === 'hollow' || dotType === 'hollow', + // Need to come up with a better name; explainable? + // Need to be able to target the non-hollow variant another way + // other than using `:not` all over the styles + [`${prefix}--slug--enabled`]: kind !== 'hollow' && dotType !== 'hollow', + }); + + const slugButtonClasses = cx({ + [`${prefix}--slug__button`]: true, + [`${prefix}--slug__button--${size}`]: size, + [`${prefix}--slug__button--${kind}`]: kind, + [`${prefix}--slug__button--inline-with-content`]: + kind === 'inline' && aiTextLabel, + }); + + return ( +
+ + + {aiText} + {aiTextLabel && ( + + {aiTextLabel} + + )} + + {children} + +
+ ); +}); + +Slug.propTypes = { + /** + * Specify the correct translation of the AI text + */ + aiText: PropTypes.string, + + /** + * Specify additional text to be rendered next to the AI label in the inline variant + */ + aiTextLabel: PropTypes.string, + + /** + * Specify how the popover should align with the button + */ + align: PropTypes.oneOf([ + 'top', + 'top-left', + 'top-right', + + 'bottom', + 'bottom-left', + 'bottom-right', + + 'left', + 'left-bottom', + 'left-top', + + 'right', + 'right-bottom', + 'right-top', + ]), + + /** + * Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to future changes. + */ + autoAlign: PropTypes.bool, + + /** + * Specify the content you want rendered inside the slug ToggleTip + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to the AI slug + */ + className: PropTypes.string, + + /** + * Specify the type of dot that should be rendered in front of the inline variant + */ + dotType: PropTypes.oneOf(['default', 'hollow']), + + /** + * Specify the type of Slug, from the following list of types: + */ + kind: PropTypes.oneOf(['default', 'hollow', 'inline']), + + /** + * Specify the size of the button, from the following list of sizes: + */ + size: PropTypes.oneOf(['mini', '2xs', 'xs', 'sm', 'md', 'lg', 'xl']), + + /** + * Specify the content you want rendered inside the slug ToggleTip + */ + slugContent: PropTypes.node, +}; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 14e20ee01f13..774125a677ff 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -298,4 +298,8 @@ export { DefinitionTooltip } from './components/Tooltip/DefinitionTooltip'; export { GlobalTheme, Theme, useTheme } from './components/Theme'; export { usePrefix } from './internal/usePrefix'; export { useIdPrefix } from './internal/useIdPrefix'; -export { Slug as unstable__Slug } from './components/Slug'; +export { + Slug as unstable__Slug, + SlugContent as unstable__SlugContent, + SlugActions as unstable__SlugActions, +} from './components/Slug'; diff --git a/packages/styles/scss/components/slug/_slug.scss b/packages/styles/scss/components/slug/_slug.scss index fd625e4a4f0f..21fde4e37099 100644 --- a/packages/styles/scss/components/slug/_slug.scss +++ b/packages/styles/scss/components/slug/_slug.scss @@ -309,7 +309,7 @@ $sizes: ( } // Slug callout styles - .#{$prefix}--slug.#{$prefix}--slug--enabled .#{$prefix}--popover-content { + .#{$prefix}--slug.#{$prefix}--slug--enabled .#{$prefix}--slug-content { border: 1px solid $border-subtle; border-radius: 16px; // 84px seems to make this fully opaque? @@ -334,13 +334,44 @@ $sizes: ( min-inline-size: convert.to-rem(280px); } - .#{$prefix}--slug.#{$prefix}--slug--enabled .#{$prefix}--popover-caret { + .#{$prefix}--slug.#{$prefix}--slug--enabled + > .#{$prefix}--toggletip + > .#{$prefix}--popover + > .#{$prefix}--popover-caret { background: $border-subtle; } .#{$prefix}--slug.#{$prefix}--slug--enabled .#{$prefix}--toggletip-content { + // This sets the max size to the size of the action bar with 3 buttons + max-inline-size: convert.to-rem(334px); padding-block-end: convert.to-rem(80px); padding-block-start: convert.to-rem(32px); padding-inline: convert.to-rem(32px); } + + .#{$prefix}--slug.#{$prefix}--slug--enabled .#{$prefix}--slug-actions { + position: absolute; + justify-content: flex-end; + background: $layer-accent; + border-end-end-radius: convert.to-rem(15px); + border-end-start-radius: convert.to-rem(15px); + column-gap: 0; + inline-size: 100%; + inset-block-end: 0; + inset-inline-end: 0; + } + + .#{$prefix}--slug.#{$prefix}--slug--enabled + .#{$prefix}--slug-actions + .#{$prefix}--btn:focus { + border-color: $focus; + box-shadow: inset 0 0 0 1px $focus, inset 0 0 0 2px $background; + } + + .#{$prefix}--slug.#{$prefix}--slug--enabled + .#{$prefix}--slug-actions + .#{$prefix}--btn--primary { + order: 1; + border-end-end-radius: convert.to-rem(16px); + } }