Skip to content

Commit

Permalink
HDS::CodeEditor with Code Mirror 6 (#2573)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex <[email protected]>
Co-authored-by: Kristin Bradley <[email protected]>
  • Loading branch information
3 people authored Jan 17, 2025
1 parent 62935d5 commit 2787f32
Show file tree
Hide file tree
Showing 37 changed files with 2,191 additions and 53 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-poets-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hashicorp/design-system-components": minor
---

`Hds::CodeEditor` - Added new CodeMirror 6 supported code editor component
`hds-code-editor` modifier - Added new code editor modifier which converts the element it is applied to into a CodeMirror 6 code editor
3 changes: 2 additions & 1 deletion packages/components/babel.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
],
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-private-methods"
"@babel/plugin-transform-private-methods",
"ember-concurrency/async-arrow-task-transform"
]
}
20 changes: 20 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
"lint:js:fix": "eslint . --fix"
},
"dependencies": {
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-sql": "^6.8.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.10.3",
"@codemirror/legacy-modes": "^6.4.2",
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.36.2",
"@ember/render-modifiers": "^2.1.0",
"@ember/string": "^3.1.1",
"@ember/test-waiters": "^3.1.0",
Expand All @@ -43,6 +52,7 @@
"@hashicorp/design-system-tokens": "^2.2.2",
"@hashicorp/flight-icons": "^3.8.0",
"clipboard-polyfill": "^4.1.1",
"codemirror-lang-hcl": "^0.0.0-beta.2",
"decorator-transforms": "^1.2.1",
"ember-a11y-refocus": "^4.1.4",
"ember-cli-sass": "^11.0.1",
Expand Down Expand Up @@ -155,6 +165,11 @@
"./components/hds/code-block/description.js": "./dist/_app_/components/hds/code-block/description.js",
"./components/hds/code-block/index.js": "./dist/_app_/components/hds/code-block/index.js",
"./components/hds/code-block/title.js": "./dist/_app_/components/hds/code-block/title.js",
"./components/hds/code-editor/description.js": "./dist/_app_/components/hds/code-editor/description.js",
"./components/hds/code-editor/full-screen-button.js": "./dist/_app_/components/hds/code-editor/full-screen-button.js",
"./components/hds/code-editor/generic.js": "./dist/_app_/components/hds/code-editor/generic.js",
"./components/hds/code-editor/index.js": "./dist/_app_/components/hds/code-editor/index.js",
"./components/hds/code-editor/title.js": "./dist/_app_/components/hds/code-editor/title.js",
"./components/hds/copy/button/index.js": "./dist/_app_/components/hds/copy/button/index.js",
"./components/hds/copy/snippet/index.js": "./dist/_app_/components/hds/copy/snippet/index.js",
"./components/hds/dialog-primitive/body.js": "./dist/_app_/components/hds/dialog-primitive/body.js",
Expand Down Expand Up @@ -300,6 +315,11 @@
"./instance-initializers/load-sprite.js": "./dist/_app_/instance-initializers/load-sprite.js",
"./modifiers/hds-anchored-position.js": "./dist/_app_/modifiers/hds-anchored-position.js",
"./modifiers/hds-clipboard.js": "./dist/_app_/modifiers/hds-clipboard.js",
"./modifiers/hds-code-editor.js": "./dist/_app_/modifiers/hds-code-editor.js",
"./modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js": "./dist/_app_/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js",
"./modifiers/hds-code-editor/palettes/hds-dark-palette.js": "./dist/_app_/modifiers/hds-code-editor/palettes/hds-dark-palette.js",
"./modifiers/hds-code-editor/themes/hds-dark-theme.js": "./dist/_app_/modifiers/hds-code-editor/themes/hds-dark-theme.js",
"./modifiers/hds-code-editor/types.js": "./dist/_app_/modifiers/hds-code-editor/types.js",
"./modifiers/hds-register-event.js": "./dist/_app_/modifiers/hds-register-event.js",
"./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js",
"./services/hds-time.js": "./dist/_app_/services/hds-time.js"
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ export { default as HdsCodeBlockDescription } from './components/hds/code-block/
export { default as HdsCodeBlockTitle } from './components/hds/code-block/title.ts';
export * from './components/hds/code-block/types.ts';

// CodeEditor
export { default as HdsCodeEditor } from './components/hds/code-editor/index.ts';
export { default as HdsCodeEditorDescription } from './components/hds/code-editor/description.ts';
export { default as HdsCodeEditorTitle } from './components/hds/code-editor/title.ts';
export { default as HdsCodeEditorFullScreenButton } from './components/hds/code-editor/full-screen-button.ts';

// CopyButton
export { default as HdsCopyButton } from './components/hds/copy/button/index.ts';
export * from './components/hds/copy/button/types.ts';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Hds::Text::Body class="hds-code-editor__description" @tag="p" @size="100" ...attributes>
{{yield}}
</Hds::Text::Body>
20 changes: 20 additions & 0 deletions packages/components/src/components/hds/code-editor/description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import TemplateOnlyComponent from '@ember/component/template-only';

import type { HdsTextBodySignature } from '../text/body';

export interface HdsCodeEditorDescriptionSignature {
Blocks: {
default: [];
};
Element: HdsTextBodySignature['Element'];
}

const HdsCodeEditorDescription =
TemplateOnlyComponent<HdsCodeEditorDescriptionSignature>();

export default HdsCodeEditorDescription;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Hds::Button
class={{this.className}}
aria-pressed={{@isFullScreen}}
@isIconOnly={{true}}
@color="secondary"
@size="small"
@icon={{this.state}}
@text="Toggle full screen view"
{{on "click" @onToggleFullScreen}}
...attributes
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';

import type { HdsButtonSignature } from '../button';

export interface HdsCodeEditorFullScreenButtonSignature {
Args: {
isFullScreen: boolean;
onToggleFullScreen: () => void;
};
Element: HdsButtonSignature['Element'];
}

export default class HdsCodeEditorFullScreenButton extends Component<HdsCodeEditorFullScreenButtonSignature> {
get state(): 'minimize' | 'maximize' {
return this.args.isFullScreen ? 'minimize' : 'maximize';
}

get className(): string {
const classes = [
'hds-code-editor__full-screen-button',
'hds-code-editor__button',
];

const stateClass = `hds-code-editor__full-screen-button--${this.state}`;

classes.push(stateClass);

return classes.join(' ');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<div class="hds-code-editor__header-generic" ...attributes>
{{yield}}
</div>
18 changes: 18 additions & 0 deletions packages/components/src/components/hds/code-editor/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import TemplateOnlyComponent from '@ember/component/template-only';

export interface HdsCodeEditorGenericSignature {
Blocks: {
default: [];
};
Element: HTMLDivElement;
}

const HdsCodeEditorGeneric =
TemplateOnlyComponent<HdsCodeEditorGenericSignature>();

export default HdsCodeEditorGeneric;
70 changes: 70 additions & 0 deletions packages/components/src/components/hds/code-editor/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

<div
id={{this._id}}
class={{this.classNames}}
{{! @glint-expect-error - https://github.com/josemarluedke/ember-focus-trap/issues/86 }}
{{focus-trap isActive=this._isFullScreen}}
{{this._handleEscape}}
...attributes
>
{{! header }}
{{#if (or this.hasActions (has-block))}}
<div class="hds-code-editor__header">
<div class="hds-code-editor__header-content">
{{yield
(hash
Title=(component "hds/code-editor/title" editorId=this._id onInsert=this.registerTitleElement)
Description=(component "hds/code-editor/description")
Generic=(component "hds/code-editor/generic")
)
}}
</div>

{{#if this.hasActions}}
<div class="hds-code-editor__header-actions">
{{#if @hasCopyButton}}
<Hds::Copy::Button
class="hds-code-editor__button hds-code-editor__copy-button"
@isIconOnly={{true}}
@size="small"
@text="Copy"
@textToCopy={{this._value}}
/>
{{/if}}
{{#if @hasFullScreenButton}}
<Hds::CodeEditor::FullScreenButton
@isFullScreen={{this._isFullScreen}}
@onToggleFullScreen={{this.toggleFullScreen}}
/>
{{/if}}
</div>
{{/if}}
</div>
{{/if}}

{{! editor }}
<div
class="hds-code-editor__editor"
{{hds-code-editor
ariaLabel=@ariaLabel
ariaLabelledBy=this.ariaLabelledBy
value=@value
language=@language
onBlur=@onBlur
onInput=this.onInput
onSetup=this.onSetup
}}
/>

{{! loader }}
{{#unless this._isSetupComplete}}
<div class="hds-code-editor__loader" aria-live="polite" role="status">
<Hds::Icon @name="loading" @size="24" />
<span class="sr-only">Loading</span>
</div>
{{/unless}}
</div>
136 changes: 136 additions & 0 deletions packages/components/src/components/hds/code-editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { modifier } from 'ember-modifier';

import type { ComponentLike } from '@glint/template';
import type { HdsCodeEditorSignature as HdsCodeEditorModifierSignature } from 'src/modifiers/hds-code-editor';
import type { HdsCodeEditorDescriptionSignature } from './description';
import type { HdsCodeEditorTitleSignature } from './title';
import type { HdsCodeEditorGenericSignature } from './generic';
import type { EditorView } from '@codemirror/view';
import { guidFor } from '@ember/object/internals';

export interface HdsCodeEditorSignature {
Args: {
ariaLabel?: string;
ariaLabelledBy?: string;
hasCopyButton?: boolean;
hasFullScreenButton?: boolean;
isStandalone?: boolean;
language?: HdsCodeEditorModifierSignature['Args']['Named']['language'];
value?: HdsCodeEditorModifierSignature['Args']['Named']['value'];
onBlur?: HdsCodeEditorModifierSignature['Args']['Named']['onBlur'];
onInput?: HdsCodeEditorModifierSignature['Args']['Named']['onInput'];
onSetup?: HdsCodeEditorModifierSignature['Args']['Named']['onSetup'];
};
Blocks: {
default: [
{
Title?: ComponentLike<HdsCodeEditorTitleSignature>;
Description?: ComponentLike<HdsCodeEditorDescriptionSignature>;
Generic?: ComponentLike<HdsCodeEditorGenericSignature>;
},
];
};
Element: HTMLDivElement;
}

export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
@tracked private _isFullScreen = false;
@tracked private _isSetupComplete = false;
@tracked private _value;
@tracked private _titleId: string | undefined;

private _id = guidFor(this);

private _handleEscape = modifier(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape' || !this._isFullScreen) {
return;
}

this.toggleFullScreen();
};

document.addEventListener('keydown', handleKeyDown);

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
});

constructor(owner: unknown, args: HdsCodeEditorSignature['Args']) {
super(owner, args);

if (args.value) {
this._value = args.value;
}
}

get ariaLabelledBy(): string | undefined {
if (this.args.ariaLabel !== undefined) {
return;
}

return this.args.ariaLabelledBy ?? this._titleId;
}

get hasActions(): boolean {
return (this.args.hasCopyButton || this.args.hasFullScreenButton) ?? false;
}

get isStandalone(): boolean {
return this.args.isStandalone ?? true;
}

get classNames(): string {
// Currently there is only one theme so the class name is hard-coded.
// In the future, additional themes such as a "light" theme could be added.
const classes = ['hds-code-editor', 'hds-code-editor--theme-dark'];

if (this._isFullScreen) {
classes.push('hds-code-editor--is-full-screen');
}

if (this.isStandalone) {
classes.push('hds-code-editor--is-standalone');
}

return classes.join(' ');
}

@action
registerTitleElement(element: HdsCodeEditorTitleSignature['Element']): void {
this._titleId = element.id;
}

@action
toggleFullScreen(): void {
this._isFullScreen = !this._isFullScreen;
}

@action
onInput(newValue: string): void {
this._value = newValue;
this.args.onInput?.(newValue);
}

@action
onKeyDown(event: KeyboardEvent): void {
if (event.key === 'Escape' && this._isFullScreen) {
this.toggleFullScreen();
}
}

@action
onSetup(editorView: EditorView): void {
this._isSetupComplete = true;
this.args.onSetup?.(editorView);
}
}
Loading

0 comments on commit 2787f32

Please sign in to comment.