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

Revert "[MIRROR] Improves popper/dropdown performance" #1547

Closed
wants to merge 1 commit into from
Closed
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
14 changes: 12 additions & 2 deletions tgui/docs/component-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,9 @@ and displays selected entry.
- See inherited props: [Box](#box)
- See inherited props: [Icon](#icon)
- `options: string[] | DropdownEntry[]` - An array of strings which will be displayed in the
dropdown when open. See Dropdown.tsx for more advanced usage with DropdownEntry
dropdown when open. See Dropdown.tsx for more adcanced usage with DropdownEntry
- `selected: any` - Currently selected entry
- `width: string` - Width of dropdown button and resulting menu; css width value
- `over: boolean` - Dropdown renders over instead of below
- `color: string` - Color of dropdown button
- `noChevron: boolean` - Whether or not the arrow on the right hand side of the dropdown button is visible
Expand Down Expand Up @@ -735,7 +736,16 @@ to fine tune the value, or single click it to manually type a number.

### `Popper`

Popper lets you position elements so that they don't go out of the bounds of the window. See [react-tiny-popover](https://github.com/alexkatz/react-tiny-popover) for more information.
Popper lets you position elements so that they don't go out of the bounds of the window. See [popper.js](https://popper.js.org/) for more information.

**Props:**

- `popperContent: ReactNode` - The content that will be put inside the popper.
- `options?: { ... }` - An object of options to pass to `createPopper`. See [https://popper.js.org/docs/v2/constructors/#options]
- `isOpen: boolean` - Whether or not the popper is open.
- `placement?: string` - The placement of the popper. See [https://popper.js.org/docs/v2/constructors/#placement]
- `onClickOutside?: (e) => void` - A function that will be called when the user clicks outside of the popper.
- `additionalStyles: { ... }` - A map of CSS styles to add to the element that will contain the popper.

### `ProgressBar`

Expand Down
239 changes: 98 additions & 141 deletions tgui/packages/tgui/components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { classes } from 'common/react';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { Popover } from 'react-tiny-popover';
import { ReactNode, useState } from 'react';

import { BoxProps, unit } from './Box';
import { Box, BoxProps } from './Box';
import { Button } from './Button';
import { Icon } from './Icon';
import { Popper } from './Popper';

type DropdownEntry = {
displayText: ReactNode;
Expand All @@ -14,37 +14,21 @@ type DropdownEntry = {
type DropdownOption = string | DropdownEntry;

type Props = {
/** An array of strings which will be displayed in the
dropdown when open. See Dropdown.tsx for more advanced usage with DropdownEntry */
options: DropdownOption[];
/** Called when a value is picked from the list, `value` is the value that was picked */
onSelected: (value: any) => void;
onSelected: (selected: any) => void;
} & Partial<{
/** Whether to display previous / next buttons */
buttons: boolean;
/** Whether to clip the selected text */
clipSelectedText: boolean;
/** Color of dropdown button */
color: string;
/** Disables the dropdown */
disabled: boolean;
/** Text to always display in place of the selected text */
displayText: ReactNode;
/** Icon to display in dropdown button */
icon: string;
/** Angle of the icon */
iconRotation: number;
/** Whether or not the icon should spin */
iconSpin: boolean;
/** Width of the dropdown menu. Default: 15rem */
menuWidth: string;
/** Whether or not the arrow on the right hand side of the dropdown button is visible */
noChevron: boolean;
/** Called when dropdown button is clicked */
onClick: (event) => void;
/** Dropdown renders over instead of below */
over: boolean;
/** Currently selected entry */
selected: string | number;
}> &
BoxProps;
Expand Down Expand Up @@ -74,73 +58,51 @@ export function Dropdown(props: Props) {
width,
} = props;

const [opacity, setOpacity] = useState(0);
const [open, setOpen] = useState(true);
const [open, setOpen] = useState(false);
const adjustedOpen = over ? !open : open;
const innerRef = useRef<HTMLDivElement>(null);

/** Update the selected value when clicking the left/right buttons */
const updateSelected = useCallback(
(direction: 'previous' | 'next') => {
if (options.length < 1 || disabled) {
return;
}
const startIndex = 0;
const endIndex = options.length - 1;

let selectedIndex = options.findIndex(
(option) => getOptionValue(option) === selected,
);

if (selectedIndex < 0) {
selectedIndex = direction === 'next' ? endIndex : startIndex;
}

let newIndex = selectedIndex;
if (direction === 'next') {
newIndex = selectedIndex === endIndex ? startIndex : selectedIndex++;
} else {
newIndex = selectedIndex === startIndex ? endIndex : selectedIndex--;
}
/** Get the index of the selected option */
function getSelectedIndex() {
return options.findIndex((option) => {
return getOptionValue(option) === selected;
});
}

onSelected?.(getOptionValue(options[newIndex]));
},
[disabled, onSelected, options, selected],
);

/**
* HACK: Just like the original dropdown,
* the menu does not propagate correctly on the first event.
* This tricks it into getting the parent component's position
* so there is no flickering on open.
*/
useEffect(() => {
const timer = setTimeout(() => {
setOpen(false);
setOpacity(1);
}, 1);

return () => clearTimeout(timer);
}, []);

/** Allows the menu to be scrollable on open */
useEffect(() => {
if (!open) return;

innerRef.current?.focus();
}, [open]);
/** Update the selected value when clicking the left/right buttons */
function updateSelected(direction: 'previous' | 'next') {
if (options.length < 1 || disabled) {
return;
}

let selectedIndex = getSelectedIndex();
const startIndex = 0;
const endIndex = options.length - 1;

const hasSelected = selectedIndex >= 0;
if (!hasSelected) {
selectedIndex = direction === 'next' ? endIndex : startIndex;
}

const newIndex =
direction === 'next'
? selectedIndex === endIndex
? startIndex
: selectedIndex + 1
: selectedIndex === startIndex
? endIndex
: selectedIndex - 1;

onSelected?.(getOptionValue(options[newIndex]));
}

return (
<Popover
<Popper
autoFocus
isOpen={open}
onClickOutside={() => setOpen(false)}
positions={over ? 'top' : 'bottom'}
content={
<div
className="Layout Dropdown__menu"
style={{ minWidth: menuWidth, opacity }}
ref={innerRef}
>
placement={over ? 'top-start' : 'bottom-start'}
popperContent={
<div className="Layout Dropdown__menu" style={{ minWidth: menuWidth }}>
{options.length === 0 && (
<div className="Dropdown__menuentry">No options</div>
)}
Expand All @@ -154,7 +116,8 @@ export function Dropdown(props: Props) {
'Dropdown__menuentry',
selected === value && 'selected',
])}
key={index}
id="dropdown-item"
key={value}
onClick={() => {
setOpen(false);
onSelected?.(value);
Expand All @@ -167,70 +130,64 @@ export function Dropdown(props: Props) {
</div>
}
>
<div>
<div className="Dropdown" style={{ width: unit(width) }}>
<div
className={classes([
'Dropdown__control',
'Button',
'Button--dropdown',
'Button--color--' + color,
disabled && 'Button--disabled',
className,
])}
onClick={(event) => {
if (disabled && !open) {
return;
}
setOpen(!open);
onClick?.(event);
<Box className="Dropdown" width={width}>
<div
className={classes([
'Dropdown__control',
'Button',
'Button--dropdown',
'Button--color--' + color,
disabled && 'Button--disabled',
className,
])}
onClick={(event) => {
if (disabled && !open) {
return;
}
setOpen(!open);
onClick?.(event);
}}
>
{icon && (
<Icon mr={1} name={icon} rotation={iconRotation} spin={iconSpin} />
)}
<span
className="Dropdown__selected-text"
style={{
overflow: clipSelectedText ? 'hidden' : 'visible',
}}
>
{icon && (
<Icon
mr={1}
name={icon}
rotation={iconRotation}
spin={iconSpin}
/>
)}
<span
className="Dropdown__selected-text"
style={{
overflow: clipSelectedText ? 'hidden' : 'visible',
}}
>
{displayText || selected}
{displayText || selected}
</span>
{!noChevron && (
<span className="Dropdown__arrow-button">
<Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
</span>
{!noChevron && (
<span className="Dropdown__arrow-button">
<Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
</span>
)}
</div>
{buttons && (
<>
<Button
disabled={disabled}
height={1.8}
icon="chevron-left"
onClick={() => {
updateSelected('previous');
}}
/>

<Button
disabled={disabled}
height={1.8}
icon="chevron-right"
onClick={() => {
updateSelected('next');
}}
/>
</>
)}
</div>
</div>
</Popover>

{buttons && (
<>
<Button
disabled={disabled}
height={1.8}
icon="chevron-left"
onClick={() => {
updateSelected('previous');
}}
/>

<Button
disabled={disabled}
height={1.8}
icon="chevron-right"
onClick={() => {
updateSelected('next');
}}
/>
</>
)}
</Box>
</Popper>
);
}
Loading
Loading