Skip to content

[LG-5094] feat(code-editor): add panel component for toolbar functionality #3047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: integration/code-editor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a493776
feat(CodeEditor): enhance copy button functionality and styling
tsck Aug 14, 2025
705f5de
refactor(CodeEditor): remove unused import from CodeEditor.types.ts
tsck Aug 14, 2025
20e4ba3
feat(CodeEditor): add CodeEditorContext for internal state management
tsck Aug 15, 2025
fcaa74a
feat(CodeEditor): introduce Panel component for enhanced toolbar func…
tsck Aug 15, 2025
740b9db
changeset
tsck Aug 15, 2025
72f1988
fix(Panel): disable dark menu rendering in the Panel component
tsck Aug 15, 2025
85ccbf0
feat(CodeEditor): integrate Panel component and enhance CodeEditor fu…
tsck Aug 15, 2025
e8dbc88
refactor(CodeEditor, Panel): reorganize props for improved clarity an…
tsck Aug 15, 2025
3d2670d
docs(CodeEditor): update README to include new `panel` prop details
tsck Aug 15, 2025
76c646d
test(Panel): add comprehensive unit tests for Panel component functio…
tsck Aug 15, 2025
2d03e1c
Updated changeset
tsck Aug 15, 2025
288c313
feat(CodeEditorCopyButton): enhance icon fill color handling and simp…
tsck Aug 18, 2025
60ab5b4
refactor(CodeEditor): rename getContents prop for clarity
tsck Aug 18, 2025
3bfad7c
feat(CodeEditor): enhance copy button functionality and styling
tsck Aug 14, 2025
d0978ea
refactor(CodeEditor): remove unused import from CodeEditor.types.ts
tsck Aug 14, 2025
665a0f1
refactor(CodeEditor): rename getContents prop to getContentsToCopy
tsck Aug 19, 2025
cb12287
docs(CodeEditor): add SecondaryButtonConfig section to README
tsck Aug 19, 2025
a3b0f25
fix(Panel): update copy button accessibility labels in tests
tsck Aug 19, 2025
7da72c8
refactor(Panel): update panel styles for consistency and clarity
tsck Aug 20, 2025
d551e73
feat(Panel): add support for custom secondary button properties
tsck Aug 20, 2025
bc925b7
fix(CodeEditor): remove href from custom secondary button configuration
tsck Aug 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/plenty-cities-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@leafygreen-ui/code-editor': minor
---

Adds **Panel component**: A comprehensive toolbar interface for CodeEditor with formatting, copying, and custom action buttons

- **New Panel component**: Provides a configurable toolbar that displays at the top of CodeEditor
- **Built-in actions**: Includes format button, copy button, and secondary menu with undo/redo/download/shortcuts
- **Customizable**: Supports custom secondary buttons and inner content for application-specific needs
- **Fully accessible**: All buttons and menu items include proper ARIA labels and keyboard navigation
- **Context integration**: Seamlessly integrates with CodeEditor context for copy functionality
- **Styling**: Matches CodeEditor theme with proper grid layout and responsive design

The Panel component enhances the CodeEditor user experience by providing easy access to common editor actions through a clean, organized interface.
67 changes: 67 additions & 0 deletions packages/code-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,78 @@ console.log(greet('MongoDB user'));`;
| `minHeight` _(optional)_ | Sets the editor's minimum height. | `string` | `undefined` |
| `minWidth` _(optional)_ | Sets the editor's minimum width. | `string` | `undefined` |
| `onChange` _(optional)_ | Callback that receives the updated editor value when changes are made. | `(value: string) => void` | `undefined` |
| `panel` _(optional)_ | Panel component to render at the top of the CodeEditor. Provides a toolbar interface with formatting, copying, and custom action buttons. See the Panel component documentation for available options. | `React.ReactNode` | `undefined` |
| `placeholder` _(optional)_ | Value to display in the editor when it is empty. | `HTMLElement \| string` | `undefined` |
| `readOnly` _(optional)_ | Enables read only mode, making the contents uneditable. | `boolean` | `false` |
| `tooltips` _(optional)_ | Add tooltips to the editor content that appear on hover. | `Array<CodeEditorTooltip>` | `undefined` |
| `value` _(optional)_ | Controlled value of the editor. If set, the editor will be controlled and will not update its value on change. Use `onChange` to update the value externally. | `string` | `undefined` |
| `width` _(optional)_ | Sets the editor's width. If not set, the editor will be 100% width of its parent container. | `string` | `undefined` |

### `<Panel>`

The Panel component provides a toolbar interface for the CodeEditor with formatting, copying, and custom action buttons. It displays at the top of the CodeEditor and can include a title, action buttons, and custom content.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about adding a note about what functionality is provided out-of-the-box vs what consumers will need to implement?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, will all of these actions be supported out-of-the-box? (I might be incorrectly assuming based off of having only reviewed copy)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, all of these actions will be provided! Only added secondary actions won't be.


#### Example

```tsx
import { CodeEditor, Panel, LanguageName } from '@leafygreen-ui/code-editor';
import CloudIcon from '@leafygreen-ui/icon';

<CodeEditor
defaultValue="const greeting = 'Hello World';"
language={LanguageName.javascript}
panel={
<Panel
title="index.ts"
showFormatButton
showCopyButton
showSecondaryMenuButton
customSecondaryButtons={[
{
label: 'Deploy to Cloud',
glyph: <CloudIcon />,
onClick: () => console.log('Deploy clicked'),
'aria-label': 'Deploy code to cloud',
},
]}
onFormatClick={() => console.log('Format clicked')}
onCopyClick={() => console.log('Copy clicked')}
onDownloadClick={() => console.log('Download clicked')}
/>
}
/>;
```

#### Properties

| Name | Description | Type | Default |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ----------- |
| `baseFontSize` _(optional)_ | Font size of text in the panel. Controls the typography scale used for the panel title and other text elements. | `14 \| 16` | `14` |
| `customSecondaryButtons` _(optional)_ | Array of custom secondary buttons to display in the secondary menu. Each button can include a label, icon, click handler, href, and aria-label for accessibility. | `Array<SecondaryButtonConfig>` | `undefined` |
| `darkMode` _(optional)_ | Determines if the component appears in dark mode. When not provided, the component will inherit the dark mode state from the LeafyGreen Provider. | `boolean` | `undefined` |
| `innerContent` _(optional)_ | React node to render between the title and the buttons. Can be used to add custom controls to the panel. | `React.ReactNode` | `undefined` |
| `onCopyClick` _(optional)_ | Callback fired when the copy button is clicked. Called after the copy operation is attempted. | `() => void` | `undefined` |
| `onDownloadClick` _(optional)_ | Callback fired when the download button in the secondary menu is clicked. Called after the download operation is attempted. | `() => void` | `undefined` |
| `onFormatClick` _(optional)_ | Callback fired when the format button is clicked. Called after the formatting operation is attempted. | `() => void` | `undefined` |
| `onRedoClick` _(optional)_ | Callback fired when the redo button in the secondary menu is clicked. Called after the redo operation is attempted. | `() => void` | `undefined` |
| `onUndoClick` _(optional)_ | Callback fired when the undo button in the secondary menu is clicked. Called after the undo operation is attempted. | `() => void` | `undefined` |
| `onViewShortcutsClick` _(optional)_ | Callback fired when the view shortcuts button in the secondary menu is clicked. Called after the view shortcuts operation is attempted. | `() => void` | `undefined` |
| `showCopyButton` _(optional)_ | Determines whether to show the copy button in the panel. When enabled, users can copy the editor content to their clipboard. | `boolean` | `undefined` |
| `showFormatButton` _(optional)_ | Determines whether to show the format button in the panel. When enabled and formatting is available for the current language, users can format/prettify their code. | `boolean` | `undefined` |
| `showSecondaryMenuButton` _(optional)_ | Determines whether to show the secondary menu button (ellipsis icon) in the panel. When enabled, displays a menu with additional actions like undo, redo, download, and view shortcuts. | `boolean` | `undefined` |
| `title` _(optional)_ | Title text to display in the panel header. Typically used to show the current language or content description. | `string` | `undefined` |

#### `SecondaryButtonConfig`

| Name | Description | Type | Default |
| ------------------------- | ---------------------------------------------------------------------- | -------------------- | ----------- |
| `aria-label` _(optional)_ | Accessible label for the button to provide context for screen readers. | `string` | `undefined` |
| `disabled` _(optional)_ | Whether the button is disabled. | `boolean` | `undefined` |
| `glyph` _(optional)_ | Icon element to display in the button. | `React.ReactElement` | `undefined` |
| `href` _(optional)_ | URL to navigate to when the button is clicked. | `string` | `undefined` |
| `label` | Text label for the button. | `string` | — |
| `onClick` _(optional)_ | Callback fired when the button is clicked. | `() => void` | `undefined` |

## Types and Variables

| Name | Description |
Expand All @@ -89,6 +155,7 @@ console.log(greet('MongoDB user'));`;
| `IndentUnits` | Constant object defining indent unit options (`space`, `tab`) for the `indentUnit` prop. |
| `LanguageName` | Constant object containing all supported programming languages for syntax highlighting. |
| `CodeEditorModules` | TypeScript interface defining the structure of lazy-loaded CodeMirror modules used by extension hooks. |
| `PanelProps` | TypeScript interface defining all props that can be passed to the `Panel` component. |

## Test Utilities

Expand Down
1 change: 1 addition & 0 deletions packages/code-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@leafygreen-ui/icon-button": "workspace:^",
"@leafygreen-ui/leafygreen-provider": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/menu": "workspace:^",
"@leafygreen-ui/palette": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@leafygreen-ui/tooltip": "workspace:^",
Expand Down
26 changes: 25 additions & 1 deletion packages/code-editor/src/CodeEditor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import type { StoryFn, StoryObj } from '@storybook/react';
import { expect, waitFor } from '@storybook/test';

import { css } from '@leafygreen-ui/emotion';
// @ts-ignore LG icons don't currently support TS
import CloudIcon from '@leafygreen-ui/icon/dist/Cloud';

import { CopyButtonAppearance } from './CodeEditor/CodeEditor.types';
import { LanguageName } from './CodeEditor/hooks/extensions/useLanguageExtension';
import { codeSnippets } from './CodeEditor/testing';
import { IndentUnits } from './CodeEditor';
import { CodeEditor } from '.';
import { CodeEditor, Panel } from '.';

const MyTooltip = ({
line,
Expand Down Expand Up @@ -180,6 +182,28 @@ const Template: StoryFn<typeof CodeEditor> = args => <CodeEditor {...args} />;

export const LiveExample = Template.bind({});

export const WithPanel = Template.bind({});
WithPanel.args = {
language: 'typescript',
defaultValue: codeSnippets.typescript,
panel: (
<Panel
showCopyButton
showFormatButton
showSecondaryMenuButton
title="index.tsx"
customSecondaryButtons={[
{
label: 'Custom Button',
onClick: () => {},
'aria-label': 'Custom Button',
glyph: <CloudIcon />,
},
]}
/>
),
};

export const TooltipOnHover: StoryObj<{}> = {
render: () => {
return (
Expand Down
61 changes: 36 additions & 25 deletions packages/code-editor/src/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,39 @@ import {
CopyButtonLgId,
type HTMLElementWithCodeMirror,
} from './CodeEditor.types';
import { CodeEditorProvider } from './CodeEditorContext';
import { useExtensions, useLazyModules, useModuleLoaders } from './hooks';

export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
(props, forwardedRef) => {
const {
defaultValue,
value,
forceParsing: forceParsingProp = false,
onChange: onChangeProp,
isLoading: isLoadingProp = false,
extensions: consumerExtensions = [],
darkMode: darkModeProp,
baseFontSize: baseFontSizeProp,
className,
height,
maxHeight,
maxWidth,
minHeight,
minWidth,
width,
language,
copyButtonAppearance,
darkMode: darkModeProp,
defaultValue,
enableClickableUrls,
enableCodeFolding,
enableLineNumbers,
enableLineWrapping,
indentUnit,
extensions: consumerExtensions = [],
forceParsing: forceParsingProp = false,
height,
indentSize,
indentUnit,
isLoading: isLoadingProp = false,
language,
maxHeight,
maxWidth,
minHeight,
minWidth,
onChange: onChangeProp,
panel,
placeholder,
readOnly,
tooltips,
copyButtonAppearance,
value,
width,
...rest
} = props;

Expand Down Expand Up @@ -147,8 +149,13 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(

useImperativeHandle(forwardedRef, () => ({
getEditorViewInstance: () => editorViewRef.current,
getContents,
}));

const contextValue = {
getContents,
};

return (
<div
ref={editorContainerRef}
Expand All @@ -164,16 +171,20 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
})}
{...rest}
>
{(copyButtonAppearance === CopyButtonAppearance.Hover ||
copyButtonAppearance === CopyButtonAppearance.Persist) && (
<CodeEditorCopyButton
getContentsToCopy={getContents}
className={getCopyButtonStyles(copyButtonAppearance)}
variant={CopyButtonVariant.Button}
disabled={isLoadingProp || isLoading}
data-lgid={CopyButtonLgId}
/>
{panel && (
<CodeEditorProvider value={contextValue}>{panel}</CodeEditorProvider>
)}
{!panel &&
(copyButtonAppearance === CopyButtonAppearance.Hover ||
copyButtonAppearance === CopyButtonAppearance.Persist) && (
<CodeEditorCopyButton
getContentsToCopy={getContents}
className={getCopyButtonStyles(copyButtonAppearance)}
variant={CopyButtonVariant.Button}
disabled={isLoadingProp || isLoading}
data-lgid={CopyButtonLgId}
/>
)}
{(isLoadingProp || isLoading) && (
<div
className={getLoaderStyles({
Expand Down
Loading