;
+ if (
+ child.type &&
+ child.type.displayName !== undefined &&
+ child.type.displayName.includes('OverflowMenu')
+ ) {
+ const horizontalOverflowIcon = (
+
+ );
+ return (
+
+ {React.cloneElement(child, {
+ menuOptionsClass: `${prefix}--breadcrumb-menu-options`,
+ menuOffset: { top: 10, left: 59 },
+ renderIcon: () => horizontalOverflowIcon,
+ })}
+
+ );
+ }
+
+ if (typeof children === 'string') {
+ return (
+
+ {href ? (
+
+ {children}
+
+ ) : (
+ {children}
+ )}
+
+ );
+ }
+
+ return (
+
+ {React.cloneElement(child, {
+ 'aria-current': ariaCurrent,
+ className: cx(`${prefix}--link`, child.props.className),
+ })}
+
+ );
+ });
+
+BreadcrumbItem.displayName = 'BreadcrumbItem';
+
+BreadcrumbItem.propTypes = {
+ // @ts-expect-error - v12 TODO: BREAKING: This should match AriaAttributes['aria-current']
+ 'aria-current': PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+
+ /**
+ * Pass in content that will be inside of the BreadcrumbItem
+ */
+ children: PropTypes.node,
+
+ /**
+ * Specify an optional className to be applied to the container node
+ */
+ className: PropTypes.string,
+
+ /**
+ * Optional string representing the link location for the BreadcrumbItem
+ */
+ href: PropTypes.string,
+
+ /**
+ * Provide if this breadcrumb item represents the current page
+ */
+ isCurrentPage: PropTypes.bool,
+};
+
+export default BreadcrumbItem;
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}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
-
-
-
+
+ {content}
+
+
+ {content}
+
+
+ {content}
+
-
-
-
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
-
-
-
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
+
+
+ {aiContent}
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+
+
-
-
-
+
+ {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) => (
- <>
-
-
-
- Test
- Test
- >
-);
+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 && (
+
+
+
+
+
+
+
+
+
+
+ View literature
+
+ )}
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+ {renderedContent}
+
+
+ Test
+ Test
+ >
+ );
+};
+
+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/components/UIShell/SideNavMenu.js b/packages/react/src/components/UIShell/SideNavMenu.js
deleted file mode 100644
index 3595eea19b3d..000000000000
--- a/packages/react/src/components/UIShell/SideNavMenu.js
+++ /dev/null
@@ -1,172 +0,0 @@
-/**
- * Copyright IBM Corp. 2016, 2023
- *
- * 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 { ChevronDown } from '@carbon/icons-react';
-import cx from 'classnames';
-import PropTypes from 'prop-types';
-import React, { useContext, useState } from 'react';
-import SideNavIcon from './SideNavIcon';
-import { keys, match } from '../../internal/keyboard';
-import { usePrefix } from '../../internal/usePrefix';
-import { SideNavContext } from './SideNav';
-
-const SideNavMenu = React.forwardRef(function SideNavMenu(props, ref) {
- const {
- className: customClassName,
- children,
- defaultExpanded = false,
- isActive = false,
- large = false,
- renderIcon: IconElement,
- isSideNavExpanded,
- tabIndex,
- title,
- } = props;
- const isRail = useContext(SideNavContext);
- const prefix = usePrefix();
- const [isExpanded, setIsExpanded] = useState(defaultExpanded);
- const [prevExpanded, setPrevExpanded] = useState(defaultExpanded);
- const className = cx({
- [`${prefix}--side-nav__item`]: true,
- [`${prefix}--side-nav__item--active`]:
- isActive || (hasActiveChild(children) && !isExpanded),
- [`${prefix}--side-nav__item--icon`]: IconElement,
- [`${prefix}--side-nav__item--large`]: large,
- [customClassName]: !!customClassName,
- });
-
- if (isSideNavExpanded === false && isExpanded === true) {
- setIsExpanded(false);
- setPrevExpanded(true);
- } else if (isSideNavExpanded === true && prevExpanded === true) {
- setIsExpanded(true);
- setPrevExpanded(false);
- }
-
- return (
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
- {
- if (match(event, keys.Escape)) {
- setIsExpanded(false);
- }
- }}>
- {
- setIsExpanded(!isExpanded);
- }}
- ref={ref}
- type="button"
- tabIndex={
- tabIndex === undefined
- ? !isSideNavExpanded && !isRail
- ? -1
- : 0
- : tabIndex
- }>
- {IconElement && (
-
-
-
- )}
-
- {title}
-
-
-
-
-
-
-
- );
-});
-SideNavMenu.displayName = 'SideNavMenu';
-SideNavMenu.propTypes = {
- /**
- * Provide 's inside of the `SideNavMenu`
- */
- children: PropTypes.node,
-
- /**
- * Provide an optional class to be applied to the containing node
- */
- className: PropTypes.string,
-
- /**
- * Specify whether the menu should default to expanded. By default, it will
- * be closed.
- */
- defaultExpanded: PropTypes.bool,
-
- /**
- * Specify whether the `SideNavMenu` is "active". `SideNavMenu` should be
- * considered active if one of its menu items are a link for the current
- * page.
- */
- isActive: PropTypes.bool,
-
- /**
- * Property to indicate if the side nav container is open (or not). Use to
- * keep local state and styling in step with the SideNav expansion state.
- */
- isSideNavExpanded: PropTypes.bool,
-
- /**
- * Specify if this is a large variation of the SideNavMenu
- */
- large: PropTypes.bool,
-
- /**
- * Pass in a custom icon to render next to the `SideNavMenu` title
- */
- renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
-
- /**
- * Optional prop to specify the tabIndex of the button. If undefined, it will be applied default validation
- */
- tabIndex: PropTypes.number,
-
- /**
- * Provide the text for the overall menu name
- */
- title: PropTypes.string.isRequired,
-};
-
-function hasActiveChild(children) {
- // if we have children, either a single or multiple, find if it is active
- if (Array.isArray(children)) {
- return children.some((child) => {
- if (!child.props) {
- return false;
- }
-
- if (child.props.isActive === true) {
- return true;
- }
-
- if (child.props['aria-current']) {
- return true;
- }
-
- return false;
- });
- }
-
- if (children.props) {
- if (children.props.isActive === true || children.props['aria-current']) {
- return true;
- }
- }
-
- return false;
-}
-
-export default SideNavMenu;
-export { SideNavMenu };
diff --git a/packages/react/src/components/UIShell/SideNavMenu.tsx b/packages/react/src/components/UIShell/SideNavMenu.tsx
index cfee364113fa..bcdb6869fe1f 100644
--- a/packages/react/src/components/UIShell/SideNavMenu.tsx
+++ b/packages/react/src/components/UIShell/SideNavMenu.tsx
@@ -132,9 +132,7 @@ const SideNavMenu = React.forwardRef(
)}
-
- {title}
-
+ {title}
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);
+ }
}