react-compare-slider
react-compare-slider copied to clipboard
Limit the focus of the handle element
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
After just clicking to the handle
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.
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
Thanks for your reply! @nerdyman
@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 Yeah, that's a cool idea, love it!
@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 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 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.