Skip to content

Commit 2787f32

Browse files
zamoorealex-juKristinLBradley
authored
HDS::CodeEditor with Code Mirror 6 (#2573)
Co-authored-by: Alex <alex-ju@users.noreply.github.com> Co-authored-by: Kristin Bradley <kristin.bradley@hashicorp.com>
1 parent 62935d5 commit 2787f32

37 files changed

+2191
-53
lines changed

.changeset/long-poets-wonder.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
`Hds::CodeEditor` - Added new CodeMirror 6 supported code editor component
6+
`hds-code-editor` modifier - Added new code editor modifier which converts the element it is applied to into a CodeMirror 6 code editor

packages/components/babel.config.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
],
2727
["@babel/plugin-proposal-decorators", { "legacy": true }],
2828
"@babel/plugin-transform-class-properties",
29-
"@babel/plugin-transform-private-methods"
29+
"@babel/plugin-transform-private-methods",
30+
"ember-concurrency/async-arrow-task-transform"
3031
]
3132
}

packages/components/package.json

+20
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@
3535
"lint:js:fix": "eslint . --fix"
3636
},
3737
"dependencies": {
38+
"@codemirror/commands": "^6.8.0",
39+
"@codemirror/lang-go": "^6.0.1",
40+
"@codemirror/lang-json": "^6.0.1",
41+
"@codemirror/lang-sql": "^6.8.0",
42+
"@codemirror/lang-yaml": "^6.1.2",
43+
"@codemirror/language": "^6.10.3",
44+
"@codemirror/legacy-modes": "^6.4.2",
45+
"@codemirror/state": "^6.5.0",
46+
"@codemirror/view": "^6.36.2",
3847
"@ember/render-modifiers": "^2.1.0",
3948
"@ember/string": "^3.1.1",
4049
"@ember/test-waiters": "^3.1.0",
@@ -43,6 +52,7 @@
4352
"@hashicorp/design-system-tokens": "^2.2.2",
4453
"@hashicorp/flight-icons": "^3.8.0",
4554
"clipboard-polyfill": "^4.1.1",
55+
"codemirror-lang-hcl": "^0.0.0-beta.2",
4656
"decorator-transforms": "^1.2.1",
4757
"ember-a11y-refocus": "^4.1.4",
4858
"ember-cli-sass": "^11.0.1",
@@ -155,6 +165,11 @@
155165
"./components/hds/code-block/description.js": "./dist/_app_/components/hds/code-block/description.js",
156166
"./components/hds/code-block/index.js": "./dist/_app_/components/hds/code-block/index.js",
157167
"./components/hds/code-block/title.js": "./dist/_app_/components/hds/code-block/title.js",
168+
"./components/hds/code-editor/description.js": "./dist/_app_/components/hds/code-editor/description.js",
169+
"./components/hds/code-editor/full-screen-button.js": "./dist/_app_/components/hds/code-editor/full-screen-button.js",
170+
"./components/hds/code-editor/generic.js": "./dist/_app_/components/hds/code-editor/generic.js",
171+
"./components/hds/code-editor/index.js": "./dist/_app_/components/hds/code-editor/index.js",
172+
"./components/hds/code-editor/title.js": "./dist/_app_/components/hds/code-editor/title.js",
158173
"./components/hds/copy/button/index.js": "./dist/_app_/components/hds/copy/button/index.js",
159174
"./components/hds/copy/snippet/index.js": "./dist/_app_/components/hds/copy/snippet/index.js",
160175
"./components/hds/dialog-primitive/body.js": "./dist/_app_/components/hds/dialog-primitive/body.js",
@@ -300,6 +315,11 @@
300315
"./instance-initializers/load-sprite.js": "./dist/_app_/instance-initializers/load-sprite.js",
301316
"./modifiers/hds-anchored-position.js": "./dist/_app_/modifiers/hds-anchored-position.js",
302317
"./modifiers/hds-clipboard.js": "./dist/_app_/modifiers/hds-clipboard.js",
318+
"./modifiers/hds-code-editor.js": "./dist/_app_/modifiers/hds-code-editor.js",
319+
"./modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js": "./dist/_app_/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js",
320+
"./modifiers/hds-code-editor/palettes/hds-dark-palette.js": "./dist/_app_/modifiers/hds-code-editor/palettes/hds-dark-palette.js",
321+
"./modifiers/hds-code-editor/themes/hds-dark-theme.js": "./dist/_app_/modifiers/hds-code-editor/themes/hds-dark-theme.js",
322+
"./modifiers/hds-code-editor/types.js": "./dist/_app_/modifiers/hds-code-editor/types.js",
303323
"./modifiers/hds-register-event.js": "./dist/_app_/modifiers/hds-register-event.js",
304324
"./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js",
305325
"./services/hds-time.js": "./dist/_app_/services/hds-time.js"

packages/components/src/components.ts

+6
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ export { default as HdsCodeBlockDescription } from './components/hds/code-block/
7272
export { default as HdsCodeBlockTitle } from './components/hds/code-block/title.ts';
7373
export * from './components/hds/code-block/types.ts';
7474

75+
// CodeEditor
76+
export { default as HdsCodeEditor } from './components/hds/code-editor/index.ts';
77+
export { default as HdsCodeEditorDescription } from './components/hds/code-editor/description.ts';
78+
export { default as HdsCodeEditorTitle } from './components/hds/code-editor/title.ts';
79+
export { default as HdsCodeEditorFullScreenButton } from './components/hds/code-editor/full-screen-button.ts';
80+
7581
// CopyButton
7682
export { default as HdsCopyButton } from './components/hds/copy/button/index.ts';
7783
export * from './components/hds/copy/button/types.ts';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<Hds::Text::Body class="hds-code-editor__description" @tag="p" @size="100" ...attributes>
2+
{{yield}}
3+
</Hds::Text::Body>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import TemplateOnlyComponent from '@ember/component/template-only';
7+
8+
import type { HdsTextBodySignature } from '../text/body';
9+
10+
export interface HdsCodeEditorDescriptionSignature {
11+
Blocks: {
12+
default: [];
13+
};
14+
Element: HdsTextBodySignature['Element'];
15+
}
16+
17+
const HdsCodeEditorDescription =
18+
TemplateOnlyComponent<HdsCodeEditorDescriptionSignature>();
19+
20+
export default HdsCodeEditorDescription;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Hds::Button
2+
class={{this.className}}
3+
aria-pressed={{@isFullScreen}}
4+
@isIconOnly={{true}}
5+
@color="secondary"
6+
@size="small"
7+
@icon={{this.state}}
8+
@text="Toggle full screen view"
9+
{{on "click" @onToggleFullScreen}}
10+
...attributes
11+
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import Component from '@glimmer/component';
7+
8+
import type { HdsButtonSignature } from '../button';
9+
10+
export interface HdsCodeEditorFullScreenButtonSignature {
11+
Args: {
12+
isFullScreen: boolean;
13+
onToggleFullScreen: () => void;
14+
};
15+
Element: HdsButtonSignature['Element'];
16+
}
17+
18+
export default class HdsCodeEditorFullScreenButton extends Component<HdsCodeEditorFullScreenButtonSignature> {
19+
get state(): 'minimize' | 'maximize' {
20+
return this.args.isFullScreen ? 'minimize' : 'maximize';
21+
}
22+
23+
get className(): string {
24+
const classes = [
25+
'hds-code-editor__full-screen-button',
26+
'hds-code-editor__button',
27+
];
28+
29+
const stateClass = `hds-code-editor__full-screen-button--${this.state}`;
30+
31+
classes.push(stateClass);
32+
33+
return classes.join(' ');
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
<div class="hds-code-editor__header-generic" ...attributes>
6+
{{yield}}
7+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import TemplateOnlyComponent from '@ember/component/template-only';
7+
8+
export interface HdsCodeEditorGenericSignature {
9+
Blocks: {
10+
default: [];
11+
};
12+
Element: HTMLDivElement;
13+
}
14+
15+
const HdsCodeEditorGeneric =
16+
TemplateOnlyComponent<HdsCodeEditorGenericSignature>();
17+
18+
export default HdsCodeEditorGeneric;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
6+
<div
7+
id={{this._id}}
8+
class={{this.classNames}}
9+
{{! @glint-expect-error - https://github.com/josemarluedke/ember-focus-trap/issues/86 }}
10+
{{focus-trap isActive=this._isFullScreen}}
11+
{{this._handleEscape}}
12+
...attributes
13+
>
14+
{{! header }}
15+
{{#if (or this.hasActions (has-block))}}
16+
<div class="hds-code-editor__header">
17+
<div class="hds-code-editor__header-content">
18+
{{yield
19+
(hash
20+
Title=(component "hds/code-editor/title" editorId=this._id onInsert=this.registerTitleElement)
21+
Description=(component "hds/code-editor/description")
22+
Generic=(component "hds/code-editor/generic")
23+
)
24+
}}
25+
</div>
26+
27+
{{#if this.hasActions}}
28+
<div class="hds-code-editor__header-actions">
29+
{{#if @hasCopyButton}}
30+
<Hds::Copy::Button
31+
class="hds-code-editor__button hds-code-editor__copy-button"
32+
@isIconOnly={{true}}
33+
@size="small"
34+
@text="Copy"
35+
@textToCopy={{this._value}}
36+
/>
37+
{{/if}}
38+
{{#if @hasFullScreenButton}}
39+
<Hds::CodeEditor::FullScreenButton
40+
@isFullScreen={{this._isFullScreen}}
41+
@onToggleFullScreen={{this.toggleFullScreen}}
42+
/>
43+
{{/if}}
44+
</div>
45+
{{/if}}
46+
</div>
47+
{{/if}}
48+
49+
{{! editor }}
50+
<div
51+
class="hds-code-editor__editor"
52+
{{hds-code-editor
53+
ariaLabel=@ariaLabel
54+
ariaLabelledBy=this.ariaLabelledBy
55+
value=@value
56+
language=@language
57+
onBlur=@onBlur
58+
onInput=this.onInput
59+
onSetup=this.onSetup
60+
}}
61+
/>
62+
63+
{{! loader }}
64+
{{#unless this._isSetupComplete}}
65+
<div class="hds-code-editor__loader" aria-live="polite" role="status">
66+
<Hds::Icon @name="loading" @size="24" />
67+
<span class="sr-only">Loading</span>
68+
</div>
69+
{{/unless}}
70+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import Component from '@glimmer/component';
7+
import { tracked } from '@glimmer/tracking';
8+
import { action } from '@ember/object';
9+
import { modifier } from 'ember-modifier';
10+
11+
import type { ComponentLike } from '@glint/template';
12+
import type { HdsCodeEditorSignature as HdsCodeEditorModifierSignature } from 'src/modifiers/hds-code-editor';
13+
import type { HdsCodeEditorDescriptionSignature } from './description';
14+
import type { HdsCodeEditorTitleSignature } from './title';
15+
import type { HdsCodeEditorGenericSignature } from './generic';
16+
import type { EditorView } from '@codemirror/view';
17+
import { guidFor } from '@ember/object/internals';
18+
19+
export interface HdsCodeEditorSignature {
20+
Args: {
21+
ariaLabel?: string;
22+
ariaLabelledBy?: string;
23+
hasCopyButton?: boolean;
24+
hasFullScreenButton?: boolean;
25+
isStandalone?: boolean;
26+
language?: HdsCodeEditorModifierSignature['Args']['Named']['language'];
27+
value?: HdsCodeEditorModifierSignature['Args']['Named']['value'];
28+
onBlur?: HdsCodeEditorModifierSignature['Args']['Named']['onBlur'];
29+
onInput?: HdsCodeEditorModifierSignature['Args']['Named']['onInput'];
30+
onSetup?: HdsCodeEditorModifierSignature['Args']['Named']['onSetup'];
31+
};
32+
Blocks: {
33+
default: [
34+
{
35+
Title?: ComponentLike<HdsCodeEditorTitleSignature>;
36+
Description?: ComponentLike<HdsCodeEditorDescriptionSignature>;
37+
Generic?: ComponentLike<HdsCodeEditorGenericSignature>;
38+
},
39+
];
40+
};
41+
Element: HTMLDivElement;
42+
}
43+
44+
export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
45+
@tracked private _isFullScreen = false;
46+
@tracked private _isSetupComplete = false;
47+
@tracked private _value;
48+
@tracked private _titleId: string | undefined;
49+
50+
private _id = guidFor(this);
51+
52+
private _handleEscape = modifier(() => {
53+
const handleKeyDown = (event: KeyboardEvent) => {
54+
if (event.key !== 'Escape' || !this._isFullScreen) {
55+
return;
56+
}
57+
58+
this.toggleFullScreen();
59+
};
60+
61+
document.addEventListener('keydown', handleKeyDown);
62+
63+
return () => {
64+
document.removeEventListener('keydown', handleKeyDown);
65+
};
66+
});
67+
68+
constructor(owner: unknown, args: HdsCodeEditorSignature['Args']) {
69+
super(owner, args);
70+
71+
if (args.value) {
72+
this._value = args.value;
73+
}
74+
}
75+
76+
get ariaLabelledBy(): string | undefined {
77+
if (this.args.ariaLabel !== undefined) {
78+
return;
79+
}
80+
81+
return this.args.ariaLabelledBy ?? this._titleId;
82+
}
83+
84+
get hasActions(): boolean {
85+
return (this.args.hasCopyButton || this.args.hasFullScreenButton) ?? false;
86+
}
87+
88+
get isStandalone(): boolean {
89+
return this.args.isStandalone ?? true;
90+
}
91+
92+
get classNames(): string {
93+
// Currently there is only one theme so the class name is hard-coded.
94+
// In the future, additional themes such as a "light" theme could be added.
95+
const classes = ['hds-code-editor', 'hds-code-editor--theme-dark'];
96+
97+
if (this._isFullScreen) {
98+
classes.push('hds-code-editor--is-full-screen');
99+
}
100+
101+
if (this.isStandalone) {
102+
classes.push('hds-code-editor--is-standalone');
103+
}
104+
105+
return classes.join(' ');
106+
}
107+
108+
@action
109+
registerTitleElement(element: HdsCodeEditorTitleSignature['Element']): void {
110+
this._titleId = element.id;
111+
}
112+
113+
@action
114+
toggleFullScreen(): void {
115+
this._isFullScreen = !this._isFullScreen;
116+
}
117+
118+
@action
119+
onInput(newValue: string): void {
120+
this._value = newValue;
121+
this.args.onInput?.(newValue);
122+
}
123+
124+
@action
125+
onKeyDown(event: KeyboardEvent): void {
126+
if (event.key === 'Escape' && this._isFullScreen) {
127+
this.toggleFullScreen();
128+
}
129+
}
130+
131+
@action
132+
onSetup(editorView: EditorView): void {
133+
this._isSetupComplete = true;
134+
this.args.onSetup?.(editorView);
135+
}
136+
}

0 commit comments

Comments
 (0)