Skip to content

Commit

Permalink
fix: positioner hooks api
Browse files Browse the repository at this point in the history
  • Loading branch information
ifiokjr committed Jan 25, 2021
1 parent d6049a9 commit 8f9f9fc
Show file tree
Hide file tree
Showing 23 changed files with 279 additions and 237 deletions.
6 changes: 1 addition & 5 deletions examples/storybook-react/src/component.stories.tsx
Expand Up @@ -255,11 +255,7 @@ export const FloatingBlockNodeEditor = () => {

export const ActionsEditor = () => {
const { manager, state } = useRemirror({
extensions: () => [
...corePreset(),
...wysiwygPreset({ autoUpdate: false }),
new ColumnsExtension(),
],
extensions: () => [...corePreset(), ...wysiwygPreset(), new ColumnsExtension()],
selection: 'end',
stringHandler: 'html',
});
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -135,7 +135,7 @@
"@size-limit/preset-big-lib": "^4.9.1",
"@testing-library/dom": "^7.29.4",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react-hooks": "^5.0.2",
"@testing-library/react-hooks": "^5.0.3",
"@testing-library/user-event": "^12.6.2",
"@types/eslint": "^7.2.6",
"@types/jest": "^26.0.20",
Expand Down Expand Up @@ -204,7 +204,7 @@
"snapshot-diff": "^0.8.1",
"testing": "^0.0.4",
"typescript": "^4.1.3",
"typescript-plugin-css-modules": "^3.0.1",
"typescript-plugin-css-modules": "^3.1.0",
"typescript-snapshots-plugin": "^1.7.0",
"typescript-styled-plugin": "^0.15.0"
},
Expand Down
Expand Up @@ -22,7 +22,7 @@ test('creates a provider and context hook', () => {
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: props.defaultCount }),
message: () => `${get('count')} count`,
message: () => `${get((p) => p.count)} count`,
}),
);

Expand Down
2 changes: 1 addition & 1 deletion packages/create-context-state/package.json
Expand Up @@ -58,6 +58,6 @@
"access": "public"
},
"@remirror": {
"sizeLimit": "2 KB"
"sizeLimit": "1 KB"
}
}
68 changes: 26 additions & 42 deletions packages/create-context-state/src/create-context-hook.tsx
@@ -1,7 +1,13 @@
import getPath from 'dash-get';
import pick from 'object.pick';
import { ComponentType, Context as ReactContext, createContext, FC, useContext } from 'react';
import usePrevious from 'use-previous';
import {
ComponentType,
Context as ReactContext,
createContext,
FC,
useContext,
useEffect,
useLayoutEffect,
useRef,
} from 'react';

/**
* Create a `Provider` and `useContext` retriever with a custom hook.
Expand Down Expand Up @@ -98,7 +104,7 @@ type UseHook<Context extends object, Props extends object = object> = (props: Pr
export function contextHookFactory<Context extends object>(
DefaultContext: ReactContext<Context | null>,
): ContextHook<Context> {
return (pathOrSelector?: unknown, equalityCheck?: EqualityChecker<Context>) => {
return (selector?: unknown, equalityCheck?: EqualityChecker<Context>) => {
const context = useContext(DefaultContext);
const previousContext = usePrevious(context);

Expand All @@ -108,31 +114,23 @@ export function contextHookFactory<Context extends object>(
);
}

if (!pathOrSelector) {
if (!selector) {
return context;
}

if (typeof pathOrSelector === 'string') {
return getPath(context, pathOrSelector);
}

if (Array.isArray(pathOrSelector)) {
return pick(context, pathOrSelector);
}

if (typeof pathOrSelector !== 'function') {
if (typeof selector !== 'function') {
throw new TypeError(
'invalid arguments passed to `useContextHook`. This hook must be called with zero arguments, a getter function or a path string.',
);
}

const value = pathOrSelector(context);
const value = selector(context);

if (!previousContext || !equalityCheck) {
return value;
}

const previousValue = pathOrSelector(previousContext);
const previousValue = selector(previousContext);

return equalityCheck(previousValue, value) ? previousValue : value;
};
Expand All @@ -148,37 +146,12 @@ export type EqualityChecker<SelectedValue> = (

export interface ContextHook<Context extends object> {
(): Context;
<Key extends keyof Context>(pickedKeys: Key[]): Pick<Context, Key>;
<SelectedValue>(
selector: ContextSelector<Context, SelectedValue>,
equalityFn?: EqualityChecker<SelectedValue>,
): SelectedValue;
<Path extends GetPath<Context>>(path: Path): PathValue<Context, Path>;
}

export type GetRecursivePath<Type, Key extends keyof Type> = Key extends string
? Type[Key] extends Record<string, unknown>
?
| `${Key}.${GetRecursivePath<Type[Key], Exclude<keyof Type[Key], keyof any[]>> & string}`
| `${Key}.${Exclude<keyof Type[Key], keyof any[]> & string}`
: never
: never;
export type GetJoinedPath<Type> = GetRecursivePath<Type, keyof Type> | keyof Type;

export type GetPath<Type> = GetJoinedPath<Type> extends string | keyof Type
? GetJoinedPath<Type>
: keyof Type;

export type PathValue<Type, Path extends GetPath<Type>> = Path extends `${infer Key}.${infer Rest}`
? Key extends keyof Type
? Rest extends GetPath<Type[Key]>
? PathValue<Type[Key], Rest>
: never
: never
: Path extends keyof Type
? Type[Path]
: never;

/**
* Split but don't allow empty string.
*/
Expand All @@ -195,3 +168,14 @@ export type SplitEmpty<
Input extends string,
Splitter extends string
> = Input extends `${infer T}${Splitter}${infer Rest}` ? [T, ...Split<Rest, Splitter>] : [Input];

function usePrevious<T>(value: T) {
const ref = useRef<T>();
useIsomorphicLayoutEffect(() => {
ref.current = value;
});
return ref.current;
}

export const useIsomorphicLayoutEffect =
typeof document !== 'undefined' ? useLayoutEffect : useEffect;
38 changes: 1 addition & 37 deletions packages/create-context-state/src/create-context-state.tsx
@@ -1,14 +1,7 @@
/* eslint-disable @typescript-eslint/ban-types */
import getPath from 'dash-get';
import { Dispatch, MutableRefObject, useEffect, useRef, useState } from 'react';

import {
ContextSelector,
createContextHook,
CreateContextReturn,
GetPath,
PathValue,
} from './create-context-hook';
import { ContextSelector, createContextHook, CreateContextReturn } from './create-context-hook';

/**
* Create a context and provider with built in setters and getters.
Expand Down Expand Up @@ -129,30 +122,6 @@ export function createContextState<

return context;
});

// // Create the initial react context. const DefaultContext =
// createContext<Context | null>(null);

// // Create the hook for retrieving the created context state. const
// useContextHook = contextHookFactory(DefaultContext);

// const Provider = (props: PropsWithChildren<Props>) => {// Keep a ref to the
// context so that the `get` function can always be called // with the
// latest value. const contextRef = useRef<Context | null>(null); const
// setContextRef = useRef<Dispatch<React.SetStateAction<Context>>>();

// const [context, setContext] = useState(() => {return creator({ get:
// createGet(contextRef), set: createSet(setContextRef) });
// });

// // Keep the refs updated on each render. contextRef.current = context;
// setContextRef.current = setContext;

// return <DefaultContext.Provider
// value={context}>{props.children}</DefaultContext.Provider>;
// };

// return [Provider, useContextHook, DefaultContext];
}

/**
Expand All @@ -173,10 +142,6 @@ function createGet<Context extends object>(
return ref.current;
}

if (typeof pathOrSelector === 'string') {
return getPath(ref.current, pathOrSelector);
}

if (typeof pathOrSelector !== 'function') {
throw new TypeError(
'Invalid arguments passed to `useContextHook`. The hook must be called with zero arguments, a getter function or a path string.',
Expand Down Expand Up @@ -210,7 +175,6 @@ function createSet<Context extends object>(
export interface GetContext<Context extends object> {
(): Context;
<SelectedValue>(selector: ContextSelector<Context, SelectedValue>): SelectedValue;
<Path extends GetPath<Context>>(path: Path): PathValue<Context, Path>;
}
export type PartialContext<Context extends object> =
| Partial<Context>
Expand Down
2 changes: 1 addition & 1 deletion packages/remirror__cli/package.json
Expand Up @@ -61,7 +61,7 @@
"yup": "^0.32.8"
},
"devDependencies": {
"@testing-library/react-hooks": "^5.0.2",
"@testing-library/react-hooks": "^5.0.3",
"@types/ink-spinner": "^3.0.0",
"@types/ink-testing-library": "^1.0.1",
"@types/jsesc": "^2.5.1",
Expand Down
29 changes: 29 additions & 0 deletions packages/remirror__core-utils/src/core-utils.ts
Expand Up @@ -1520,6 +1520,35 @@ function checkForInvalidContent(props: CheckForInvalidContentProps): InvalidCont
return invalidNodes;
}

/**
* Checks that the selection is an empty text selection at the end of its parent
* node.
*/
export function isEndOfTextBlock(selection: Selection): selection is TextSelection {
return !!(
isTextSelection(selection) &&
selection.$cursor &&
selection.$cursor.parentOffset >= selection.$cursor.parent.content.size
);
}

/**
* Checks that the selection is an empty text selection at the start of its
* parent node.
*/
export function isStartOfTextBlock(selection: Selection): selection is TextSelection {
return !!(isTextSelection(selection) && selection.$cursor && selection.$cursor.parentOffset <= 0);
}

/**
* Returns true when the selection is a text selection at the start of the
* document.
*/
export function isStartOfDoc(selection: Selection): boolean {
const selectionAtStart = PMSelection.atStart(selection.$anchor.doc);
return !!(isStartOfTextBlock(selection) && selectionAtStart.anchor === selection.anchor);
}

declare global {
namespace Remirror {
/**
Expand Down
3 changes: 3 additions & 0 deletions packages/remirror__core-utils/src/index.ts
Expand Up @@ -61,6 +61,7 @@ export {
isEditorSchema,
isEditorState,
isEmptyBlockNode,
isEndOfTextBlock,
isIdentifierOfType,
isMarkActive,
isMarkType,
Expand All @@ -72,6 +73,8 @@ export {
isRemirrorType,
isResolvedPos,
isSelection,
isStartOfDoc,
isStartOfTextBlock,
isTextSelection,
isTransaction,
joinStyles,
Expand Down
53 changes: 46 additions & 7 deletions packages/remirror__core/__tests__/keymap-extension.spec.ts
@@ -1,6 +1,7 @@
import { extensionValidityTest, renderEditor } from 'jest-remirror';
import { CodeExtension } from 'remirror/extensions';
import { ExtensionPriority } from '@remirror/core-constants';
import { ExtensionPriority } from 'remirror';
import { CodeExtension, HeadingExtension } from 'remirror/extensions';
import { TextSelection } from '@remirror/pm/state';

import { KeymapExtension } from '../';

Expand Down Expand Up @@ -69,21 +70,21 @@ test('it supports updating options at runtime', () => {
});
});

describe('exitMark', () => {
const editor = renderEditor([new CodeExtension()], {});
const { p, doc } = editor.nodes;
describe('arrow key exits', () => {
const editor = renderEditor([new CodeExtension(), new HeadingExtension()], {});
const { p, doc, heading } = editor.nodes;
const { code } = editor.marks;

// Simulate key presses for a real browser.
editor.manager.getExtension(KeymapExtension).addCustomHandler('keymap', [
ExtensionPriority.Lowest,
{
ArrowRight: ({ tr }) => {
editor.selectText(Math.min(tr.selection.from + 1, tr.doc.nodeSize - 2));
editor.selectText(TextSelection.near(tr.doc.resolve(tr.selection.anchor + 1), +1));
return true;
},
ArrowLeft: ({ tr }) => {
editor.selectText(Math.max(tr.selection.from - 1, 1));
editor.selectText(TextSelection.near(tr.doc.resolve(tr.selection.anchor - 1), -1));
return true;
},
},
Expand All @@ -98,6 +99,16 @@ describe('exitMark', () => {
expect(editor.state.doc).toEqualRemirrorDocument(doc(p('abc', code('this ')), p('the end')));
});

it('does not prevent exits when exiting backwards', () => {
editor
.add(doc(p('the end '), p(code('<cursor>this'))))
.press('ArrowLeft')
.press('ArrowLeft')
.insertText('abc');

expect(editor.state.doc).toEqualRemirrorDocument(doc(p('the end abc'), p(code('this'))));
});

it('can exit the mark forwards', () => {
editor
.add(doc(p(code('this<cursor>')), p('the end')))
Expand All @@ -124,4 +135,32 @@ describe('exitMark', () => {

expect(editor.state.doc).toEqualRemirrorDocument(doc(p(code('thishabcello'))));
});

it('can exit the empty node backwards', () => {
editor
.add(doc(p('the end '), heading('<cursor>')))
.press('ArrowLeft')
.insertText('abc');

expect(editor.state.doc).toEqualRemirrorDocument(doc(p('the end '), p('abc')));
});

it('can exit the empty node with double arrow keypress', () => {
editor
.add(doc(p('the end '), heading('<cursor>')))
.press('ArrowLeft')
.press('ArrowLeft')
.insertText('abc');

expect(editor.state.doc).toEqualRemirrorDocument(doc(p('the end abc'), p('')));
});

it('exits node normally when block node is not empty', () => {
editor
.add(doc(p('the end '), heading('<cursor>hi')))
.press('ArrowLeft')
.insertText('abc');

expect(editor.state.doc).toEqualRemirrorDocument(doc(p('the end abc'), heading('hi')));
});
});

0 comments on commit 8f9f9fc

Please sign in to comment.