Skip to content
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

Add more props to the codemirror editor to ease migration from older version #178

Merged
merged 7 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
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 the CypherEditor component
Copy link
Collaborator

@ncordon ncordon Apr 30, 2024

Choose a reason for hiding this comment

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

Typo: to the CyherEditor component?

24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 99 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,32 @@ export class CypherEditor extends Component<
});
}

if (prevProps.lineNumbers !== this.props.lineNumbers) {
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 +406,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();
});
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 @@ -61,8 +61,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%',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This change is so that consumers can set a minimum height via the className property and have the background color extend to the full height

},
'.cm-content': {
caretColor: settings.cursor,
Expand Down
Loading