@patternfly/react-code-editor 6.0.0-alpha.5 → 6.0.0-alpha.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CHANGELOG.md +821 -17
  2. package/README.md +31 -31
  3. package/dist/esm/components/CodeEditor/CodeEditor.d.ts +8 -2
  4. package/dist/esm/components/CodeEditor/CodeEditor.d.ts.map +1 -1
  5. package/dist/esm/components/CodeEditor/CodeEditor.js +23 -17
  6. package/dist/esm/components/CodeEditor/CodeEditor.js.map +1 -1
  7. package/dist/esm/components/CodeEditor/CodeEditorControl.js +1 -1
  8. package/dist/esm/components/CodeEditor/CodeEditorControl.js.map +1 -1
  9. package/dist/js/components/CodeEditor/CodeEditor.d.ts +8 -2
  10. package/dist/js/components/CodeEditor/CodeEditor.d.ts.map +1 -1
  11. package/dist/js/components/CodeEditor/CodeEditor.js +22 -16
  12. package/dist/js/components/CodeEditor/CodeEditor.js.map +1 -1
  13. package/dist/js/components/CodeEditor/CodeEditorControl.js +1 -1
  14. package/dist/js/components/CodeEditor/CodeEditorControl.js.map +1 -1
  15. package/package.json +8 -7
  16. package/src/components/CodeEditor/CodeEditor.tsx +77 -65
  17. package/src/components/CodeEditor/CodeEditorControl.tsx +1 -1
  18. package/src/components/CodeEditor/__test__/CodeEditor.test.tsx +69 -39
  19. package/src/components/CodeEditor/__test__/CodeEditorControl.test.tsx +18 -0
  20. package/src/components/CodeEditor/__test__/__snapshots__/CodeEditor.test.tsx.snap +104 -536
  21. package/src/components/CodeEditor/__test__/__snapshots__/CodeEditorControl.test.tsx.snap +22 -0
  22. package/src/components/CodeEditor/examples/CodeEditor.md +5 -0
  23. package/src/components/CodeEditor/examples/CodeEditorCustomControl.tsx +2 -1
  24. package/src/components/CodeEditor/examples/CodeEditorShortcutMainHeader.tsx +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-code-editor",
3
- "version": "6.0.0-alpha.5",
3
+ "version": "6.0.0-alpha.51",
4
4
  "description": "This package provides a PatternFly wrapper for the Monaco code editor\n",
5
5
  "main": "dist/js/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -30,20 +30,21 @@
30
30
  "clean": "rimraf dist"
31
31
  },
32
32
  "dependencies": {
33
- "@patternfly/react-core": "^6.0.0-alpha.5",
34
- "@patternfly/react-icons": "^6.0.0-alpha.4",
35
- "@patternfly/react-styles": "^6.0.0-alpha.4",
33
+ "@monaco-editor/react": "^4.6.0",
34
+ "@patternfly/react-core": "^6.0.0-alpha.51",
35
+ "@patternfly/react-icons": "^6.0.0-alpha.20",
36
+ "@patternfly/react-styles": "^6.0.0-alpha.20",
36
37
  "react-dropzone": "14.2.3",
37
38
  "tslib": "^2.5.0"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "react": "^17 || ^18",
41
- "react-dom": "^17 || ^18",
42
- "react-monaco-editor": "^0.51.0"
42
+ "react-dom": "^17 || ^18"
43
43
  },
44
44
  "devDependencies": {
45
+ "monaco-editor": "^0.47.0",
45
46
  "rimraf": "^2.6.2",
46
47
  "typescript": "^4.7.4"
47
48
  },
48
- "gitHead": "0213323de9e62364f8cd3f401bec86f1f34ee4fe"
49
+ "gitHead": "8d93660fcd63d544d8ef0f2ee6494cfe5f09379e"
49
50
  }
@@ -7,18 +7,16 @@ import {
7
7
  ButtonVariant,
8
8
  EmptyState,
9
9
  EmptyStateBody,
10
- EmptyStateIcon,
11
10
  EmptyStateActions,
12
11
  EmptyStateVariant,
13
12
  EmptyStateFooter,
14
13
  getResizeObserver,
15
14
  Popover,
16
15
  PopoverProps,
17
- TooltipPosition,
18
- EmptyStateHeader
16
+ TooltipPosition
19
17
  } from '@patternfly/react-core';
20
- import MonacoEditor, { ChangeHandler, EditorDidMount } from 'react-monaco-editor';
21
- import { editor } from 'monaco-editor/esm/vs/editor/editor.api';
18
+ import Editor, { EditorProps, Monaco } from '@monaco-editor/react';
19
+ import type { editor } from 'monaco-editor';
22
20
  import CopyIcon from '@patternfly/react-icons/dist/esm/icons/copy-icon';
23
21
  import UploadIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon';
24
22
  import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon';
@@ -28,6 +26,9 @@ import Dropzone, { FileRejection } from 'react-dropzone';
28
26
  import { CodeEditorContext } from './CodeEditorUtils';
29
27
  import { CodeEditorControl } from './CodeEditorControl';
30
28
 
29
+ export type ChangeHandler = (value: string, event: editor.IModelContentChangedEvent) => void;
30
+ export type EditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => void;
31
+
31
32
  export interface Shortcut {
32
33
  description: string;
33
34
  keys: string[];
@@ -134,6 +135,8 @@ export interface CodeEditorProps extends Omit<React.HTMLProps<HTMLDivElement>, '
134
135
  downloadButtonToolTipText?: string;
135
136
  /** Name of the file if user downloads code to local file. */
136
137
  downloadFileName?: string;
138
+ /** Additional props to pass to the monaco editor. */
139
+ editorProps?: EditorProps;
137
140
  /** Content to display in space of the code editor when there is no code to display. */
138
141
  emptyState?: React.ReactNode;
139
142
  /** Override default empty state body text. */
@@ -154,6 +157,8 @@ export interface CodeEditorProps extends Omit<React.HTMLProps<HTMLDivElement>, '
154
157
  isCopyEnabled?: boolean;
155
158
  /** Flag indicating the editor is styled using monaco's dark theme. */
156
159
  isDarkTheme?: boolean;
160
+ /** Flag indicating the editor has a plain header. */
161
+ isHeaderPlain?: boolean;
157
162
  /** Flag to add download button to code editor actions. */
158
163
  isDownloadEnabled?: boolean;
159
164
  /** Flag to include a label indicating the currently configured editor language. */
@@ -260,6 +265,7 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
260
265
  isUploadEnabled: false,
261
266
  isDownloadEnabled: false,
262
267
  isCopyEnabled: false,
268
+ isHeaderPlain: false,
263
269
  copyButtonAriaLabel: 'Copy code to clipboard',
264
270
  uploadButtonAriaLabel: 'Upload code',
265
271
  downloadButtonAriaLabel: 'Download code',
@@ -491,12 +497,14 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
491
497
  emptyStateLink,
492
498
  customControls,
493
499
  isMinimapVisible,
500
+ isHeaderPlain,
494
501
  headerMainContent,
495
502
  shortcutsPopoverButtonText,
496
503
  shortcutsPopoverProps: shortcutsPopoverPropsProp,
497
504
  showEditor,
498
505
  options: optionsProp,
499
- overrideServices
506
+ overrideServices,
507
+ editorProps
500
508
  } = this.props;
501
509
  const shortcutsPopoverProps: PopoverProps = {
502
510
  ...CodeEditor.defaultProps.shortcutsPopoverProps,
@@ -520,12 +528,7 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
520
528
  const emptyState =
521
529
  providedEmptyState ||
522
530
  (isUploadEnabled ? (
523
- <EmptyState variant={EmptyStateVariant.sm}>
524
- <EmptyStateHeader
525
- titleText={emptyStateTitle}
526
- icon={<EmptyStateIcon icon={CodeIcon} />}
527
- headingLevel="h4"
528
- />
531
+ <EmptyState variant={EmptyStateVariant.sm} titleText={emptyStateTitle} icon={CodeIcon} headingLevel="h4">
529
532
  <EmptyStateBody>{emptyStateBody}</EmptyStateBody>
530
533
  {!isReadOnly && (
531
534
  <EmptyStateFooter>
@@ -543,12 +546,7 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
543
546
  )}
544
547
  </EmptyState>
545
548
  ) : (
546
- <EmptyState variant={EmptyStateVariant.sm}>
547
- <EmptyStateHeader
548
- titleText={emptyStateTitle}
549
- icon={<EmptyStateIcon icon={CodeIcon} />}
550
- headingLevel="h4"
551
- />
549
+ <EmptyState variant={EmptyStateVariant.sm} titleText={emptyStateTitle} icon={CodeIcon} headingLevel="h4">
552
550
  {!isReadOnly && (
553
551
  <EmptyStateFooter>
554
552
  <EmptyStateActions>
@@ -569,45 +567,50 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
569
567
  trigger: 'mouseenter focus'
570
568
  };
571
569
 
572
- const editorHeader = (
573
- <div className={css(styles.codeEditorHeader)}>
574
- {
575
- <div className={css(styles.codeEditorControls)}>
576
- <CodeEditorContext.Provider value={{ code: value }}>
577
- {isCopyEnabled && (!showEmptyState || !!value) && (
578
- <CodeEditorControl
579
- icon={<CopyIcon />}
580
- aria-label={copyButtonAriaLabel}
581
- tooltipProps={{
582
- ...tooltipProps,
583
- 'aria-live': 'polite',
584
- content: <div>{copied ? copyButtonSuccessTooltipText : copyButtonToolTipText}</div>,
585
- exitDelay: copied ? toolTipCopyExitDelay : toolTipDelay,
586
- onTooltipHidden: () => this.setState({ copied: false })
587
- }}
588
- onClick={this.copyCode}
589
- />
590
- )}
591
- {isUploadEnabled && (
592
- <CodeEditorControl
593
- icon={<UploadIcon />}
594
- aria-label={uploadButtonAriaLabel}
595
- tooltipProps={{ content: <div>{uploadButtonToolTipText}</div>, ...tooltipProps }}
596
- onClick={open}
597
- />
598
- )}
599
- {isDownloadEnabled && (!showEmptyState || !!value) && (
600
- <CodeEditorControl
601
- icon={<DownloadIcon />}
602
- aria-label={downloadButtonAriaLabel}
603
- tooltipProps={{ content: <div>{downloadButtonToolTipText}</div>, ...tooltipProps }}
604
- onClick={this.download}
605
- />
606
- )}
607
- {customControls && customControls}
608
- </CodeEditorContext.Provider>
609
- </div>
610
- }
570
+ const hasEditorHeaderContent =
571
+ ((isCopyEnabled || isDownloadEnabled) && (!showEmptyState || !!value)) ||
572
+ isUploadEnabled ||
573
+ customControls ||
574
+ headerMainContent ||
575
+ !!shortcutsPopoverProps.bodyContent;
576
+
577
+ const editorHeaderContent = (
578
+ <React.Fragment>
579
+ <div className={css(styles.codeEditorControls)}>
580
+ <CodeEditorContext.Provider value={{ code: value }}>
581
+ {isCopyEnabled && (!showEmptyState || !!value) && (
582
+ <CodeEditorControl
583
+ icon={<CopyIcon />}
584
+ aria-label={copyButtonAriaLabel}
585
+ tooltipProps={{
586
+ ...tooltipProps,
587
+ 'aria-live': 'polite',
588
+ content: <div>{copied ? copyButtonSuccessTooltipText : copyButtonToolTipText}</div>,
589
+ exitDelay: copied ? toolTipCopyExitDelay : toolTipDelay,
590
+ onTooltipHidden: () => this.setState({ copied: false })
591
+ }}
592
+ onClick={this.copyCode}
593
+ />
594
+ )}
595
+ {isUploadEnabled && (
596
+ <CodeEditorControl
597
+ icon={<UploadIcon />}
598
+ aria-label={uploadButtonAriaLabel}
599
+ tooltipProps={{ content: <div>{uploadButtonToolTipText}</div>, ...tooltipProps }}
600
+ onClick={open}
601
+ />
602
+ )}
603
+ {isDownloadEnabled && (!showEmptyState || !!value) && (
604
+ <CodeEditorControl
605
+ icon={<DownloadIcon />}
606
+ aria-label={downloadButtonAriaLabel}
607
+ tooltipProps={{ content: <div>{downloadButtonToolTipText}</div>, ...tooltipProps }}
608
+ onClick={this.download}
609
+ />
610
+ )}
611
+ {customControls && customControls}
612
+ </CodeEditorContext.Provider>
613
+ </div>
611
614
  {<div className={css(styles.codeEditorHeaderMain)}>{headerMainContent}</div>}
612
615
  {!!shortcutsPopoverProps.bodyContent && (
613
616
  <div className={`${styles.codeEditor}__keyboard-shortcuts`}>
@@ -618,6 +621,14 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
618
621
  </Popover>
619
622
  </div>
620
623
  )}
624
+ </React.Fragment>
625
+ );
626
+
627
+ const editorHeader = (
628
+ <div className={css(styles.codeEditorHeader, isHeaderPlain && styles.modifiers.plain)}>
629
+ {hasEditorHeaderContent && (
630
+ <div className={css(styles.codeEditorHeaderContent)}>{editorHeaderContent}</div>
631
+ )}
621
632
  {isLanguageLabelVisible && (
622
633
  <div className={css(styles.codeEditorTab)}>
623
634
  <span className={css(styles.codeEditorTabIcon)}>
@@ -631,7 +642,7 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
631
642
 
632
643
  const editor = (
633
644
  <div className={css(styles.codeEditorCode)} ref={this.wrapperRef} tabIndex={0} dir="ltr">
634
- <MonacoEditor
645
+ <Editor
635
646
  height={height}
636
647
  width={width}
637
648
  language={language}
@@ -639,8 +650,9 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
639
650
  options={options}
640
651
  overrideServices={overrideServices}
641
652
  onChange={this.onChange}
642
- editorDidMount={this.editorDidMount}
653
+ onMount={this.editorDidMount}
643
654
  theme={isDarkTheme ? 'vs-dark' : 'vs-light'}
655
+ {...editorProps}
644
656
  />
645
657
  </div>
646
658
  );
@@ -652,14 +664,14 @@ class CodeEditor extends React.Component<CodeEditorProps, CodeEditorState> {
652
664
  {...getRootProps({
653
665
  onClick: (event) => event.stopPropagation() // Prevents clicking TextArea from opening file dialog
654
666
  })}
655
- className={`${fileUploadStyles.fileUpload} ${isDragActive && fileUploadStyles.modifiers.dragHover} ${
656
- isLoading && fileUploadStyles.modifiers.loading
657
- }`}
667
+ className={css(isLoading && fileUploadStyles.modifiers.loading)}
658
668
  >
659
669
  {editorHeader}
660
- <div className={css(styles.codeEditorMain)}>
661
- <input {...getInputProps()} /* hidden, necessary for react-dropzone */ />
662
- {(showEmptyState || providedEmptyState) && !value ? emptyState : editor}
670
+ <div className={css(styles.codeEditorMain, isDragActive && styles.modifiers.dragHover)}>
671
+ <div className={css(styles.codeEditorUpload)}>
672
+ <input {...getInputProps()} /* hidden, necessary for react-dropzone */ />
673
+ {(showEmptyState || providedEmptyState) && !value ? emptyState : editor}
674
+ </div>
663
675
  </div>
664
676
  </div>
665
677
  ) : (
@@ -38,7 +38,7 @@ export const CodeEditorControl: React.FunctionComponent<CodeEditorControlProps>
38
38
 
39
39
  return isVisible ? (
40
40
  <Tooltip {...tooltipProps}>
41
- <Button className={className} onClick={onCustomClick} variant="control" aria-label={ariaLabel} {...props}>
41
+ <Button className={className} onClick={onCustomClick} variant="plain" aria-label={ariaLabel} {...props}>
42
42
  {icon}
43
43
  </Button>
44
44
  </Tooltip>
@@ -1,46 +1,76 @@
1
1
  import React from 'react';
2
- import { render } from '@testing-library/react';
2
+ import { render, screen, act } from '@testing-library/react';
3
3
  import { CodeEditor, Language } from '../CodeEditor';
4
+ import styles from '@patternfly/react-styles/css/components/CodeEditor/code-editor';
4
5
 
5
- Object.defineProperty(window, 'matchMedia', {
6
- writable: true,
7
- value: jest.fn().mockImplementation((query) => ({
8
- matches: false,
9
- media: query,
10
- onchange: null,
11
- addListener: jest.fn(), // Deprecated
12
- removeListener: jest.fn(), // Deprecated
13
- addEventListener: jest.fn(),
14
- removeEventListener: jest.fn(),
15
- dispatchEvent: jest.fn()
16
- }))
17
- });
18
-
19
- describe('CodeEditor', () => {
20
- beforeAll(() => {
21
- window.HTMLCanvasElement.prototype.getContext = () => ({}) as any;
22
- });
6
+ jest.mock('@monaco-editor/react', () => jest.fn(() => <div data-testid="mock-editor"></div>));
23
7
 
24
- test('matches snapshot without props', () => {
25
- const { asFragment } = render(<CodeEditor />);
26
- expect(asFragment()).toMatchSnapshot();
27
- });
8
+ test('Matches snapshot without props', () => {
9
+ const { asFragment } = render(<CodeEditor code="test" />);
10
+ expect(asFragment()).toMatchSnapshot();
11
+ });
12
+
13
+ test('Matches snapshot with control buttons enabled', () => {
14
+ const { asFragment } = render(<CodeEditor isUploadEnabled isDownloadEnabled isCopyEnabled code="test" />);
15
+ expect(asFragment()).toMatchSnapshot();
16
+ });
17
+
18
+ test(`Renders with default classes ${styles.codeEditor}, ${styles.codeEditorMain}, ${styles.codeEditorCode}`, () => {
19
+ render(<CodeEditor />);
20
+ expect(screen.getByTestId('mock-editor').parentElement).toHaveClass(styles.codeEditorCode);
21
+ expect(screen.getByTestId('mock-editor').parentElement?.parentElement).toHaveClass(styles.codeEditorMain);
22
+ expect(screen.getByTestId('mock-editor').parentElement?.parentElement?.parentElement).toHaveClass(styles.codeEditor);
23
+ });
24
+
25
+ test('Renders custom class when className is passed', () => {
26
+ render(<CodeEditor className="custom" />);
27
+ expect(screen.getByTestId('mock-editor').parentElement?.parentElement?.parentElement).toHaveClass('custom');
28
+ });
29
+
30
+ test(`Renders with ${styles.modifiers.readOnly} when isReadOnly = true`, () => {
31
+ render(<CodeEditor isReadOnly />);
32
+ expect(screen.getByTestId('mock-editor').parentElement?.parentElement?.parentElement).toHaveClass(
33
+ styles.modifiers.readOnly
34
+ );
35
+ });
36
+
37
+ test(`Renders with ${styles.codeEditorUpload} when isUploadEnabled = true`, () => {
38
+ render(<CodeEditor isUploadEnabled code="test" />);
39
+ expect(screen.getByTestId('mock-editor').parentElement?.parentElement).toHaveClass(styles.codeEditorUpload);
40
+ });
41
+
42
+ test(`Renders with empty state when code = undefined`, () => {
43
+ render(<CodeEditor emptyState={<div>empty</div>} />);
44
+ expect(screen.getByText('empty')).toBeInTheDocument();
45
+ });
46
+
47
+ test(`Renders with empty state when isUploadEnabled = true and code = undefined`, () => {
48
+ render(<CodeEditor emptyState={<div>empty</div>} isUploadEnabled />);
49
+ expect(screen.getByText('empty')).toBeInTheDocument();
50
+ });
51
+
52
+ test(`Renders with language label when isLanguageLabelVisible`, () => {
53
+ render(<CodeEditor isLanguageLabelVisible language={Language.java} />);
54
+ expect(screen.getByText('JAVA')).toBeInTheDocument();
55
+ });
56
+
57
+ test(`Renders with custom controls when customControls is passed`, () => {
58
+ render(<CodeEditor customControls={<div>control</div>} />);
59
+ expect(screen.getByText('control')).toBeInTheDocument();
60
+ });
61
+
62
+ test(`Renders with custom header content when headerMainContent is passed`, () => {
63
+ render(<CodeEditor headerMainContent="header content" />);
64
+ expect(screen.getByText('header content')).toBeInTheDocument();
65
+ });
28
66
 
29
- test('matches snapshot with all props', () => {
30
- const { asFragment } = render(
31
- <CodeEditor
32
- isReadOnly
33
- isDarkTheme
34
- isLineNumbersVisible={false}
35
- isUploadEnabled
36
- isLanguageLabelVisible
37
- isDownloadEnabled
38
- isCopyEnabled
39
- height="400px"
40
- code={'test'}
41
- language={Language.javascript}
42
- />
43
- );
44
- expect(asFragment()).toMatchSnapshot();
67
+ test(`Renders with shortcuts when shortcutsPopoverButtonText is passed`, () => {
68
+ render(
69
+ <CodeEditor shortcutsPopoverButtonText="shortcuts-button" shortcutsPopoverProps={{ bodyContent: 'shortcuts' }} />
70
+ );
71
+ expect(screen.getByText('shortcuts-button')).toBeInTheDocument();
72
+ act(() => {
73
+ screen.getByText('shortcuts-button').click();
45
74
  });
75
+ expect(screen.getByText('shortcuts')).toBeInTheDocument();
46
76
  });
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { CodeEditorControl } from '../CodeEditorControl';
4
+
5
+ test('Matches snapshot', () => {
6
+ const { asFragment } = render(<CodeEditorControl icon={<div>icon</div>} onClick={jest.fn()} />);
7
+ expect(asFragment()).toMatchSnapshot();
8
+ });
9
+
10
+ test('Renders with custom class when className is passed', () => {
11
+ render(<CodeEditorControl className="custom" icon={<div>icon</div>} onClick={jest.fn()} />);
12
+ expect(screen.getByText('icon').parentElement).toHaveClass('custom');
13
+ });
14
+
15
+ test('Renders with accessible name when aria-label is passed', () => {
16
+ render(<CodeEditorControl aria-label="aria-test" icon={<div>icon</div>} onClick={jest.fn()} />);
17
+ expect(screen.getByLabelText('aria-test'));
18
+ });