Skip to content
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
4 changes: 2 additions & 2 deletions packages/shared/src/components/MenuIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export const MenuIcon = ({
}: MenuIconProps): ReactElement => {
return (
<Icon
size={IconSize.Small}
size={IconSize.XSmall}
secondary={secondary}
className={classNames('mr-2 text-2xl', className)}
className={classNames('text-xl', className)}
/>
);
};
63 changes: 45 additions & 18 deletions packages/shared/src/components/dropdown/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,6 +35,7 @@ interface DropdownMenuContentProps
children: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end';
variant?: 'action' | 'field';
}

export const DropdownMenuTrigger = React.forwardRef<
Expand Down Expand Up @@ -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 (
<DropdownMenuPortal container={container}>
<DropdownMenuContentRoot
{...props}
ref={forwardedRef}
className={classNames('DropdownMenuContent overflow-hidden', className)}
align={align}
>
<div className="DropdownMenuScrollable max-h-72 overflow-y-auto bg-inherit">
{children}
</div>
</DropdownMenuContentRoot>
</DropdownMenuPortal>
);
});
>(
(
{
children,
className,
align = 'end',
collisionPadding,
sideOffset,
variant = 'action',
...props
},
forwardedRef,
) => {
const { isCompanion } = useRequestProtocol();
const container = isCompanion ? getCompanionWrapper() : undefined;
const scrollFadeRef = useScrollFade<HTMLDivElement>();
return (
<DropdownMenuPortal container={container}>
<DropdownMenuContentRoot
{...props}
ref={forwardedRef}
className={classNames(
'DropdownMenuContent overflow-hidden',
variant === 'field'
? 'DropdownMenuContentField'
: 'DropdownMenuContentAction',
className,
)}
align={align}
sideOffset={sideOffset ?? (variant === 'action' ? 6 : undefined)}
collisionPadding={collisionPadding ?? 24}
>
<div
ref={scrollFadeRef}
className="DropdownMenuScrollable max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto bg-inherit"
>
{children}
</div>
</DropdownMenuContentRoot>
</DropdownMenuPortal>
);
},
);

DropdownMenuContent.displayName = 'DropdownMenuContent';

Expand Down
35 changes: 34 additions & 1 deletion packages/shared/src/components/dropdown/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/fields/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export function Dropdown({
<DropdownMenuTrigger asChild>{renderButton()}</DropdownMenuTrigger>
<DropdownMenuContent
id={`${id}-content`}
variant="field"
className={classNames(
'overflow-hidden',
className.menu || 'menu-primary',
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/components/fields/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ const Select = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
variant="field"
align="start"
sideOffset={10}
className="flex w-[var(--radix-popper-anchor-width)] flex-col gap-1 overflow-y-auto overflow-x-hidden !p-0"
className="flex flex-col gap-1 !p-0"
>
<DropdownMenuOptions options={menuItems} />
</DropdownMenuContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ const PollDurationDropdown = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
variant="field"
align="start"
sideOffset={10}
className="flex w-[var(--radix-popper-anchor-width)] flex-col gap-1 overflow-y-auto overflow-x-hidden !p-0"
className="flex flex-col gap-1 !p-0"
>
<DropdownMenuOptions options={menuItems} />
</DropdownMenuContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}}
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/components/profile/MonthSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ const MonthSelect = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
variant="field"
align="start"
sideOffset={10}
className="flex w-[var(--radix-popper-anchor-width)] flex-col gap-1 !p-0"
className="flex flex-col gap-1 !p-0"
>
<DropdownMenuOptions options={menuItems} />
</DropdownMenuContent>
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/components/profile/YearSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ const YearSelect = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
variant="field"
align="start"
sideOffset={10}
className="flex w-[var(--radix-popper-anchor-width)] flex-col gap-1 !p-0"
className="flex flex-col gap-1 !p-0"
>
<DropdownMenuOptions options={menuItems} />
</DropdownMenuContent>
Expand Down
84 changes: 84 additions & 0 deletions packages/shared/src/hooks/useScrollFade.ts
Original file line number Diff line number Diff line change
@@ -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 = <El extends HTMLElement = HTMLDivElement>() => {
const cleanupRef = useRef<(() => void) | undefined>(undefined);
const frameRef = useRef<number | undefined>(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;
};