Skip to content

Commit ff5976a

Browse files
committed
[LG-5094] feat(code-editor): add panel component for toolbar functionality (#3047)
* feat(CodeEditor): enhance copy button functionality and styling - Introduced `copyButtonAppearance` prop to control the visibility of the copy button in the CodeEditor. - Implemented styles for different copy button appearances: 'hover', 'persist', and 'none'. - Updated tests to verify the rendering behavior of the copy button based on the new prop. - Refactored CodeEditor component to integrate the new copy button functionality and ensure proper rendering based on user interactions. * refactor(CodeEditor): remove unused import from CodeEditor.types.ts - Eliminated the unused `copyButtonClassName` import to clean up the code and improve maintainability. * feat(CodeEditor): add CodeEditorContext for internal state management - Introduced a new `CodeEditorContext` to provide internal context values to child components, enhancing state management within the CodeEditor. - Implemented `CodeEditorProvider` for context provisioning and `useCodeEditorContext` hook for easy access to context values. - This addition supports better organization and encapsulation of editor-related functionalities. * feat(CodeEditor): introduce Panel component for enhanced toolbar functionality - Added a new `Panel` component to the CodeEditor, providing a toolbar interface with options for formatting, copying, and custom actions. - Integrated the `Panel` into the CodeEditor, allowing for a customizable user experience with buttons for common actions like undo, redo, and download. - Updated the README to include documentation for the new `Panel` component and its properties. - Enhanced TypeScript definitions to support the new component and its props. - Included storybook examples to demonstrate the usage of the `Panel` component. * changeset * fix(Panel): disable dark menu rendering in the Panel component - Updated the Panel component to set `renderDarkMenu` to false, ensuring the dark menu is not rendered. - This change improves the consistency of the user interface in light mode. * feat(CodeEditor): integrate Panel component and enhance CodeEditor functionality - Added the `Panel` component to the `CodeEditor`, allowing for a customizable toolbar with options for formatting, copying, and additional actions. - Updated `CodeEditor` to accept a `panel` prop, enabling the rendering of the `Panel` within the editor. - Enhanced TypeScript definitions to support the new `panel` prop and its interaction with the `copyButtonAppearance` prop. - Modified styling in `useThemeExtension` to accommodate the new panel layout. - Removed the `Panel.stories.tsx` file as part of the integration process. * refactor(CodeEditor, Panel): reorganize props for improved clarity and consistency - Rearranged the props in both `CodeEditor` and `Panel` components for better readability and logical grouping. - Ensured that related props are positioned together, enhancing the overall structure of the components. - Maintained existing functionality while improving the code organization. * docs(CodeEditor): update README to include new `panel` prop details - Added documentation for the optional `panel` prop in the CodeEditor, which allows rendering a toolbar interface at the top of the editor. - Provided a description of the `Panel` component's functionality and its available options, enhancing the clarity of the README. * test(Panel): add comprehensive unit tests for Panel component functionality - Introduced a new test suite for the Panel component, covering various aspects including rendering, button functionality, and accessibility. - Implemented tests for the format, copy, and secondary menu buttons, ensuring they behave as expected when interacted with. - Verified the integration with the CodeEditor context and confirmed proper handling of props like `showFormatButton`, `showCopyButton`, and custom secondary buttons. - Included tests for edge cases, such as handling undefined callback functions and empty custom button arrays, to enhance robustness. * Updated changeset * feat(CodeEditorCopyButton): enhance icon fill color handling and simplify styles - Introduced a new `getIconFill` function to dynamically determine the icon fill color based on the theme and copied state. - Simplified the CSS styles for the copy button by removing redundant selectors and consolidating transition properties. - Updated the `CopyButtonTrigger` component to accept an `iconFill` prop, allowing for more flexible icon color management. * refactor(CodeEditor): rename getContents prop for clarity - Updated the `CodeEditor` component to rename the `getContents` prop to `getContentsToCopy`, improving clarity regarding its functionality. - Ensured consistency with the recent changes in the `CodeEditorCopyButton` component. * feat(CodeEditor): enhance copy button functionality and styling - Introduced `copyButtonAppearance` prop to control the visibility of the copy button in the CodeEditor. - Implemented styles for different copy button appearances: 'hover', 'persist', and 'none'. - Updated tests to verify the rendering behavior of the copy button based on the new prop. - Refactored CodeEditor component to integrate the new copy button functionality and ensure proper rendering based on user interactions. * refactor(CodeEditor): remove unused import from CodeEditor.types.ts - Eliminated the unused `copyButtonClassName` import to clean up the code and improve maintainability. * refactor(CodeEditor): rename getContents prop to getContentsToCopy - Updated the `getContents` prop in the `CodeEditorCopyButton` to `getContentsToCopy` for improved clarity regarding its purpose. - This change aligns with recent naming conventions and enhances the overall consistency of the CodeEditor component. * docs(CodeEditor): add SecondaryButtonConfig section to README - Introduced a new section in the README for `SecondaryButtonConfig`, detailing its properties including `aria-label`, `glyph`, `href`, `label`, and `onClick`. - This addition enhances documentation clarity and provides developers with comprehensive information on configuring secondary buttons in the CodeEditor component. * fix(Panel): update copy button accessibility labels in tests - Changed the accessibility label for the copy button in tests from 'Copy text' to 'Copy' to align with the actual implementation. - Updated related test cases to ensure consistency and accuracy in verifying the copy button's presence and functionality. * refactor(Panel): update panel styles for consistency and clarity - Replaced hardcoded height with a constant for better maintainability. - Adjusted padding values to use spacing tokens for consistency with design system. - Renamed grid area from 'children' to 'innerContent' to improve clarity in layout structure. * feat(Panel): add support for custom secondary button properties - Introduced a new `disabled` property to the `SecondaryButtonConfig` interface, allowing buttons to be disabled. - Updated the `Panel` component to handle the `disabled` state for custom secondary buttons. - Enhanced the README documentation for `SecondaryButtonConfig` to include the new `disabled` property, improving clarity for developers. * fix(CodeEditor): remove href from custom secondary button configuration - Eliminated the `href` property from the custom secondary button configuration in the CodeEditor stories, streamlining the button setup. - This change enhances clarity and aligns with the updated button properties. * fix(Panel): update grid area naming and type definitions - Renamed the grid area from 'innerContent' to 'inner-content' for consistency with CSS naming conventions. - Updated type definitions in `Panel.types.ts` to use `ReactElement` and `ReactNode` directly, enhancing clarity and type safety for the `glyph` and `innerContent` properties. - Changed the `title` property type to `ReactNode` to allow for more flexible content rendering in the panel header.
1 parent e2cd0d4 commit ff5976a

19 files changed

+1134
-57
lines changed

.changeset/plenty-cities-buy.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@leafygreen-ui/code-editor': minor
3+
---
4+
5+
Adds **Panel component**: A comprehensive toolbar interface for CodeEditor with formatting, copying, and custom action buttons
6+
7+
- **New Panel component**: Provides a configurable toolbar that displays at the top of CodeEditor
8+
- **Built-in actions**: Includes format button, copy button, and secondary menu with undo/redo/download/shortcuts
9+
- **Customizable**: Supports custom secondary buttons and inner content for application-specific needs
10+
- **Fully accessible**: All buttons and menu items include proper ARIA labels and keyboard navigation
11+
- **Context integration**: Seamlessly integrates with CodeEditor context for copy functionality
12+
- **Styling**: Matches CodeEditor theme with proper grid layout and responsive design
13+
14+
The Panel component enhances the CodeEditor user experience by providing easy access to common editor actions through a clean, organized interface.

packages/code-editor/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,78 @@ console.log(greet('MongoDB user'));`;
6868
| `minHeight` _(optional)_ | Sets the editor's minimum height. | `string` | `undefined` |
6969
| `minWidth` _(optional)_ | Sets the editor's minimum width. | `string` | `undefined` |
7070
| `onChange` _(optional)_ | Callback that receives the updated editor value when changes are made. | `(value: string) => void` | `undefined` |
71+
| `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` |
7172
| `placeholder` _(optional)_ | Value to display in the editor when it is empty. | `HTMLElement \| string` | `undefined` |
7273
| `readOnly` _(optional)_ | Enables read only mode, making the contents uneditable. | `boolean` | `false` |
7374
| `tooltips` _(optional)_ | Add tooltips to the editor content that appear on hover. | `Array<CodeEditorTooltip>` | `undefined` |
7475
| `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` |
7576
| `width` _(optional)_ | Sets the editor's width. If not set, the editor will be 100% width of its parent container. | `string` | `undefined` |
7677

78+
### `<Panel>`
79+
80+
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.
81+
82+
#### Example
83+
84+
```tsx
85+
import { CodeEditor, Panel, LanguageName } from '@leafygreen-ui/code-editor';
86+
import CloudIcon from '@leafygreen-ui/icon';
87+
88+
<CodeEditor
89+
defaultValue="const greeting = 'Hello World';"
90+
language={LanguageName.javascript}
91+
panel={
92+
<Panel
93+
title="index.ts"
94+
showFormatButton
95+
showCopyButton
96+
showSecondaryMenuButton
97+
customSecondaryButtons={[
98+
{
99+
label: 'Deploy to Cloud',
100+
glyph: <CloudIcon />,
101+
onClick: () => console.log('Deploy clicked'),
102+
'aria-label': 'Deploy code to cloud',
103+
},
104+
]}
105+
onFormatClick={() => console.log('Format clicked')}
106+
onCopyClick={() => console.log('Copy clicked')}
107+
onDownloadClick={() => console.log('Download clicked')}
108+
/>
109+
}
110+
/>;
111+
```
112+
113+
#### Properties
114+
115+
| Name | Description | Type | Default |
116+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ----------- |
117+
| `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` |
118+
| `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` |
119+
| `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` |
120+
| `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` |
121+
| `onCopyClick` _(optional)_ | Callback fired when the copy button is clicked. Called after the copy operation is attempted. | `() => void` | `undefined` |
122+
| `onDownloadClick` _(optional)_ | Callback fired when the download button in the secondary menu is clicked. Called after the download operation is attempted. | `() => void` | `undefined` |
123+
| `onFormatClick` _(optional)_ | Callback fired when the format button is clicked. Called after the formatting operation is attempted. | `() => void` | `undefined` |
124+
| `onRedoClick` _(optional)_ | Callback fired when the redo button in the secondary menu is clicked. Called after the redo operation is attempted. | `() => void` | `undefined` |
125+
| `onUndoClick` _(optional)_ | Callback fired when the undo button in the secondary menu is clicked. Called after the undo operation is attempted. | `() => void` | `undefined` |
126+
| `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` |
127+
| `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` |
128+
| `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` |
129+
| `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` |
130+
| `title` _(optional)_ | Title text to display in the panel header. Typically used to show the current language or content description. | `string` | `undefined` |
131+
132+
#### `SecondaryButtonConfig`
133+
134+
| Name | Description | Type | Default |
135+
| ------------------------- | ---------------------------------------------------------------------- | -------------------- | ----------- |
136+
| `aria-label` _(optional)_ | Accessible label for the button to provide context for screen readers. | `string` | `undefined` |
137+
| `disabled` _(optional)_ | Whether the button is disabled. | `boolean` | `undefined` |
138+
| `glyph` _(optional)_ | Icon element to display in the button. | `React.ReactElement` | `undefined` |
139+
| `href` _(optional)_ | URL to navigate to when the button is clicked. | `string` | `undefined` |
140+
| `label` | Text label for the button. | `string` ||
141+
| `onClick` _(optional)_ | Callback fired when the button is clicked. | `() => void` | `undefined` |
142+
77143
## Types and Variables
78144

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

93160
## Test Utilities
94161

packages/code-editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@leafygreen-ui/icon-button": "workspace:^",
5353
"@leafygreen-ui/leafygreen-provider": "workspace:^",
5454
"@leafygreen-ui/lib": "workspace:^",
55+
"@leafygreen-ui/menu": "workspace:^",
5556
"@leafygreen-ui/palette": "workspace:^",
5657
"@leafygreen-ui/tokens": "workspace:^",
5758
"@leafygreen-ui/tooltip": "workspace:^",

packages/code-editor/src/CodeEditor.stories.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import type { StoryFn, StoryObj } from '@storybook/react';
88
import { expect, waitFor } from '@storybook/test';
99

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

1214
import { CopyButtonAppearance } from './CodeEditor/CodeEditor.types';
1315
import { LanguageName } from './CodeEditor/hooks/extensions/useLanguageExtension';
1416
import { codeSnippets } from './CodeEditor/testing';
1517
import { IndentUnits } from './CodeEditor';
16-
import { CodeEditor } from '.';
18+
import { CodeEditor, Panel } from '.';
1719

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

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

185+
export const WithPanel = Template.bind({});
186+
WithPanel.args = {
187+
language: 'typescript',
188+
defaultValue: codeSnippets.typescript,
189+
panel: (
190+
<Panel
191+
showCopyButton
192+
showFormatButton
193+
showSecondaryMenuButton
194+
title="index.tsx"
195+
customSecondaryButtons={[
196+
{
197+
label: 'Custom Button',
198+
onClick: () => {},
199+
'aria-label': 'Custom Button',
200+
glyph: <CloudIcon />,
201+
},
202+
]}
203+
/>
204+
),
205+
};
206+
183207
export const TooltipOnHover: StoryObj<{}> = {
184208
render: () => {
185209
return (

packages/code-editor/src/CodeEditor/CodeEditor.tsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,37 +27,39 @@ import {
2727
CopyButtonLgId,
2828
type HTMLElementWithCodeMirror,
2929
} from './CodeEditor.types';
30+
import { CodeEditorProvider } from './CodeEditorContext';
3031
import { useExtensions, useLazyModules, useModuleLoaders } from './hooks';
3132

3233
export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
3334
(props, forwardedRef) => {
3435
const {
35-
defaultValue,
36-
value,
37-
forceParsing: forceParsingProp = false,
38-
onChange: onChangeProp,
39-
isLoading: isLoadingProp = false,
40-
extensions: consumerExtensions = [],
41-
darkMode: darkModeProp,
4236
baseFontSize: baseFontSizeProp,
4337
className,
44-
height,
45-
maxHeight,
46-
maxWidth,
47-
minHeight,
48-
minWidth,
49-
width,
50-
language,
38+
copyButtonAppearance,
39+
darkMode: darkModeProp,
40+
defaultValue,
5141
enableClickableUrls,
5242
enableCodeFolding,
5343
enableLineNumbers,
5444
enableLineWrapping,
55-
indentUnit,
45+
extensions: consumerExtensions = [],
46+
forceParsing: forceParsingProp = false,
47+
height,
5648
indentSize,
49+
indentUnit,
50+
isLoading: isLoadingProp = false,
51+
language,
52+
maxHeight,
53+
maxWidth,
54+
minHeight,
55+
minWidth,
56+
onChange: onChangeProp,
57+
panel,
5758
placeholder,
5859
readOnly,
5960
tooltips,
60-
copyButtonAppearance,
61+
value,
62+
width,
6163
...rest
6264
} = props;
6365

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

148150
useImperativeHandle(forwardedRef, () => ({
149151
getEditorViewInstance: () => editorViewRef.current,
152+
getContents,
150153
}));
151154

155+
const contextValue = {
156+
getContents,
157+
};
158+
152159
return (
153160
<div
154161
ref={editorContainerRef}
@@ -164,16 +171,20 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(
164171
})}
165172
{...rest}
166173
>
167-
{(copyButtonAppearance === CopyButtonAppearance.Hover ||
168-
copyButtonAppearance === CopyButtonAppearance.Persist) && (
169-
<CodeEditorCopyButton
170-
getContentsToCopy={getContents}
171-
className={getCopyButtonStyles(copyButtonAppearance)}
172-
variant={CopyButtonVariant.Button}
173-
disabled={isLoadingProp || isLoading}
174-
data-lgid={CopyButtonLgId}
175-
/>
174+
{panel && (
175+
<CodeEditorProvider value={contextValue}>{panel}</CodeEditorProvider>
176176
)}
177+
{!panel &&
178+
(copyButtonAppearance === CopyButtonAppearance.Hover ||
179+
copyButtonAppearance === CopyButtonAppearance.Persist) && (
180+
<CodeEditorCopyButton
181+
getContentsToCopy={getContents}
182+
className={getCopyButtonStyles(copyButtonAppearance)}
183+
variant={CopyButtonVariant.Button}
184+
disabled={isLoadingProp || isLoading}
185+
data-lgid={CopyButtonLgId}
186+
/>
187+
)}
177188
{(isLoadingProp || isLoading) && (
178189
<div
179190
className={getLoaderStyles({

0 commit comments

Comments
 (0)