react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

[RAC] inputRef is undefined in Textfield

Open mehdibha opened this issue 1 year ago โ€ข 1 comments

Provide a general summary of the issue here

Textfield is providing an InputContext but when consuming it, the inputRef is undefined I am consuming it like this

const { ref } = useSlottedContext(InputContext)
const Demo = ()=> {
  return (
    <TextField>
      <Input />
      <Btn />
    </TextField>
}

const Btn = () => {
  const inputCtx = useSlottedContext(InputContext)
  return (
    <Button onPress={()=>{console.log(inputCtx?.ref?.current))>Log</Button>
}

๐Ÿค” Expected Behavior?

Expecting to ref to return the actual ref of the รŒnput in the TextField component

๐Ÿ˜ฏ Current Behavior

Currently the ref is undefined

๐Ÿ’ Possible Solution

No response

๐Ÿ”ฆ Context

No response

๐Ÿ–ฅ๏ธ Steps to Reproduce

https://codesandbox.io/p/sandbox/rac-textfield-input-ref-issue-d93cvl

Version

1.2

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

Windows

๐Ÿงข Your Company/Team

No response

๐Ÿ•ท Tracking Issue

No response

mehdibha avatar May 19 '24 13:05 mehdibha

It doesn't look like we are sending a ref you'd be able to access in the way you expect anyways. Can I ask more about what you're trying to accomplish? Could you do something along these lines instead?

let CustomContext = createContext({ ref: null });
const Field = (props) => {
  let inputRef = useRef(null);
  return (
    <CustomContext.Provider value={{ ref: inputRef }}>
      <TextField defaultValue="with textfield">
        <Input ref={inputRef} />
        <LogButton />
      </TextField>
    </CustomContext.Provider>
  );
};

const LogButton = (props) => {
  let { ref } = useContext(CustomContext);
  return (
    <Button
      onPress={() => {
        console.log(ref?.current);
      }}
    >
      Log ref
    </Button>
  );
};

snowystinger avatar May 20 '24 07:05 snowystinger

I think you're wrong

ref: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/TextField.tsx

    [InputContext, {...inputProps, ref: inputOrTextAreaRef}],
    [TextAreaContext, {...inputProps, ref: inputOrTextAreaRef}],

mehdibha avatar Jun 05 '24 13:06 mehdibha

My useCase: i have an <InputWrapper /> component that wraps any input where i need the ref to focus on input element when needed

interface InputWrapperProps
  extends Omit<AriaGroupProps, "className" | "prefix">,
    VariantProps<typeof inputStyles> {
  prefix?: React.ReactNode;
  suffix?: React.ReactNode;
  loading?: boolean;
  loaderPosition?: "prefix" | "suffix";
  className?: string;
}
const InputWrapper = React.forwardRef<HTMLDivElement, InputWrapperProps>(
  (
    {
      className,
      size,
      variant,
      loading,
      prefix,
      suffix,
      loaderPosition = "suffix",
      multiline = false,
      ...props
    },
    ref
  ) => {
    const { isInvalid } = React.useContext(AriaFieldErrorContext);
    const inputProps = React.useContext(AriaInputContext);
    const textAreaProps = React.useContext(AriaTextAreaContext);
    const localRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
    const inputRef = inputProps?.ref ?? textAreaProps?.ref ?? localRef; // TODO Fix this
    const { root } = inputStyles({
      size,
      variant: variant ?? (isInvalid ? "danger" : undefined),
      multiline,
    });
    const showPrefixLoading = loading && loaderPosition === "prefix";
    const showSuffixLoading = loading && loaderPosition === "suffix";
    return (
      <Provider
        values={[
          [AriaInputContext, { ...inputProps, ref: inputRef as React.RefObject<HTMLInputElement> }],
          [
            AriaTextAreaContext,
            { ...textAreaProps, ref: inputRef as React.RefObject<HTMLTextAreaElement> },
          ],
        ]}
      >
        <AriaGroup
          ref={ref}
          role="presentation"
          className={root({ className })}
          {...props}
          onPointerDown={(event) => {
            const target = event.target as HTMLElement;
            if (target.closest("input, button, a")) return;
            const input = inputRef.current;
            if (!input) return;
            requestAnimationFrame(() => {
              input.focus();
            });
          }}
        >
          {composeRenderProps(props.children, (children) => (
            <>
              <InputInnerVisual
                side="start"
                loading={showPrefixLoading}
                multiline={multiline}
              >
                {prefix}
              </InputInnerVisual>
              {children}
              <InputInnerVisual
                side="end"
                loading={showSuffixLoading}
                multiline={multiline}
              >
                {suffix}
              </InputInnerVisual>
            </>
          ))}
        </AriaGroup>
      </Provider>
    );
  }
);
InputWrapper.displayName = "InputWrapper";

mehdibha avatar Jun 05 '24 13:06 mehdibha

I think you're wrong

ref: https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/TextField.tsx

    [InputContext, {...inputProps, ref: inputOrTextAreaRef}],
    [TextAreaContext, {...inputProps, ref: inputOrTextAreaRef}],

Those are defined as a callback ref, it has no 'current' property, therefore, you cannot access it in the way you are expecting. https://github.com/adobe/react-spectrum/blob/0c17289de18041e6f8b99df26a5a3ca922cc5145/packages/react-aria-components/src/TextField.tsx#L70

What you could do though, is merge the ref already on the context with your own ref. Then place that merged ref on the context to propagate it down. https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/utils/src/mergeRefs.ts

snowystinger avatar Jun 05 '24 21:06 snowystinger

Okey, it's more clear for me, thank you so much ๐Ÿ™

mehdibha avatar Jun 06 '24 14:06 mehdibha