Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide the scrollbar thumb after few seconds of inactivity #46

Closed
gamegee opened this issue Mar 18, 2019 · 17 comments
Closed

Hide the scrollbar thumb after few seconds of inactivity #46

gamegee opened this issue Mar 18, 2019 · 17 comments
Labels
enhancement New feature or request good first issue Good for newcomers wontfix This will not be worked on

Comments

@gamegee
Copy link

gamegee commented Mar 18, 2019

Do you want to request a feature or report a bug?
Request a feature

What is the current behavior?
The API does not provide a way to hide the scrollbar thumb after inactivity. This is the default behavior for some browsers (on mac OS), the scrollbar is hidden when not used, once you scroll it's visible again.

What is the expected behavior?
Provide a prop to enable this hidding behavior.. And we may also have a property to set the timer after which the scrollbar thumb should appear / disappear.

@xobotyi
Copy link
Owner

xobotyi commented Mar 18, 2019

You can implement it with help of onScrollStart and onScrollStop callbacks.
That feature is neded only for a very small group of people so no need to incerease bundle size for everyone.
At least i think so atm

@gamegee
Copy link
Author

gamegee commented Mar 18, 2019

Yes I already implemented it really easily thanks to the API.
It was more an idea for your next release, but of course if only few people need it, they can implement it themself

@xobotyi xobotyi added the enhancement New feature or request label Mar 19, 2019
@krazyjakee
Copy link

Just a note, I am thumbs upping because I expected this functionality out of the box as it simulates osx approach to scrollbars.

The use case is I want consistent scroll bars on all platforms, something I believe most devs strive for and why they would use this awesome package.

@xobotyi
Copy link
Owner

xobotyi commented Apr 23, 2019

@krazyjakee as i already said above this is a very situative functionality which a very few developers need.
There is no need to force everyone to have useless (for those who dont need it, and i suppose theyre the most of users) code in their production bundle.

Requested feature can be implemented in 10-15 rows of code on your side.

I've developed this package to be most performant and handy minimal base to implement anything you wish about scrollbars, but that dont try to be a "silver bullet".

@krazyjakee
Copy link

@xobotyi thanks for the quick response.

Made a small wrapper that adds an autoHide prop for anyone interested. https://gist.github.com/krazyjakee/8c87cae9bb33cb89fecb8e2f233657a9

@xobotyi xobotyi added good first issue Good for newcomers wontfix This will not be worked on labels May 16, 2019
@xobotyi xobotyi closed this as completed May 19, 2019
@makivlach
Copy link

makivlach commented Jun 5, 2019

@krazyjakee Thank you for your example! I think your code would be better by using visibility attribute instead of display. Display: hidden made, in my case, scrollbar somehow forget overall inner width.

Take a look at this screen portraying the issue:
image

true scrollbar width:
image

Another reason for using visibility attribute instead of display would the fact that display might reposition your layout after showing/hiding, while visibility doesn't.

@xobotyi
Copy link
Owner

xobotyi commented Jun 6, 2019

@vlachmilan in other hand, visibility will not remove element from layout, so all the events for thumbs and tracks will preserve.
That will cause the scrolling if user will click the area of invisible scrollbar.

Please make an issue, i'll investigate it when i have some time

@makivlach
Copy link

Here's the reference to the issue. #82

@amok
Copy link

amok commented Jul 1, 2019

another solution

import React, { useState, useMemo, useCallback, useRef } from 'react';
import ReactScrollbarsCustom, { ScrollbarProps } from 'react-scrollbars-custom';

interface PropsType {
  autoHide?: boolean
  hideTimeout?: number
}

export default (props: PropsType & ScrollbarProps) => {
  const {
    autoHide,
    children,
    hideTimeout = 500,
    ...other
  } = props

  const [isScrolling, setIsScrolling] = useState()
  const [isMouseOver, setIsMouseOver] = useState()

  const trackStyle = useMemo(() => ({
    opacity: autoHide && !isScrolling ? 0 : 1,
    transition: 'opacity 0.4s ease-in-out',
    background: 'none'
  }), [autoHide, isScrolling])

  const stopTimer = useRef<number | undefined>()

  const showTrack = useCallback(() => {
    clearTimeout(stopTimer.current)
    setIsScrolling(true)
  }, [stopTimer])

  const hideTrack = useCallback(() => {
    stopTimer.current = setTimeout(() => {
      setIsScrolling(false)
    }, hideTimeout)
  }, [stopTimer, hideTimeout])

  const thumbYStyle = useMemo(() => ({
    left: 2,
    width: 6,
    position: 'relative'
  }), [])

  const wrapperStyle = useMemo(() => ({
    right: 0,
    bottom: 0
  }), [])

  const thumbXStyle = useMemo(() => ({
    top: 2,
    height: 6,
    position: 'relative'
  }), [])

  const onScrollStart = useCallback(() => {
    if (autoHide) {
      showTrack()
    }
  }, [])

  const onScrollStop = useCallback(() => {
    if (autoHide && !isMouseOver) {
      hideTrack()
    }
  }, [])

  const onMouseEnter = useCallback(() => {
    if (autoHide) {
      showTrack()
      setIsMouseOver(true)
    }
  }, [])

  const onMouseLeave = useCallback(() => {
    if (autoHide) {
      hideTrack()
      setIsMouseOver(false)
    }
  }, [])

  return (
    <ReactScrollbarsCustom
      thumbYProps={{ style: thumbYStyle }}
      thumbXProps={{ style: thumbXStyle }}
      wrapperProps={{ style: wrapperStyle }}
      trackXProps={{ style: trackStyle, onMouseEnter, onMouseLeave }}
      trackYProps={{ style: trackStyle, onMouseEnter, onMouseLeave }}
      onScrollStop={onScrollStop}
      onScrollStart={onScrollStart}
      {...other}
    >
      {children}
    </ReactScrollbarsCustom>
  )
};

@amcdnl
Copy link

amcdnl commented Jul 6, 2019

I'd love to see this implemented natively too.

@dqunbp
Copy link

dqunbp commented Jul 16, 2019

@amok Good shot! But it has a potential memory leak without unmount effect:

useEffect(() => {
    return () => clearTimeout(stopTimer.current);
  }, []);

It also has many react-hooks/exhaustive-deps warnings, for a missing dependencies in useCallback functions.
I tried to fix this:

import React, { useState, useMemo, useCallback, useRef } from 'react';
import ReactScrollbarsCustom, { ScrollbarProps } from 'react-scrollbars-custom';

interface PropsType {
  autoHide?: boolean
  hideTimeout?: number
}

export default (props: PropsType & ScrollbarProps) => {
  const {
    autoHide,
    children,
    hideTimeout = 500,
    ...other
  } = props

  const [isScrolling, setIsScrolling] = useState()
  const [isMouseOver, setIsMouseOver] = useState()

  const trackStyle = useMemo(
    () => ({
      opacity: autoHide && !isScrolling ? 0 : 1,
      transition: "opacity 0.4s ease-in-out",
      background: "none"
    }),
    [autoHide, isScrolling]
  );

  const stopTimer = useRef();

  const showTrack = useCallback(() => {
    clearTimeout(stopTimer.current);
    setIsScrolling(true);
  }, [stopTimer]);

  const hideTrack = useCallback(() => {
    stopTimer.current = setTimeout(() => {
      setIsScrolling(false);
    }, hideTimeout);
  }, [stopTimer, hideTimeout]);

  const thumbYStyle = useMemo(
    () => ({
      left: 2,
      width: 6,
      position: "relative"
    }),
    []
  );

  const wrapperStyle = useMemo(
    () => ({
      right: 0,
      bottom: 0
    }),
    []
  );

  const thumbXStyle = useMemo(
    () => ({
      top: 2,
      height: 6,
      position: "relative"
    }),
    []
  );

  const onScrollStart = useCallback(() => {
    if (autoHide) {
      showTrack();
    }
  }, [autoHide, showTrack]);

  const onScrollStop = useCallback(() => {
    if (autoHide && !isMouseOver) {
      hideTrack();
    }
  }, [autoHide, hideTrack, isMouseOver]);

  const onMouseEnter = useCallback(() => {
    if (autoHide) {
      showTrack();
      setIsMouseOver(true);
    }
  }, [autoHide, showTrack]);

  const onMouseLeave = useCallback(() => {
    if (autoHide) {
      hideTrack();
      setIsMouseOver(false);
    }
  }, [autoHide, hideTrack]);

  useEffect(() => {
    return () => clearTimeout(stopTimer.current);
  }, []);

  return (
    <ReactScrollbarsCustom
      thumbYProps={{ style: thumbYStyle }}
      thumbXProps={{ style: thumbXStyle }}
      wrapperProps={{ style: wrapperStyle }}
      trackXProps={{ style: trackStyle, onMouseEnter, onMouseLeave }}
      trackYProps={{ style: trackStyle, onMouseEnter, onMouseLeave }}
      onScrollStop={onScrollStop}
      onScrollStart={onScrollStart}
      {...other}
    >
      {children}
    </ReactScrollbarsCustom>
  )
};

@amcdnl
Copy link

amcdnl commented Jul 16, 2019

I ended up doing this w/ some simple css to show them when you hover into the area.

@amok
Copy link

amok commented Jul 16, 2019

@dqunbp thank you for noticing it and sharing the code!

@KirillSkomarovskiy
Copy link

KirillSkomarovskiy commented Nov 15, 2019

Example without setTimeout

import React, { useCallback, useMemo, useState } from "react";
import ReactScrollbarsCustom from "react-scrollbars-custom";

export function Scrollbar({ children, ...props }) {
  const [isScrolling, setIsScrolling] = useState(false);
  const [isMouseOver, setIsMouseOver] = useState(false);
  const isShow = isScrolling || isMouseOver;

  const onScrollStart = useCallback(() => {
    setIsScrolling(true);
  }, []);
  const onScrollStop = useCallback(() => {
    setIsScrolling(false);
  }, []);
  const onMouseEnter = useCallback(() => {
    setIsMouseOver(true);
  }, []);
  const onMouseLeave = useCallback(() => {
    setIsMouseOver(false);
  }, []);

  const trackProps = useMemo(() => ({
    renderer: ({ elementRef, style, ...restProps }) => (
      <span
        {...restProps}
        ref={elementRef}
        style={{ ...style, opacity: isShow ? 1 : 0, transition: "opacity 0.4s ease-in-out", }}
        onMouseEnter={onMouseEnter}
        onMouseLeave={onMouseLeave}
      />
    )
  }), [isShow, onMouseEnter, onMouseLeave]);

  return (
    <ReactScrollbarsCustom
      {...props}
      wrapperProps={{
        renderer: ({ elementRef, style, ...restProps }) => (
          <div {...restProps} ref={elementRef} style={{ ...style, right: 0 }} />
        ),
      }}
      trackXProps={trackProps}
      trackYProps={trackProps}
      onScrollStart={onScrollStart}
      onScrollStop={onScrollStop}
      scrollDetectionThreshold={500} // ms
    >
      {children}
    </ReactScrollbarsCustom>
  );
}

@josh-peterson-bose
Copy link

This is something lots of developers need. Look at the other react scrollbar components, many have it. +1

@Sir-hennihau
Copy link

Sir-hennihau commented Aug 12, 2021

Here is a TypeScript version of @KirillSkomarovskiy 's code.

I removed the callbacks, but I didn't measure if memoization is worth it here. But to me the callbacks didn't look expensive and memoization always has an overhead.

import React, { useState } from "react";
import ReactScrollbarsCustom, { ScrollbarProps } from "react-scrollbars-custom";
import { ElementPropsWithElementRef } from "react-scrollbars-custom/dist/types/types";

interface ScrollbarsProps extends ScrollbarProps {}

/**
 * This component enhances react-scrollbars-custom with autohide.
 *
 * @see https://github.com/xobotyi/react-scrollbars-custom/issues/46#issuecomment-554425245
 */
export function Scrollbar(props: ScrollbarsProps) {
    const [isScrolling, setIsScrolling] = useState(false);
    const [isMouseOver, setIsMouseOver] = useState(false);
    const isShow = isScrolling || isMouseOver;

    const onScrollStart = () => setIsScrolling(true);
    const onScrollStop = () => setIsScrolling(false);
    const onMouseEnter = () => setIsMouseOver(true);
    const onMouseLeave = () => setIsMouseOver(false);

    const trackProps = {
        renderer: ({
            elementRef,
            style,
            ...restProps
        }: ElementPropsWithElementRef) => (
            <span
                {...restProps}
                ref={elementRef}
                style={{
                    ...style,
                    opacity: isShow ? 1 : 0,
                    transition: "opacity 0.4s ease-in-out",
                }}
                onMouseEnter={onMouseEnter}
                onMouseLeave={onMouseLeave}
            />
        ),
    };

    const wrapperProps = {
        renderer: ({
            elementRef,
            style,
            ...restProps
        }: ElementPropsWithElementRef) => (
            <div
                {...restProps}
                ref={elementRef}
                style={{ ...style, right: 0 }}
            />
        ),
    };

    return (
        // @see https://github.com/xobotyi/react-scrollbars-custom/issues/187
        // @ts-ignore
        <ReactScrollbarsCustom
            wrapperProps={wrapperProps}
            trackXProps={trackProps}
            trackYProps={trackProps}
            onScrollStart={onScrollStart}
            onScrollStop={onScrollStop}
            scrollDetectionThreshold={500} // ms
            {...props}
        />
    );
}

Keep in mind there is still one weird ts-ignore that I couldn't fix yet.

@LeeAlephium
Copy link

Today we needed this to simulate how scrollbars are hidden in Slack or macos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests