react-compare-slider icon indicating copy to clipboard operation
react-compare-slider copied to clipboard

Limit the focus of the handle element

Open MiracleHorizon opened this issue 1 year ago • 7 comments

Hello! I am facing the following situation: I have a page with a vertical slide using your library. Suppose I can't see the slide completely, but I can move the handle. In this case, the page will scroll to the element because of the focus (https://github.com/nerdyman/react-compare-slider/blob/main/lib/src/ReactCompareSlider.tsx#L192-L195)

I would like to be able to disable this behavior. For example, the possibility to pass a boolean flag that will default to 'true' to keep the current behavior. Thanks!

Before clicking

Снимок экрана 2024-01-30 в 16 32 45

After just clicking to the handle

Снимок экрана 2024-01-30 в 16 33 11

MiracleHorizon avatar Jan 30 '24 13:01 MiracleHorizon

I have a custom handle. Because I don't have direct access to the button element (handleContainer), it seems impossible for me to solve this problem on my own. I also can't remove event listener from the code.

MiracleHorizon avatar Jan 30 '24 13:01 MiracleHorizon

Thanks for opening the issue @MiracleHorizon. I'll see if I can find a workaround for this - I think I opted for this behaviour to adhere to how <input type="range"> components work since that's what this component renders as to screen readers.

~~Something like a autoFocusHandle prop would be a good addition if there's no workaround.~~

Edit: TIL there's also an official API to control this behaviour so it might be good for the lib to expose these options as a prop https://html.spec.whatwg.org/multipage/interaction.html#focus-management-apis

nerdyman avatar Jan 30 '24 14:01 nerdyman

Thanks for your reply! @nerdyman

MiracleHorizon avatar Jan 30 '24 18:01 MiracleHorizon

@MiracleHorizon Allowing a completely custom callback function would be more flexible than a scroll prop, what do you think?

E.g.

const Example = () => {
  const handleClick = useCallback((ev: MouseEvent) => {
    // Prevent default always needed for Firefox.
    ev.preventDefault();

    // Do whatever you like here.
    (ev.currentTarget as HTMLButtonElement).focus({ preventScroll: true });
  }, [])

  return (
    <ReactCompareSlider onHandleContainerClick={handleClick} />
  )
};

nerdyman avatar Jan 30 '24 18:01 nerdyman

@nerdyman Yeah, that's a cool idea, love it!

MiracleHorizon avatar Jan 30 '24 19:01 MiracleHorizon

@MiracleHorizon I found that if you do ev.stopImmediatePropagation() in a click handler on the button element it does prevent the other event handlers from being fired so I'm thinking of exposing the handleContainer DOM node via the useReactCompareSliderRef instead of adding another prop to the component. The component is already pretty prop heavy so I'd prefer to do overrides like this through the DOM directly where possible.

It would end up looking like this:

const Example = () => {
  const reactCompareSliderRef = useReactCompareSliderRef();

  useEffect(() => {
      const handleContainer = reactCompareSliderRef.current.handleContainer!;
  
      const containerClick = (ev: MouseEvent) => {
        ev.preventDefault();
        ev.stopImmediatePropagation();
        handleContainer.focus({ preventScroll: true });
      };
  
      handleContainer.addEventListener('click', containerClick, { capture: true });
  
      return () => {
        handleContainer.removeEventListener('click', containerClick, { capture: true });
      };
    }, []);

  return (
    <ReactCompareSlider ref={reactCompareSliderRef} />
  )
};

If you have any standard hook libs like react-use or @uidotdev/usehooks you could use their hooks to make things simpler. E.g. with react-use:

const containerClick = (ev: MouseEvent) => {
  const handleContainer = ev.currentTarget as HTMLButtonElement;
  ev.preventDefault();
  ev.stopImmediatePropagation();
  handleContainer.focus({ preventScroll: true });
};

const Example = () => {
  const reactCompareSliderRef = useReactCompareSliderRef();

  useEvent('click', containerClick, reactCompareSliderRef.current.handleContainer, { capture: true });

  return (
    <ReactCompareSlider ref={reactCompareSliderRef} />
  )
};

It's a bit more verbose but gives you the same functionality, also exposing handleContainer allows you to attach any other event bindings as you see fit pretty easily.

If you want to test this out on the current version of the lib (3.0.1) you can do the following:

const Example = () => {
  const reactCompareSliderRef = useReactCompareSliderRef();

  useEffect(() => {
    const rootContainer = reactCompareSliderRef.current.rootContainer!;
    // v3.0.1 doesn't have `handleContainer` so use a query selector from the root container instead.
    const handleContainer = rootContainer.querySelector(
      '[data-rcs="handle-container"]',
    ) as HTMLButtonElement;

    const containerClick = (ev: MouseEvent) => {
      ev.preventDefault();
      ev.stopImmediatePropagation();
      handleContainer.focus({ preventScroll: true });
    };

    handleContainer.addEventListener('click', containerClick, { capture: true });

    return () => {
      handleContainer.removeEventListener('click', containerClick, { capture: true });
    };
  }, []);

  return (
    <ReactCompareSlider ref={reactCompareSliderRef} />
  )
};

If you can give this a go and let me know if it works that'd be great! I'm not 100% sold on this but think it's probably better than adding another prop to the main component.

nerdyman avatar Jan 31 '24 14:01 nerdyman

@nerdyman It's all working! Thank you! When I tried to solve this problem myself, I used ev.stopImmediatePropagation(). But to make it work the way it should, you have to set capture: true :( I didn't think of that.

MiracleHorizon avatar Jan 31 '24 15:01 MiracleHorizon

@MiracleHorizon Going to close this since it looks like the workaround works well, thanks for raising the issue!

The reactCompareSliderRef.current.handleContainer is now available in 3.1.0 so you should be able to use the simpler workaround above if you'd like.

nerdyman avatar Apr 16 '24 17:04 nerdyman