Skip to content

Commit

Permalink
Add more props to the codemirror editor to ease migration from older …
Browse files Browse the repository at this point in the history
…version (#178)

* support toggling line numbers

* ensure min-height works as intended

* add changeset

* fix e2e test

* fix test with overlapping panels

* fix changeset typo
  • Loading branch information
OskarDamkjaer committed May 2, 2024
1 parent df8f703 commit 6cc9022
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-beans-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@neo4j-cypher/react-codemirror': patch
---

Adds more props to the CypherEditor component
112 changes: 102 additions & 10 deletions packages/react-codemirror/src/CypherEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
KeyBinding,
keymap,
lineNumbers,
placeholder,
ViewUpdate,
} from '@codemirror/view';
import type { DbSchema } from '@neo4j-cypher/language-support';
Expand All @@ -23,6 +24,7 @@ import { cleanupWorkers } from './lang-cypher/syntaxValidation';
import { basicNeo4jSetup } from './neo4jSetup';
import { getThemeExtension } from './themes';

type DomEventHandlers = Parameters<typeof EditorView.domEventHandlers>[0];
export interface CypherEditorProps {
/**
* The prompt to show on single line editors
Expand All @@ -42,7 +44,7 @@ export interface CypherEditorProps {
*/
onExecute?: (cmd: string) => void;
/**
* The editor history navigateable via up/down arrow keys. Order newest to oldest.
* The editor history navigable via up/down arrow keys. Order newest to oldest.
* Add to this list with the `onExecute` callback for REPL style history.
*/
history?: string[];
Expand Down Expand Up @@ -102,6 +104,37 @@ export interface CypherEditorProps {
* @param {ViewUpdate} viewUpdate - the view update from codemirror
*/
onChange?(value: string, viewUpdate: ViewUpdate): void;

/**
* Map of event handlers to add to the editor.
*
* Note that the props are compared by reference, meaning object defined inline
* will cause the editor to re-render (much like the style prop does in this example:
* <div style={{}} />
*
* Memoize the object if you want/need to avoid this.
*
* @example
* // listen to blur events
* <CypherEditor domEventHandlers={{blur: () => console.log("blur event fired")}} />
*/
domEventHandlers?: DomEventHandlers;
/**
* Placeholder text to display when the editor is empty.
*/
placeholder?: string;
/**
* Whether the editor should show line numbers.
*
* @default true
*/
lineNumbers?: boolean;
/**
* Whether the editor is read-only.
*
* @default false
*/
readonly?: boolean;
}

const executeKeybinding = (onExecute?: (cmd: string) => void) =>
Expand All @@ -125,6 +158,20 @@ const executeKeybinding = (onExecute?: (cmd: string) => void) =>

const themeCompartment = new Compartment();
const keyBindingCompartment = new Compartment();
const lineNumbersCompartment = new Compartment();
const readOnlyCompartment = new Compartment();
const placeholderCompartment = new Compartment();
const domEventHandlerCompartment = new Compartment();

const formatLineNumber =
(prompt?: string) => (a: number, state: EditorState) => {
if (state.doc.lines === 1 && prompt !== undefined) {
return prompt;
}

return a.toString();
};

type CypherEditorState = { cypherSupportEnabled: boolean };

const ExternalEdit = Annotation.define<boolean>();
Expand Down Expand Up @@ -188,6 +235,7 @@ export class CypherEditor extends Component<
extraKeybindings: [],
history: [],
theme: 'light',
lineNumbers: true,
};

private debouncedOnChange = this.props.onChange
Expand Down Expand Up @@ -249,15 +297,20 @@ export class CypherEditor extends Component<
cypher(this.schemaRef.current),
lineWrap ? EditorView.lineWrapping : [],

lineNumbers({
formatNumber: (a, state) => {
if (state.doc.lines === 1 && this.props.prompt !== undefined) {
return this.props.prompt;
}

return a.toString();
},
}),
lineNumbersCompartment.of(
this.props.lineNumbers
? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
: [],
),
readOnlyCompartment.of(EditorState.readOnly.of(this.props.readonly)),
placeholderCompartment.of(
this.props.placeholder ? placeholder(this.props.placeholder) : [],
),
domEventHandlerCompartment.of(
this.props.domEventHandlers
? EditorView.domEventHandlers(this.props.domEventHandlers)
: [],
),
],
doc: this.props.value,
});
Expand Down Expand Up @@ -313,6 +366,35 @@ export class CypherEditor extends Component<
});
}

if (
prevProps.lineNumbers !== this.props.lineNumbers ||
prevProps.prompt !== this.props.prompt
) {
this.editorView.current.dispatch({
effects: lineNumbersCompartment.reconfigure(
this.props.lineNumbers
? lineNumbers({ formatNumber: formatLineNumber(this.props.prompt) })
: [],
),
});
}

if (prevProps.readonly !== this.props.readonly) {
this.editorView.current.dispatch({
effects: readOnlyCompartment.reconfigure(
EditorState.readOnly.of(this.props.readonly),
),
});
}

if (prevProps.placeholder !== this.props.placeholder) {
this.editorView.current.dispatch({
effects: placeholderCompartment.reconfigure(
this.props.placeholder ? placeholder(this.props.placeholder) : [],
),
});
}

if (
prevProps.extraKeybindings !== this.props.extraKeybindings ||
prevProps.onExecute !== this.props.onExecute
Expand All @@ -327,6 +409,16 @@ export class CypherEditor extends Component<
});
}

if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
this.editorView.current.dispatch({
effects: domEventHandlerCompartment.reconfigure(
this.props.domEventHandlers
? EditorView.domEventHandlers(this.props.domEventHandlers)
: [],
),
});
}

// This component rerenders on every keystroke and comparing the
// full lists of editor strings on every render could be expensive.
const didChangeHistoryEstimate =
Expand Down
97 changes: 97 additions & 0 deletions packages/react-codemirror/src/e2e_tests/configuration.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { expect, test } from '@playwright/experimental-ct-react';
import { CypherEditor } from '../CypherEditor';

test.use({ viewport: { width: 500, height: 500 } });

test('prompt shows up', async ({ mount, page }) => {
const component = await mount(<CypherEditor prompt="neo4j>" />);

await expect(component).toContainText('neo4j>');

await component.update(<CypherEditor prompt="test>" />);
await expect(component).toContainText('test>');

const textField = page.getByRole('textbox');
await textField.press('a');

await expect(textField).toHaveText('a');
});

test('line numbers can be turned on/off', async ({ mount }) => {
const component = await mount(<CypherEditor lineNumbers />);

await expect(component).toContainText('1');

await component.update(<CypherEditor lineNumbers={false} />);
await expect(component).not.toContainText('1');
});

test('can configure readonly', async ({ mount, page }) => {
const component = await mount(<CypherEditor readonly />);

const textField = page.getByRole('textbox');
await textField.press('a');
await expect(textField).not.toHaveText('a');

await component.update(<CypherEditor readonly={false} />);
await textField.press('b');
await expect(textField).toHaveText('b');
});

test('can set placeholder ', async ({ mount, page }) => {
const component = await mount(<CypherEditor placeholder="bulbasaur" />);

const textField = page.getByRole('textbox');
await expect(textField).toHaveText('bulbasaur');

await component.update(<CypherEditor placeholder="venusaur" />);
await expect(textField).not.toHaveText('bulbasaur');
await expect(textField).toHaveText('venusaur');

await textField.fill('abc');
await expect(textField).not.toHaveText('venusaur');
await expect(textField).toHaveText('abc');
});

test('can set/unset onFocus/onBlur', async ({ mount, page }) => {
const component = await mount(<CypherEditor />);

let focusFireCount = 0;
let blurFireCount = 0;

const focus = () => {
focusFireCount += 1;
};
const blur = () => {
blurFireCount += 1;
};

await component.update(<CypherEditor domEventHandlers={{ blur, focus }} />);

const textField = page.getByRole('textbox');
await textField.click();
await expect(textField).toBeFocused();

// this is to give the events time to fire
await expect(() => {
expect(focusFireCount).toBe(1);
expect(blurFireCount).toBe(0);
}).toPass();

await textField.blur();

await expect(() => {
expect(focusFireCount).toBe(1);
expect(blurFireCount).toBe(1);
}).toPass();

await component.update(<CypherEditor />);
await textField.click();
await expect(textField).toBeFocused();
await textField.blur();

await expect(() => {
expect(focusFireCount).toBe(1);
expect(blurFireCount).toBe(1);
}).toPass();
});
5 changes: 5 additions & 0 deletions packages/react-codemirror/src/e2e_tests/e2eUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,10 @@ export class CypherEditorPage {
await this.page.getByText(queryChunk, { exact: true }).hover();
await expect(this.page.locator('.cm-tooltip-hover').last()).toBeVisible();
await expect(this.page.getByText(expectedMsg)).toBeVisible();
// make the sure the tooltip closes
await this.page.mouse.move(0, 0);
await expect(
this.page.locator('.cm-tooltip-hover').last(),
).not.toBeVisible();
}
}
14 changes: 0 additions & 14 deletions packages/react-codemirror/src/e2e_tests/sanityChecks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,3 @@ test('can complete CALL/CREATE', async ({ page, mount }) => {

await expect(textField).toHaveText('CALL');
});

test('prompt shows up', async ({ mount, page }) => {
const component = await mount(<CypherEditor prompt="neo4j>" />);

await expect(component).toContainText('neo4j>');

await component.update(<CypherEditor prompt="test>" />);
await expect(component).toContainText('test>');

const textField = page.getByRole('textbox');
await textField.press('a');

await expect(textField).toHaveText('a');
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ export const createCypherTheme = ({
color: settings.gutterForeground,
border: 'none',
},
'&.cm-editor .cm-scroller': {
'&.cm-editor': {
fontFamily: 'Fira Code, Menlo, Monaco, Lucida Console, monospace',
height: '100%',
},
'.cm-content': {
caretColor: settings.cursor,
Expand Down

0 comments on commit 6cc9022

Please sign in to comment.