Repository: linode/manager
Pull request #11841: refactor: [M3-9435] [Akamai Design System] Breadcrumb Component
Author: login: bill-akamai
Labels: name: Approved
description: Multiple approvals and ready to merge!
name: Design Tokens
description: Laying the groundwork for Design Tokens
Created: 2025-03-13T16:28:17Z | Merged: 2025-03-21T15:48:20Z

## Description 📝

This PR updates the Breadcrumb component to conform to the Akamai Design System specifications. 

## Changes  🔄

- Implemented Akamai Design System breadcrumb theme tokens
- Updated typography, spacing, etc to match design specs
- Used `cx` utility for cleaner class names in `EditableText.tsx`

## Preview 📷

| Before  | After   |
| ------- | ------- |
| <img src="https://github.com/user-attachments/assets/8f162d90-1f33-4ce2-88d2-af3ff8f1144b"/> | <img src="https://github.com/user-attachments/assets/a8619ae1-877d-46ee-952a-2887ea572ccfhttps://github.com/user-attachments/assets/a8619ae1-877d-46ee-952a-2887ea572ccf"/> |

### Verification steps

- [ ] Open Cloud Manager, navigate to different views
- [ ] Confirm breadcrumb design matches ADS specs
- [ ] Also check if Dark Mode theme looks correct

<details>
<summary> Author Checklists </summary>

## As an Author, to speed up the review process, I considered 🤔

👀 Doing a self review
❔ Our [contribution guidelines](https://github.com/linode/manager/blob/develop/docs/CONTRIBUTING.md)
🤏 Splitting feature into small PRs
➕ Adding a [changeset](https://github.com/linode/manager/blob/develop/docs/CONTRIBUTING.md#writing-a-changeset)
🧪 Providing/improving test coverage
 🔐 Removing all sensitive information from the code and PR description
🚩 Using a feature flag to protect the release
👣 Providing comprehensive reproduction steps
📑 Providing or updating our documentation
🕛 Scheduling a pair reviewing session
📱 Providing mobile support
♿  Providing accessibility support

<br/>

- [x] I have read and considered all applicable items listed above.

## As an Author, before moving this PR from Draft to Open, I confirmed ✅

- [x] All unit tests are passing
- [x] TypeScript compilation succeeded without errors
- [x] Code passes all linting rules

</details>


Diff:
diff --git a/packages/manager/.changeset/pr-11841-changed-1741884096366.md b/packages/manager/.changeset/pr-11841-changed-1741884096366.md
new file mode 100644
index 00000000000..f40405718e9
--- /dev/null
+++ b/packages/manager/.changeset/pr-11841-changed-1741884096366.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Changed
+---
+
+Updated Breadcrumb component to conform to Akamai Design System specs ([#11841](https://github.com/linode/manager/pull/11841))
diff --git a/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx b/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx
index a50d3a4206f..4edad9d654b 100644
--- a/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx
+++ b/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx
@@ -1,12 +1,57 @@
 import { action } from '@storybook/addon-actions';
 import { Meta, StoryObj } from '@storybook/react';
 import React from 'react';
+import { Chip } from '@linode/ui';
 
 import { Breadcrumb } from './Breadcrumb';
 
+const withBadgeCrumbs = [
+  {
+    position: 3,
+    label: (
+      <>
+        <span>test</span>
+        <span
+          style={{
+            display: 'inline-block',
+            marginLeft: '4px',
+            textDecoration: 'none',
+          }}
+        >
+          <Chip label="beta" component="span" />
+        </span>
+      </>
+    ),
+  },
+];
+
+const noBadgeCrumbs = [
+  {
+    position: 3,
+    label: <span>test</span>,
+  },
+];
+
 const meta: Meta<typeof Breadcrumb> = {
   component: Breadcrumb,
   title: 'Foundations/Breadcrumb',
+  argTypes: {
+    crumbOverrides: {
+      options: ['With Badge', 'No Badge'],
+      mapping: {
+        'With Badge': withBadgeCrumbs,
+        'No Badge': noBadgeCrumbs,
+      },
+      control: {
+        type: 'radio',
+        labels: {
+          'With Badge': 'Show Beta Badge',
+          'No Badge': 'Hide Beta Badge',
+        },
+      },
+      defaultValue: 'No Badge',
+    },
+  },
 };
 
 type Story = StoryObj<typeof Breadcrumb>;
@@ -20,6 +65,7 @@ export const Default: Story = {
       onEdit: async () => action('onEdit'),
     },
     pathname: '/linodes/9872893679817/test/lastcrumb',
+    crumbOverrides: noBadgeCrumbs,
   },
   render: (args) => <Breadcrumb {...args} />,
 };
diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx
index 241f6705b90..8536c2dffd8 100644
--- a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx
+++ b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx
@@ -3,23 +3,24 @@ import { styled } from '@mui/material';
 
 export const StyledTypography = styled(Typography, {
   label: 'StyledTypography',
-})(({}) => ({
+})(({ theme }) => ({
   '&:hover': {
     textDecoration: 'underline',
   },
-  fontSize: '1.125rem',
+  fontSize: '1rem',
   lineHeight: 'normal',
   textTransform: 'capitalize',
   whiteSpace: 'nowrap',
+  color: theme.tokens.component.Breadcrumb.LastItem.Text,
 }));
 
 export const StyledSlashTypography = styled(Typography, {
   label: 'StyledSlashTypography',
 })(({ theme }) => ({
   color: theme.textColors.tableHeader,
-  fontSize: 20,
-  marginLeft: 2,
-  marginRight: 2,
+  fontSize: 16,
+  marginLeft: 4,
+  marginRight: 4,
 }));
 
 export const StyledDiv = styled('div', { label: 'StyledDiv' })({
diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.tsx
index 174d86166c4..bd8e6585f87 100644
--- a/packages/manager/src/components/Breadcrumb/Crumbs.tsx
+++ b/packages/manager/src/components/Breadcrumb/Crumbs.tsx
@@ -14,7 +14,7 @@ import type { EditableProps, LabelProps } from './types';
 import type { LinkProps } from 'react-router-dom';
 
 export interface CrumbOverridesProps {
-  label?: string;
+  label?: string | React.ReactNode;
   linkTo?: LinkProps['to'];
   noCap?: boolean;
   position: number;
diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx
index af43289b40f..0fd695b3110 100644
--- a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx
+++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx
@@ -14,16 +14,16 @@ export const StyledEditableText = styled(EditableText, {
   '& > div': {
     width: 250,
   },
-  marginLeft: `-${theme.spacing()}`,
 }));
 
 export const StyledH1Header = styled(H1Header, { label: 'StyledH1Header' })(
   ({ theme }) => ({
-    color: theme.textColors.tableStatic,
-    fontSize: '1.125rem',
+    color: theme.tokens.component.Breadcrumb.Normal.Text.Default,
+    fontSize: '1rem',
+    paddingLeft: 0,
     textTransform: 'capitalize',
     [theme.breakpoints.up('lg')]: {
-      fontSize: '1.125rem',
+      fontSize: '1rem',
     },
   })
 );
diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx
index 729f1222230..f5f011ff4fd 100644
--- a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx
+++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx
@@ -38,6 +38,7 @@ export const FinalCrumb = React.memo((props: Props) => {
         disabledBreadcrumbEditButton={disabledBreadcrumbEditButton}
         errorText={onEditHandlers.errorText}
         handleAnalyticsEvent={onEditHandlers.handleAnalyticsEvent}
+        isBreadcrumb
         onCancel={onEditHandlers.onCancel}
         onEdit={onEditHandlers.onEdit}
         text={onEditHandlers.editableTextTitle}
diff --git a/packages/ui/src/components/EditableText/EditableText.tsx b/packages/ui/src/components/EditableText/EditableText.tsx
index a19323bc83f..541bab797c9 100644
--- a/packages/ui/src/components/EditableText/EditableText.tsx
+++ b/packages/ui/src/components/EditableText/EditableText.tsx
@@ -13,7 +13,7 @@ import type { TextFieldProps } from '../TextField';
 import type { Theme } from '@mui/material/styles';
 import type { PropsWithChildren } from 'react';
 
-const useStyles = makeStyles<void, 'editIcon' | 'icon'>()(
+const useStyles = makeStyles<void, 'editIcon' | 'icon' | 'breadcrumbText'>()(
   (theme: Theme, _params, classes) => ({
     button: {
       '&[aria-label="Save"]': {
@@ -99,6 +99,11 @@ const useStyles = makeStyles<void, 'editIcon' | 'icon'>()(
         textDecoration: 'underline !important',
       },
     },
+    breadcrumbText: {
+      color: theme.tokens.component.Breadcrumb.Normal.Text.Default,
+      fontSize: '1rem !important',
+      paddingLeft: 0,
+    },
   })
 );
 
@@ -135,6 +140,10 @@ interface BaseProps extends Omit<TextFieldProps, 'label'> {
    * Optional suffix to append to the text when it is not in editing mode
    */
   textSuffix?: string;
+  /**
+   * Whether this EditableText is used as a breadcrumb
+   */
+  isBreadcrumb?: boolean;
 }
 
 interface PropsWithoutLink extends BaseProps {
@@ -163,7 +172,7 @@ interface PropsWithLink extends BaseProps {
 export type EditableTextProps = PropsWithLink | PropsWithoutLink;
 
 export const EditableText = (props: EditableTextProps) => {
-  const { classes } = useStyles();
+  const { classes, cx } = useStyles();
 
   const [isEditing, setIsEditing] = React.useState(Boolean(props.errorText));
   const [text, setText] = React.useState(props.text);
@@ -173,6 +182,7 @@ export const EditableText = (props: EditableTextProps) => {
     disabledBreadcrumbEditButton,
     errorText,
     handleAnalyticsEvent,
+    isBreadcrumb,
     labelLink,
     onCancel,
     onEdit,
@@ -237,7 +247,7 @@ export const EditableText = (props: EditableTextProps) => {
   };
   const labelText = (
     <H1Header
-      className={classes.root}
+      className={cx(classes.root, { [classes.breadcrumbText]: isBreadcrumb })}
       data-qa-editable-text
       title={`${text}${textSuffix ?? ''}`}
     />
@@ -245,7 +255,7 @@ export const EditableText = (props: EditableTextProps) => {
 
   return !isEditing && !errorText ? (
     <div
-      className={`${classes.container} ${classes.initial} ${className}`}
+      className={cx(classes.container, classes.initial, className)}
       data-testid={'editable-text'}
     >
       {!!labelLink ? (
@@ -258,7 +268,7 @@ export const EditableText = (props: EditableTextProps) => {
       {/** pencil icon */}
       <Button
         aria-label={`Edit ${text}`}
-        className={`${classes.button} ${classes.editIcon}`}
+        className={cx(classes.button, classes.editIcon)}
         data-qa-edit-button
         disabled={disabledBreadcrumbEditButton}
         onClick={openEdit}
@@ -268,11 +278,13 @@ export const EditableText = (props: EditableTextProps) => {
     </div>
   ) : (
     <ClickAwayListener mouseEvent="onMouseDown" onClickAway={cancelEditing}>
-      <div className={`${classes.container} ${className}`} data-qa-edit-field>
+      <div className={cx(classes.container, className)} data-qa-edit-field>
         <TextField
           {...rest}
           inputProps={{
-            className: classes.input,
+            className: cx(classes.input, {
+              [classes.breadcrumbText]: isBreadcrumb,
+            }),
           }}
           InputProps={{ className: classes.inputRoot }}
           // eslint-disable-next-line


Reviews:
user: login: hkhalil-akamai
body: Apologies for the delay in reviewing this. Breadcrumb appears as expected according to CDS guidelines.

Test failures appear unrelated.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:37:15Z
user: login: bnussman-akamai
body: It looks like we lost some spacing between entity headers and the content

| Production  |  This PR |
|---|---|
| ![Screenshot 2025-03-18 at 2 42 25 PM](https://github.com/user-attachments/assets/58a528a3-4498-44da-b550-cda408d1f146)  | ![Screenshot 2025-03-18 at 2 42 00 PM](https://github.com/user-attachments/assets/083a1dc8-bbf7-4e97-afe5-ee66da78c4e2)  |


state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-18T18:53:45Z
user: login: bill-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:29:04Z
user: login: bill-akamai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:29:18Z
user: login: bnussman-akamai
body: Looks good! Thanks for addressing that feedback 🙏 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T01:39:23Z

Review comments:
diff_hunk: @@ -135,6 +135,10 @@ interface BaseProps extends Omit<TextFieldProps, 'label'> {
    * Optional suffix to append to the text when it is not in editing mode
    */
   textSuffix?: string;
+  /**
+   * Whether this EditableText is used as a breadcrumb
+   */
+  isBreadcrumb?: boolean;
path: packages/ui/src/components/EditableText/EditableText.tsx
user: login: hkhalil-akamai
body: Optional nit: having a `className` prop instead would be more general and useful outside of this 1-off
created_at: 2025-03-17T20:31:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -237,7 +242,7 @@ export const EditableText = (props: EditableTextProps) => {
   };
   const labelText = (
     <H1Header
-      className={classes.root}
+      className={`${classes.root} ${isBreadcrumb ? 'breadcrumb-text' : ''}`}
path: packages/ui/src/components/EditableText/EditableText.tsx
user: login: bnussman-akamai
body: We should probably use the `cx` utility to concatenate classnames. I think it essentially does the same thing, but it's a bit neater.  There are a lot of instances in this file where we can make this change. 

```ts
      className={cx(classes.root, { ['breadcrumb-text']: isBreadcrumb })}
```

```suggestion
      className={cx(classes.root, { ['breadcrumb-text']: isBreadcrumb })}
```
created_at: 2025-03-18T18:49:28Z
author_association: MEMBER
diff_hunk: @@ -272,7 +277,9 @@ export const EditableText = (props: EditableTextProps) => {
         <TextField
           {...rest}
           inputProps={{
-            className: classes.input,
+            className: `${classes.input} ${
+              isBreadcrumb ? 'breadcrumb-text' : ''
path: packages/ui/src/components/EditableText/EditableText.tsx
user: login: bnussman-akamai
body: This might be related to @hkhalil-akamai 's comment. I'm not a big fan of magic classnames like `breadcrumb-text`. Could we handle this differently?
created_at: 2025-03-18T18:53:30Z
author_association: MEMBER
diff_hunk: @@ -237,7 +242,7 @@ export const EditableText = (props: EditableTextProps) => {
   };
   const labelText = (
     <H1Header
-      className={classes.root}
+      className={`${classes.root} ${isBreadcrumb ? 'breadcrumb-text' : ''}`}
path: packages/ui/src/components/EditableText/EditableText.tsx
user: login: bill-akamai
body: Added
created_at: 2025-03-18T20:29:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -272,7 +277,9 @@ export const EditableText = (props: EditableTextProps) => {
         <TextField
           {...rest}
           inputProps={{
-            className: classes.input,
+            className: `${classes.input} ${
+              isBreadcrumb ? 'breadcrumb-text' : ''
path: packages/ui/src/components/EditableText/EditableText.tsx
user: login: bill-akamai
body: Thanks @bnussman-akamai, feedback addressed
created_at: 2025-03-18T20:29:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: bnussman-akamai
created_at: 2025-03-19T02:35:00Z
body: Looks good but I'm still seeing the spacing issue mention in my comment
author_association: MEMBER
user: login: bill-akamai
created_at: 2025-03-19T16:50:51Z
body: > Looks good but I'm still seeing the spacing issue mention in my comment
Sorry @bnussman-akamai, I overlooked that. Addressed.
author_association: CONTRIBUTOR
user: login: linode-gh-bot
created_at: 2025-03-19T19:38:54Z
body: <!-- PULL REQUEST COMMENT: cloud-manager-ci-comment -->
<!-- BEGIN COMMENT SECTION: cloud-manager-ui-tests -->
## Cloud Manager UI test results
### :tada: 539 passing tests on [test run #10 ↗︎](https://baker.linode.com/job/manager/job/manager-test/job/PR-11841/10/display/redirect)

<table><thead><tr><td><strong>:x: Failing</strong></td><td><strong>:white_check_mark: Passing</strong></td><td><strong>:arrow_right_hook: Skipped</strong></td><td><strong>:clock1: Duration</strong></td></tr></thead><tbody><tr><td><code>0 Failing</code></td><td><code>539 Passing</code></td><td><code>3 Skipped</code></td><td><code>103m 12s</code></td></tr></tbody></table>
<!-- END COMMENT SECTION: cloud-manager-ui-tests -->
author_association: NONE