diff --git a/packages/shared/src/components/MenuIcon.tsx b/packages/shared/src/components/MenuIcon.tsx
index e69fb86fe4b..6d5d02ed9e2 100644
--- a/packages/shared/src/components/MenuIcon.tsx
+++ b/packages/shared/src/components/MenuIcon.tsx
@@ -14,9 +14,9 @@ export const MenuIcon = ({
}: MenuIconProps): ReactElement => {
return (
);
};
diff --git a/packages/shared/src/components/dropdown/DropdownMenu.tsx b/packages/shared/src/components/dropdown/DropdownMenu.tsx
index bf49b1c95e2..5ba594b0797 100644
--- a/packages/shared/src/components/dropdown/DropdownMenu.tsx
+++ b/packages/shared/src/components/dropdown/DropdownMenu.tsx
@@ -23,6 +23,7 @@ import Link from '../utilities/Link';
import type { MenuItemProps } from './common';
import { useRequestProtocol } from '../../hooks/useRequestProtocol';
import { getCompanionWrapper } from '../../lib/extension';
+import { useScrollFade } from '../../hooks/useScrollFade';
export const DropdownMenuItem = classed(
DropdownMenuItemRoot,
@@ -34,6 +35,7 @@ interface DropdownMenuContentProps
children: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end';
+ variant?: 'action' | 'field';
}
export const DropdownMenuTrigger = React.forwardRef<
@@ -100,24 +102,49 @@ DropdownMenu.displayName = 'DropdownMenu';
export const DropdownMenuContent = React.forwardRef<
HTMLDivElement,
DropdownMenuContentProps
->(({ children, className, align = 'end', ...props }, forwardedRef) => {
- const { isCompanion } = useRequestProtocol();
- const container = isCompanion ? getCompanionWrapper() : undefined;
- return (
-
-
-
- {children}
-
-
-
- );
-});
+>(
+ (
+ {
+ children,
+ className,
+ align = 'end',
+ collisionPadding,
+ sideOffset,
+ variant = 'action',
+ ...props
+ },
+ forwardedRef,
+ ) => {
+ const { isCompanion } = useRequestProtocol();
+ const container = isCompanion ? getCompanionWrapper() : undefined;
+ const scrollFadeRef = useScrollFade();
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ },
+);
DropdownMenuContent.displayName = 'DropdownMenuContent';
diff --git a/packages/shared/src/components/dropdown/style.css b/packages/shared/src/components/dropdown/style.css
index b5a30a93fc5..7ae8b4f14f4 100644
--- a/packages/shared/src/components/dropdown/style.css
+++ b/packages/shared/src/components/dropdown/style.css
@@ -31,13 +31,46 @@
animation-name: slideRightAndFade;
}
+.DropdownMenuContentAction {
+ @apply min-w-64 max-w-[--radix-dropdown-menu-content-available-width];
+}
+
+.DropdownMenuContentField {
+ @apply min-w-40 w-[--radix-dropdown-menu-trigger-width] max-w-[--radix-dropdown-menu-content-available-width];
+}
+
+.DropdownMenuContentField .DropdownMenuScrollable {
+ max-height: min(var(--radix-dropdown-menu-content-available-height), 20rem);
+}
+
+.DropdownMenuScrollable {
+ --scroll-fade-top: 0px;
+ --scroll-fade-bottom: 0px;
+ --scroll-fade-top-opacity: 1;
+ --scroll-fade-bottom-opacity: 1;
+ -webkit-mask-image: linear-gradient(
+ to bottom,
+ rgb(0 0 0 / var(--scroll-fade-top-opacity)) 0,
+ black var(--scroll-fade-top),
+ black calc(100% - var(--scroll-fade-bottom)),
+ rgb(0 0 0 / var(--scroll-fade-bottom-opacity)) 100%
+ );
+ mask-image: linear-gradient(
+ to bottom,
+ rgb(0 0 0 / var(--scroll-fade-top-opacity)) 0,
+ black var(--scroll-fade-top),
+ black calc(100% - var(--scroll-fade-bottom)),
+ rgb(0 0 0 / var(--scroll-fade-bottom-opacity)) 100%
+ );
+}
+
.DropdownMenuItem,
.DropdownMenuCheckboxItem,
.DropdownMenuRadioItem,
.DropdownMenuSubTrigger {
border-radius: 0.625rem;
@apply text-text-tertiary;
- @apply flex items-center typo-footnote h-8 px-3 py-0 truncate;
+ @apply flex items-center typo-footnote h-7 px-2 py-0 truncate;
}
.DropdownMenuItem[data-disabled],
.DropdownMenuCheckboxItem[data-disabled],
diff --git a/packages/shared/src/components/fields/Dropdown.tsx b/packages/shared/src/components/fields/Dropdown.tsx
index 39e455b507f..9072f2e6577 100644
--- a/packages/shared/src/components/fields/Dropdown.tsx
+++ b/packages/shared/src/components/fields/Dropdown.tsx
@@ -231,6 +231,7 @@ export function Dropdown({
{renderButton()}
diff --git a/packages/shared/src/components/post/poll/PollDurationDropdown.tsx b/packages/shared/src/components/post/poll/PollDurationDropdown.tsx
index c6475fa9525..a6be7ebc827 100644
--- a/packages/shared/src/components/post/poll/PollDurationDropdown.tsx
+++ b/packages/shared/src/components/post/poll/PollDurationDropdown.tsx
@@ -80,9 +80,10 @@ const PollDurationDropdown = () => {
diff --git a/packages/shared/src/components/profile/ExperienceLevelDropdown.tsx b/packages/shared/src/components/profile/ExperienceLevelDropdown.tsx
index e0fd552265f..3ac9871fa74 100644
--- a/packages/shared/src/components/profile/ExperienceLevelDropdown.tsx
+++ b/packages/shared/src/components/profile/ExperienceLevelDropdown.tsx
@@ -63,10 +63,7 @@ const ExperienceLevelDropdown = ({
'!shadow-[inset_0.125rem_0_0_var(--status-error)]',
selectedIndex > -1 && '!text-text-primary',
),
- menu: classNames(
- menuClassName,
- 'menu-primary max-h-[15.375rem] overflow-y-auto p-1',
- ), // fit 6 items
+ menu: classNames(menuClassName, 'menu-primary p-1'),
item: classNames(itemClassName, '*:min-h-10 *:!typo-callout'),
container: dropdownClassName,
}}
diff --git a/packages/shared/src/components/profile/MonthSelect.tsx b/packages/shared/src/components/profile/MonthSelect.tsx
index 53879995ec6..12f7fc5b968 100644
--- a/packages/shared/src/components/profile/MonthSelect.tsx
+++ b/packages/shared/src/components/profile/MonthSelect.tsx
@@ -62,9 +62,10 @@ const MonthSelect = ({
diff --git a/packages/shared/src/components/profile/YearSelect.tsx b/packages/shared/src/components/profile/YearSelect.tsx
index ee30e04afa2..24c5ab2acc9 100644
--- a/packages/shared/src/components/profile/YearSelect.tsx
+++ b/packages/shared/src/components/profile/YearSelect.tsx
@@ -61,9 +61,10 @@ const YearSelect = ({
diff --git a/packages/shared/src/hooks/useScrollFade.ts b/packages/shared/src/hooks/useScrollFade.ts
new file mode 100644
index 00000000000..dd5b5b97550
--- /dev/null
+++ b/packages/shared/src/hooks/useScrollFade.ts
@@ -0,0 +1,84 @@
+import { useCallback, useEffect, useRef } from 'react';
+
+const SCROLL_FADE_SIZE = '2rem';
+const TOP_FADE_VARIABLE = '--scroll-fade-top';
+const BOTTOM_FADE_VARIABLE = '--scroll-fade-bottom';
+const TOP_FADE_OPACITY_VARIABLE = '--scroll-fade-top-opacity';
+const BOTTOM_FADE_OPACITY_VARIABLE = '--scroll-fade-bottom-opacity';
+const EDGE_VISIBLE_OPACITY = '0.24';
+const FULLY_VISIBLE_OPACITY = '1';
+
+const updateScrollFade = (element: HTMLElement): void => {
+ const canScroll = element.scrollHeight > element.clientHeight + 1;
+ if (!canScroll) {
+ element.style.setProperty(TOP_FADE_VARIABLE, '0px');
+ element.style.setProperty(BOTTOM_FADE_VARIABLE, '0px');
+ element.style.setProperty(TOP_FADE_OPACITY_VARIABLE, FULLY_VISIBLE_OPACITY);
+ element.style.setProperty(
+ BOTTOM_FADE_OPACITY_VARIABLE,
+ FULLY_VISIBLE_OPACITY,
+ );
+ return;
+ }
+
+ const canScrollUp = element.scrollTop > 1;
+ const canScrollDown =
+ element.scrollTop + element.clientHeight < element.scrollHeight - 1;
+
+ element.style.setProperty(
+ TOP_FADE_VARIABLE,
+ canScrollUp ? SCROLL_FADE_SIZE : '0px',
+ );
+ element.style.setProperty(
+ TOP_FADE_OPACITY_VARIABLE,
+ canScrollUp ? EDGE_VISIBLE_OPACITY : FULLY_VISIBLE_OPACITY,
+ );
+ element.style.setProperty(
+ BOTTOM_FADE_VARIABLE,
+ canScrollDown ? SCROLL_FADE_SIZE : '0px',
+ );
+ element.style.setProperty(
+ BOTTOM_FADE_OPACITY_VARIABLE,
+ canScrollDown ? EDGE_VISIBLE_OPACITY : FULLY_VISIBLE_OPACITY,
+ );
+};
+
+export const useScrollFade = () => {
+ const cleanupRef = useRef<(() => void) | undefined>(undefined);
+ const frameRef = useRef(undefined);
+
+ const setElementRef = useCallback((element: El | null) => {
+ cleanupRef.current?.();
+ cleanupRef.current = undefined;
+
+ if (!element) {
+ return;
+ }
+
+ const scheduleUpdate = () => {
+ if (frameRef.current !== undefined) {
+ cancelAnimationFrame(frameRef.current);
+ }
+ frameRef.current = requestAnimationFrame(() => updateScrollFade(element));
+ };
+
+ const resizeObserver = new ResizeObserver(scheduleUpdate);
+ resizeObserver.observe(element);
+ element.addEventListener('scroll', scheduleUpdate, { passive: true });
+
+ updateScrollFade(element);
+
+ cleanupRef.current = () => {
+ element.removeEventListener('scroll', scheduleUpdate);
+ resizeObserver.disconnect();
+ if (frameRef.current !== undefined) {
+ cancelAnimationFrame(frameRef.current);
+ frameRef.current = undefined;
+ }
+ };
+ }, []);
+
+ useEffect(() => () => cleanupRef.current?.(), []);
+
+ return setElementRef;
+};