Skip to content

LG-5067: Create basic CodeEditor component/package #2858

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

Merged
merged 32 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0989452
feat(code-editor): initialize code editor package with README, config…
tsck May 13, 2025
f17150a
feat(code-editor): enhance CodeEditor component with new props and de…
tsck May 13, 2025
73d1ac5
refactor(code-editor): change import statements to use 'type' for Cod…
tsck May 13, 2025
5809ce3
feat(code-editor): update CodeEditor stories with args and argTypes f…
tsck May 13, 2025
ae315fa
Start test suite
tsck May 14, 2025
815fff1
Fix deps
tsck May 14, 2025
3d48e45
feat(code-editor): refactor extensions handling using Compartment for…
tsck May 15, 2025
5759b82
feat(tests): enhance CodeEditor tests with improved selector handling…
tsck May 15, 2025
67e2a5f
feat(code-editor): rename 'value' prop to 'initialValue' for consiste…
tsck May 15, 2025
4a9abe4
feat(code-editor): rename 'initialValue' prop to 'defaultValue' for c…
tsck May 15, 2025
bee8b1d
feat(code-editor): enable active line highlighting based on prop for …
tsck May 15, 2025
2f21453
feat(tests): enable forceParsing test and update mock implementation …
tsck May 15, 2025
b82df4c
feat(tests): refactor CodeEditor tests to use new renderCodeEditor ut…
tsck May 15, 2025
0a846db
refactor(tests): move MutationObserver mock to testUtils for better o…
tsck May 15, 2025
de05360
feat(docs): update README to include new CodeEditor properties and te…
tsck May 16, 2025
1deb79f
refactor(tests): update typing test to use userEvent and export edito…
tsck May 16, 2025
3aa98de
refactor(tests): update CodeEditor interactions and test utilities fo…
tsck May 16, 2025
55b389a
test: update forceParsing method test to remove async and improve cla…
tsck May 16, 2025
647ad60
changeset
tsck May 16, 2025
c26ce0f
feat(docs): add CodeEditor package information to README
tsck May 16, 2025
d79e9a2
feat: expand exports in index.ts to include additional CodeEditor typ…
tsck May 16, 2025
747f201
refactor(tests): move renderCodeEditor utility to CodeEditor.testUtil…
tsck May 16, 2025
5142f97
feat(tests): add TestUtils for improved test rendering utilities
tsck May 16, 2025
1553697
fix: correct rendering description in tests and update README for Cod…
tsck May 20, 2025
40e1508
docs(code-editor): update README with CodeEditor example and default …
tsck May 20, 2025
e2f6d50
chore(package): update node engine version to >= 18.20.8 (#2868)
tsck May 23, 2025
e7165e0
fix(package): update main and types paths in package.json
tsck May 23, 2025
8eae613
Merge branch 'main' of github.com:mongodb/leafygreen-ui into LG-5067/…
tsck May 23, 2025
4e651f4
chore(package): add @lg-tools/build to devDependencies
tsck May 23, 2025
d2d186a
chore(package): update build script in package.json
tsck May 23, 2025
af0b4db
feat(button): add exports field for module resolution
tsck May 23, 2025
ee82832
feat(code-editor): add exports field for module resolution
tsck May 23, 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
12 changes: 12 additions & 0 deletions .changeset/fair-boats-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@leafygreen-ui/code-editor': minor
---

- Creates package.
- Adds `CodeEditor` component with the following functionality:
- Basic setup
- Line wrapping
- Hyperlink support
- Forced parsing
- Placeholders
- Adds `renderEditor` test utility which exposes a number of helper methods.
Comment on lines +2 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is redundant? I believe this will lead to a republish of the same source code in 0.1.0 and 0.2.0

Also, is the plan to do incremental releases or did you want to wrap it all up into an integration branch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh I see. So on the init package commit we typically don't have a changeset at all? Curious if it would be better then to bump the package down to 0.0.0?

I was just going to do incremental releases but if the team prefers integration branches I'm ok with that as well. What's the benefit of doing an integration branch here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

My understanding is that publishing v0.1.0 and using incremental releases can be useful for consumers that need immediate access. Thinking some more on it, I'm not sure it would make sense to allow this to release until it's ready to be consumed which might be after designs/styling are complete?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think where I'm not following is if something is a sub-v1 release, that indicates to consumers that there's substantial risk in using it. That to me is a beta release by definition. So I'm not sure I understand why an integration branch would be necessary here. I could see that being useful when something is already versioned. For example, if this was already a v1 and the work in this epic would bring it to v2, an integration branch would be necessary since incomplete work would minor bump the v1 version, and indicate to consumers that its ready when its not. However, for sub-v1 releases, I'm not sure I feel that applies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, spoke this one through with the team and am going to rebase this to an integration branch to mitigate the concerns that this isn't styled yet. Thanks for bringing this up!

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import Button from '@leafygreen-ui/button';
| [@leafygreen-ui/checkbox](./packages/checkbox) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/checkbox)](https://www.npmjs.com/package/@leafygreen-ui/checkbox) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/checkbox?color=white) | [Live Example](http://mongodb.design/component/checkbox/live-example) |
| [@leafygreen-ui/chip](./packages/chip) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/chip)](https://www.npmjs.com/package/@leafygreen-ui/chip) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/chip?color=white) | [Live Example](http://mongodb.design/component/chip/live-example) |
| [@leafygreen-ui/code](./packages/code) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/code)](https://www.npmjs.com/package/@leafygreen-ui/code) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/code?color=white) | [Live Example](http://mongodb.design/component/code/live-example) |
| [@leafygreen-ui/code-editor](./packages/code-editor) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/code-editor)](https://www.npmjs.com/package/@leafygreen-ui/code-editor) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/code-editor?color=white) | [Live Example](http://mongodb.design/component/code-editor/live-example) |
| [@leafygreen-ui/combobox](./packages/combobox) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/combobox)](https://www.npmjs.com/package/@leafygreen-ui/combobox) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/combobox?color=white) | [Live Example](http://mongodb.design/component/combobox/live-example) |
| [@leafygreen-ui/confirmation-modal](./packages/confirmation-modal) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/confirmation-modal)](https://www.npmjs.com/package/@leafygreen-ui/confirmation-modal) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/confirmation-modal?color=white) | [Live Example](http://mongodb.design/component/confirmation-modal/live-example) |
| [@leafygreen-ui/copyable](./packages/copyable) | [![version](https://img.shields.io/npm/v/@leafygreen-ui/copyable)](https://www.npmjs.com/package/@leafygreen-ui/copyable) | ![downloads](https://img.shields.io/npm/dm/@leafygreen-ui/copyable?color=white) | [Live Example](http://mongodb.design/component/copyable/live-example) |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "Apache-2.0",
"private": true,
"engines": {
"node": ">= 18.12.0",
"node": ">= 18.20.8",
"pnpm": ">= 9.15.0"
},
"scripts": {
Expand Down
183 changes: 183 additions & 0 deletions packages/code-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Code Editor

![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/code-editor.svg)

#### [View on MongoDB.design](https://www.mongodb.design/component/code-editor/live-example/)

## Installation

### PNPM

```shell
pnpm add @leafygreen-ui/code-editor
```

### Yarn

```shell
yarn add @leafygreen-ui/code-editor
```

### NPM

```shell
npm install @leafygreen-ui/code-editor
```

## Component

### `<CodeEditor>`
Comment on lines +27 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this a stub for the future or can this be removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope, that's the component name that's being documented here.


#### Example

```tsx
import { CodeEditor } from '@leafygreen-ui/code-editor';

const sampleCode = `// Your code goes here
function greet(name: string): string {
return \`Hello, \${name}!\`;
}

console.log(greet('MongoDB user'));`;

<CodeEditor defaultValue={sampleCode} />;
```

#### Properties

| Name | Description | Type | Default |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | ----------- |
| `defaultValue` _(optional)_ | Initial value to render in the editor. | `string` | `undefined` |
| `enableActiveLineHighlighting` _(optional)_ | Enables highlighting of the active line. | `boolean` | `true` |
| `enableClickableUrls` _(optional)_ | Renders URLs as clickable links in the editor. | `boolean` | `true` |
| `enableCodeFolding` _(optional)_ | Enables code folding arrows in the gutter. | `boolean` | `true` |
| `enableLineNumbers` _(optional)_ | Enables line numbers in the editor’s gutter. | `boolean` | `true` |
| `enableLineWrapping` _(optional)_ | Enables line wrapping when the text exceeds the editor’s width. | `boolean` | `true` |
| `forceParsing` _(optional)_ | _**This should be used with caution as it can significantly impact performance!**_<br><br>Forces the parsing of the complete document, even parts not currently visible.<br><br>By default, the editor optimizes performance by only parsing the code that is visible on the screen, which is especially beneficial when dealing with large amounts of code. Enabling this option overrides this behavior and forces the parsing of all code, visible or not. This should generally be reserved for exceptional circumstances. | `boolean` | `false` |
| `onChange` _(optional)_ | Callback that receives the updated editor value when changes are made. | `(value: string) => void;` | `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` |

## Types

| Name | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CodeEditorProps` | Props that can be passed to the `CodeEditor` component. |
| `CodeEditorSelectors` | Enum-like map of CSS selectors for common elements that make up the code editor. These can be useful in testing. |
| `CodeMirrorExtension` | Underlying CodeMirror editor `Extension` type. For more information see https://codemirror.net/docs/ref/#state.Extension. |
| `CodeMirrorRef` | Underlying CodeMirror editor ref type. When a ref is passed to the `CodeEditor` it will be of this type and give you direct access to CodeMirror internals such as `CodeMirrorState` and `CodeMirrorView` |
| `CodeMirrorState` | Underlying CodeMirror editor `EditorState` type. For more information see https://codemirror.net/docs/ref/#state.EditorState. |
| `CodeMirrorView` | Underlying CodeMirror editor `EditorView` type. For more information see https://codemirror.net/docs/ref/#view.EditorView. |
| `RenderedTestResult` | Type returned by the `renderEditor` test utility. More info in Test Utilities section. |
| `RenderedTestEditorType` | Editor type used to interact with editor in a Jest test. More info in Test Utilities section. |

## Test Utlities

### `renderEditor`

Renders the editor in a Jest test.

```tsx
function renderEditor(props?: Partial<CodeEditorProps>): RenderResult;
```

### `RenderResult`

#### `container`

HTML element of the container of the editor that was rendered.

#### `editor`

Editor object used for querying and interacting with the rendered editor.

Has the following interface:

```tsx
{
// Used to find single element in the editor. Will throw error if not found.
getBySelector(
selector: CodeEditorSelectors,
options?: { text?: string },
): Element;

// Used to find single element in the editor. Will return null if not found. Useful when checking if something has not rendered.
queryBySelector(selector: CodeEditorSelectors, options?: {
text?: string;
}): Element | null;

// Returns whether editor is in a read only state.
isReadOnly(): boolean;

// Returns whether line wrapping is enabled in editor.
isLineWrappingEnabled(): boolean;

// Group of actions that can be performed on the editor.
interactions: {
// Insert text into the editor
insertText(text: string, options?: { to?: number; from?: number; }): undefined;
}
}
```

### Examples

#### Test selector has rendered

```tsx
import { TestUtils } from '@leafygreen-ui/code-editor';

const { renderEditor } = TestUtils;

test('Line numbers rendered', () => {
const { editor } = renderCodeEditor({
defaultValue: 'content',
enableLineNumbers: true,
});
expect(
editor.getBySelector(CodeEditorSelectors.GutterElement, {
text: '1',
}),
).toBeInTheDocument();
});
```

#### Test selector has not rendered

```tsx
import { TestUtils } from '@leafygreen-ui/code-editor';

const { renderEditor } = TestUtils;

test('Fold gutter does not render', () => {
const { editor } = renderCodeEditor({ enableCodeFolding: false });
expect(
// Note use of queryBy instead of getBy when test if not rendered
editor.queryBySelector(CodeEditorSelectors.FoldGutter),
).not.toBeInTheDocument();
});
```

#### Test user interaction

```tsx
import { TestUtils } from '@leafygreen-ui/code-editor';

const { renderEditor } = TestUtils;

test('Updates value on when user types', () => {
const { editor } = renderCodeEditor();

expect(
editor.getBySelector(CodeEditorSelectors.Content),
).not.toHaveTextContent('new content');

act(() => {
editor.interactions.insertText('new content');
});

expect(editor.getBySelector(CodeEditorSelectors.Content)).toHaveTextContent(
'new content',
);
});
```
45 changes: 45 additions & 0 deletions packages/code-editor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@leafygreen-ui/code-editor",
"version": "0.1.0",
"description": "LeafyGreen UI Kit Code Editor",
"main": "./dist/umd/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"license": "Apache-2.0",
"exports": {
".": {
"require": "./dist/umd/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts"
}
},
"scripts": {
"build": "lg-build bundle",
"tsc": "lg-build tsc",
"docs": "lg-build docs"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@codemirror/language": "^6.11.0",
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/hooks": "workspace:^",
"@leafygreen-ui/leafygreen-provider": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@uiw/codemirror-extensions-hyper-link": "^4.23.12",
"@uiw/react-codemirror": "^4.23.10"
},
"homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/code-editor",
"repository": {
"type": "git",
"url": "https://github.com/mongodb/leafygreen-ui"
},
"bugs": {
"url": "https://jira.mongodb.org/projects/LG/summary"
},
"devDependencies": {
"@lg-tools/build": "workspace:^"
}
}
57 changes: 57 additions & 0 deletions packages/code-editor/src/CodeEditor.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { StoryFn } from '@storybook/react';

import { CodeEditor } from '.';

export default {
title: 'Components/CodeEditor',
component: CodeEditor,
decorators: [
Story => (
<div style={{ width: '100%', height: '100vh' }}>
<Story />
</div>
),
],
args: {
enableActiveLineHighlighting: true,
enableClickableUrls: true,
enableCodeFolding: true,
enableLineNumbers: true,
enableLineWrapping: true,
defaultValue: '',
forceParsing: false,
placeholder: 'Type your code here...',
readOnly: false,
},
argTypes: {
enableActiveLineHighlighting: {
control: { type: 'boolean' },
},
enableClickableUrls: {
control: { type: 'boolean' },
},
enableCodeFolding: {
control: { type: 'boolean' },
},
enableLineNumbers: {
control: { type: 'boolean' },
},
enableLineWrapping: {
control: { type: 'boolean' },
},
placeholder: {
control: { type: 'text' },
},
readOnly: {
control: { type: 'boolean' },
},
defaultValue: {
control: { type: 'text' },
},
},
};

const Template: StoryFn<typeof CodeEditor> = args => <CodeEditor {...args} />;

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