All files / collaborative-ui/src/CollaborativeInput index.tsx

82.6% Statements 19/23
41.66% Branches 5/12
80% Functions 4/5
84.21% Lines 16/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 941x 1x                                                                                                                                 1x 6x 6x     6x 6x 6x 6x   6x 6x 6x   6x 6x 6x       6x              
import * as React from 'react';
import {bind} from '@jsonjoy.com/collaborative-input';
import type {CollaborativeStr} from '@jsonjoy.com/collaborative-str';
 
/**
 * Imperative handle that a custom input element, rendered through the
 * {@link CollaborativeInputProps.inp} render prop, may expose to
 * `<CollaborativeInput>` via its `ref`.
 */
export interface CollaborativeInputHandle {
  /** The underlying `<input>` or `<textarea>` element to bind the CRDT to. */
  readonly input: HTMLInputElement | HTMLTextAreaElement | null;
 
  /**
   * Optional callback to re-measure/resize the element. Called once after the
   * initial binding and whenever the bound "str" node changes (i.e. on remote
   * or programmatic edits that don't emit a DOM `input` event).
   */
  resize?: () => void;
}
 
/** Props passed to the {@link CollaborativeInputProps.inp} render prop. */
export interface CollaborativeInputRenderProps {
  /**
   * Ref which must receive the input element's {@link CollaborativeInputHandle}.
   * Typed loosely so it can be spread onto any element/component whose handle is
   * structurally compatible with {@link CollaborativeInputHandle} (such as
   * `<FlexibleInput>`, whose handle is a superset).
   */
  ref: React.Ref<any>;
 
  /** Whether the input is multiline. */
  multiline?: boolean;
 
  /** The value is driven by the CRDT binding, not controlled by React. */
  uncontrolled: true;
}
 
export interface CollaborativeInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  /** JSON CRDT "str" node API. */
  str: () => CollaborativeStr;
 
  /**
   * Whether to poll for updates the underlying <input> or <textarea> element
   * in case some third-party code modifies the value of the input element.
   */
  polling?: boolean;
 
  /** Whether the input is multiline. */
  multiline?: boolean;
 
  /**
   * Render prop for a custom input element. It receives props that must be
   * spread onto a component which exposes a {@link CollaborativeInputHandle}
   * through its `ref` (such as `<FlexibleInput>`):
   *
   * ```tsx
   * <CollaborativeInput str={str} inp={(props) => <FlexibleInput {...props} />} />
   * ```
   *
   * When omitted, a plain `<input>`/`<textarea>` element is rendered and all
   * other props are forwarded to it.
   */
  inp?: (props: CollaborativeInputRenderProps) => React.ReactElement;
}
 
export const CollaborativeInput: React.FC<CollaborativeInputProps> = ({inp, str, polling, multiline, ...rest}) => {
  const handleRef = React.useRef<CollaborativeInputHandle | null>(null);
  const domRef = React.useRef<HTMLInputElement | HTMLTextAreaElement | null>(null);
 
  // biome-ignore lint/correctness/useExhaustiveDependencies: the binding only depends on the "str" node and polling
  React.useLayoutEffect(() => {
    const input = inp ? (handleRef.current?.input ?? null) : domRef.current;
    Iif (!input) return;
    const unbind = bind(str, input, !!polling);
    let stop: undefined | (() => void);
    Eif (handleRef.current?.resize) {
      handleRef.current.resize();
      stop = str().api.onChange.listen(() => handleRef.current?.resize?.());
    }
    return () => {
      unbind();
      stop?.();
    };
  }, [str, polling]);
 
  Eif (inp) return inp({ref: handleRef, multiline, uncontrolled: true});
 
  (rest as any).ref = (el: HTMLInputElement | HTMLTextAreaElement) => {
    domRef.current = el;
  };
  return React.createElement(multiline ? 'textarea' : 'input', rest);
};