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

feat(scroll btn): Add SelectScrollButton for scrolling dropdown, visual overflow indication #7579

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions packages/ui-components/Common/Select/SelectScrollButton/index.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add stories for this component ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Keep in mind, that, ideally, the stories will show this component working within a Select component, and not on its own)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do it for pagination

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
import { useEffect, useRef, useState } from 'react';
import { type FC, type RefObject } from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { type FC, type RefObject } from 'react';
import type { FC, RefObject } from 'react';


import styles from '@node-core/ui-components/Common/Select/index.module.css';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import styles from '@node-core/ui-components/Common/Select/index.module.css';
import styles from '@node-core/ui-components/Common/Select/SelectScrollButton/index.module.css';

Maybe use dedicate css-module to simplify code


type SelectScrollButtonProps = {
direction: 'up' | 'down';
selectContentRef?: RefObject<HTMLDivElement | null>;
scrollAmount?: number;
scrollInterval?: number;
};

const SelectScrollButton: FC<SelectScrollButtonProps> = ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primitive radix select already has ScrollUpButton and ScrollDownButton Instead of adding these features, you can use what radix provides directly. Here is an example of primitive select anatomy; https://www.radix-ui.com/primitives/docs/components/select#anatomy

direction,
selectContentRef,
scrollAmount = 35,
scrollInterval = 50,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
scrollInterval = 50,
scrollInterval = 500,

for moment it's to quick

}) => {
const DirectionComponent =
direction === 'down' ? ChevronDownIcon : ChevronUpIcon;
const [isVisible, setIsVisible] = useState(false);
const [hasOverflow, setOverflow] = useState(false);
const intervalRef = useRef<number | null>(null);
const isScrollingRef = useRef(false);

const clearScrollInterval = () => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
};

const startScrolling = () => {
if (!selectContentRef?.current || !isVisible || !hasOverflow) return;

clearScrollInterval();

intervalRef.current = window.setInterval(() => {
if (!selectContentRef.current || !isScrollingRef.current) return;

const container = selectContentRef.current;

if (direction === 'down') {
container.scrollBy({ top: scrollAmount, behavior: 'smooth' });

if (
container.scrollTop >=
container.scrollHeight - container.clientHeight
) {
clearScrollInterval();
setIsVisible(false);
}
} else {
container.scrollBy({
top: -Math.abs(scrollAmount),
behavior: 'smooth',
});

if (container.scrollTop <= 0) {
clearScrollInterval();
setIsVisible(false);
}
}
Comment on lines +44 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an extremely extremely minor nitpick, but this could probably be simplified into a helper function?

(But this is such a tiny nitpick that you really don't have to do anything)

}, scrollInterval);
};

useEffect(() => {
if (!selectContentRef?.current) return;

const container = selectContentRef.current;
setOverflow(container.scrollHeight > container.clientHeight);

const updateButtonVisibility = () => {
if (!container) return;

if (direction === 'down') {
setIsVisible(
container.scrollTop < container.scrollHeight - container.clientHeight
);
} else {
setIsVisible(container.scrollTop > 0);
}
};

updateButtonVisibility();

const handleScroll = () => {
updateButtonVisibility();

if (!isScrollingRef.current && intervalRef.current !== null) {
clearScrollInterval();
}
};

container.addEventListener('scroll', handleScroll);
window.addEventListener('resize', updateButtonVisibility);

return () => {
container.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', updateButtonVisibility);
clearScrollInterval();
};
}, [direction, selectContentRef]);

const handleMouseEnter = () => {
isScrollingRef.current = true;
startScrolling();
};

const handleMouseLeave = () => {
isScrollingRef.current = false;
clearScrollInterval();
};

if (!isVisible) return null;

return (
<div
className={styles.scrollBtn}
data-direction={direction}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<DirectionComponent className={styles.scrollBtnIcon} aria-hidden="true" />
</div>
);
};

export default SelectScrollButton;
29 changes: 29 additions & 0 deletions packages/ui-components/Common/Select/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,32 @@
rounded;
}
}

.scrollBtn {
@apply sticky
z-10
flex
w-full
cursor-pointer
justify-center
bg-white
p-1
transition-colors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
transition-colors
motion-safe:transition-colors

hover:bg-neutral-100
dark:bg-neutral-950
dark:hover:bg-neutral-900;
}

.scrollBtn[data-direction='down'] {
bottom: 0;
}

.scrollBtn[data-direction='up'] {
top: 0;
}

.scrollBtnIcon {
@apply size-5
text-neutral-600
dark:text-neutral-400;
}
Comment on lines +153 to +180
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.scrollBtn {
@apply sticky
z-10
flex
w-full
cursor-pointer
justify-center
bg-white
p-1
transition-colors
hover:bg-neutral-100
dark:bg-neutral-950
dark:hover:bg-neutral-900;
}
.scrollBtn[data-direction='down'] {
bottom: 0;
}
.scrollBtn[data-direction='up'] {
top: 0;
}
.scrollBtnIcon {
@apply size-5
text-neutral-600
dark:text-neutral-400;
}
.scrollBtn {
@apply sticky
z-10
flex
w-full
cursor-pointer
justify-center
bg-white
p-1
transition-colors
hover:bg-neutral-100
dark:bg-neutral-950
dark:hover:bg-neutral-900;
&[data-direction='down'] {
@apply bottom-0;
}
&[data-direction='up'] {
@apply top-0
}
.scrollBtnIcon {
@apply size-5
text-neutral-600
dark:text-neutral-400;
}
}

you can use nested css to make it easier to maintain

9 changes: 8 additions & 1 deletion packages/ui-components/Common/Select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
import * as ScrollPrimitive from '@radix-ui/react-scroll-area';
import * as SelectPrimitive from '@radix-ui/react-select';
import classNames from 'classnames';
import { useEffect, useId, useMemo, useState } from 'react';
import { useEffect, useId, useMemo, useState, useRef } from 'react';
import type { ReactElement, ReactNode } from 'react';

import SelectScrollButton from '@node-core/ui-components/Common/Select/SelectScrollButton';
import Skeleton from '@node-core/ui-components/Common/Skeleton';
import type { FormattedMessage } from '@node-core/ui-components/types';

Expand Down Expand Up @@ -59,6 +60,7 @@ const Select = <T extends string>({
}: SelectProps<T>): ReactNode => {
const id = useId();
const [value, setValue] = useState(defaultValue);
const SelectContentRef = useRef<HTMLDivElement>(null);

useEffect(() => setValue(defaultValue), [defaultValue]);

Expand Down Expand Up @@ -163,6 +165,7 @@ const Select = <T extends string>({

<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={SelectContentRef}
position={inline ? 'popper' : 'item-aligned'}
className={classNames(styles.dropdown, {
[styles.inline]: inline,
Expand All @@ -178,6 +181,10 @@ const Select = <T extends string>({
<ScrollPrimitive.Thumb />
</ScrollPrimitive.Scrollbar>
</ScrollPrimitive.Root>
<SelectScrollButton
direction="down"
selectContentRef={SelectContentRef}
/>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
Expand Down
Loading