{
])}
/>
);
-};
+}
diff --git a/tgui/packages/tgui/components/DraggableControl.jsx b/tgui/packages/tgui/components/DraggableControl.jsx
index 8ada6f2fa4..bb55287b02 100644
--- a/tgui/packages/tgui/components/DraggableControl.jsx
+++ b/tgui/packages/tgui/components/DraggableControl.jsx
@@ -5,8 +5,8 @@
*/
import { clamp } from 'common/math';
-import { pureComponentHooks } from 'common/react';
-import { Component, createRef } from 'inferno';
+import { Component, createRef } from 'react';
+
import { AnimatedNumber } from './AnimatedNumber';
const DEFAULT_UPDATE_RATE = 400;
@@ -97,13 +97,13 @@ export class DraggableControl extends Component {
state.internalValue = clamp(
state.internalValue + (offset * step) / stepPixelSize,
minValue - step,
- maxValue + step
+ maxValue + step,
);
// Clamp the final value
state.value = clamp(
state.internalValue - (state.internalValue % step) + stepOffset,
minValue,
- maxValue
+ maxValue,
);
state.origin = getScalarScreenOffset(e, dragMatrix);
} else if (Math.abs(offset) > 4) {
@@ -196,8 +196,8 @@ export class DraggableControl extends Component {
style={{
display: !editing ? 'none' : undefined,
height: height,
- 'line-height': lineHeight,
- 'font-size': fontSize,
+ lineHeight: lineHeight,
+ fontsize: fontSize,
}}
onBlur={(e) => {
if (!editing) {
@@ -276,7 +276,6 @@ export class DraggableControl extends Component {
}
}
-DraggableControl.defaultHooks = pureComponentHooks;
DraggableControl.defaultProps = {
minValue: -Infinity,
maxValue: +Infinity,
diff --git a/tgui/packages/tgui/components/Dropdown.tsx b/tgui/packages/tgui/components/Dropdown.tsx
index 7fa0e3ae8d..4fabfbf0bb 100644
--- a/tgui/packages/tgui/components/Dropdown.tsx
+++ b/tgui/packages/tgui/components/Dropdown.tsx
@@ -1,386 +1,220 @@
-import { createPopper, VirtualElement } from '@popperjs/core';
import { classes } from 'common/react';
-import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
-import { Box, BoxProps } from './Box';
+import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
+
+import { BoxProps, unit } from './Box';
import { Button } from './Button';
import { Icon } from './Icon';
-import { Stack } from './Stack';
+import { Popper } from './Popper';
-export interface DropdownEntry {
- displayText: string | number | InfernoNode;
- value: string | number | Enumerator;
+type DropdownEntry = {
+ displayText: ReactNode;
+ value: string | number;
+};
+
+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;
+} & 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;
+
+function getOptionValue(option: DropdownOption) {
+ return typeof option === 'string' ? option : option.value;
}
-type DropdownUniqueProps = {
- options: string[] | DropdownEntry[];
- icon?: string;
- iconRotation?: number;
- clipSelectedText?: boolean;
- width?: string;
- menuWidth?: string;
- over?: boolean;
- color?: string;
- nochevron?: boolean;
- displayText?: string | number | InfernoNode;
- onClick?: (event) => void;
- // you freaks really are just doing anything with this shit
- selected?: any;
- onSelected?: (selected: any) => void;
- buttons?: boolean;
-};
+export function Dropdown(props: Props) {
+ const {
+ buttons,
+ className,
+ clipSelectedText = true,
+ color = 'default',
+ disabled,
+ displayText,
+ icon,
+ iconRotation,
+ iconSpin,
+ menuWidth = '15rem',
+ noChevron,
+ onClick,
+ onSelected,
+ options = [],
+ over,
+ selected,
+ width,
+ } = props;
-export type DropdownProps = BoxProps & DropdownUniqueProps;
+ const [open, setOpen] = useState(false);
+ const adjustedOpen = over ? !open : open;
+ const innerRef = useRef
(null);
-const DEFAULT_OPTIONS = {
- placement: 'left-start',
- modifiers: [
- {
- name: 'eventListeners',
- enabled: false,
- },
- ],
-};
-const NULL_RECT: DOMRect = {
- width: 0,
- height: 0,
- top: 0,
- right: 0,
- bottom: 0,
- left: 0,
- x: 0,
- y: 0,
- toJSON: () => null,
-} as const;
-
-type DropdownState = {
- selected?: string;
- open: boolean;
-};
-
-const DROPDOWN_DEFAULT_CLASSNAMES = 'Layout Dropdown__menu';
-const DROPDOWN_SCROLL_CLASSNAMES = 'Layout Dropdown__menu-scroll';
-
-export class Dropdown extends Component {
- static renderedMenu: HTMLDivElement | undefined;
- static singletonPopper: ReturnType | undefined;
- static currentOpenMenu: Element | undefined;
- static virtualElement: VirtualElement = {
- getBoundingClientRect: () =>
- Dropdown.currentOpenMenu?.getBoundingClientRect() ?? NULL_RECT,
- };
- menuContents: any;
- state: DropdownState = {
- open: false,
- selected: this.props.selected,
- };
-
- handleClick = () => {
- if (this.state.open) {
- this.setOpen(false);
- }
- };
-
- getDOMNode() {
- return findDOMfromVNode(this.$LI, true);
- }
-
- componentDidMount() {
- const domNode = this.getDOMNode();
-
- if (!domNode) {
- return;
- }
- }
-
- openMenu() {
- let renderedMenu = Dropdown.renderedMenu;
- if (renderedMenu === undefined) {
- renderedMenu = document.createElement('div');
- renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES;
- document.body.appendChild(renderedMenu);
- Dropdown.renderedMenu = renderedMenu;
- }
-
- const domNode = this.getDOMNode()!;
- Dropdown.currentOpenMenu = domNode;
-
- renderedMenu.scrollTop = 0;
- renderedMenu.style.width =
- this.props.menuWidth ||
- // Hack, but domNode should *always* be the parent control meaning it will have width
- // @ts-ignore
- `${domNode.offsetWidth}px`;
- renderedMenu.style.opacity = '1';
- renderedMenu.style.pointerEvents = 'auto';
-
- // ie hack
- // ie has this bizarre behavior where focus just silently fails if the
- // element being targeted "isn't ready"
- // 400 is probably way too high, but the lack of hotloading is testing my
- // patience on tuning it
- // I'm beyond giving a shit at this point it fucking works whatever
- setTimeout(() => {
- Dropdown.renderedMenu?.focus();
- }, 400);
- this.renderMenuContent();
- }
-
- closeMenu() {
- if (Dropdown.currentOpenMenu !== this.getDOMNode()) {
- return;
- }
-
- Dropdown.currentOpenMenu = undefined;
- Dropdown.renderedMenu!.style.opacity = '0';
- Dropdown.renderedMenu!.style.pointerEvents = 'none';
- }
-
- componentWillUnmount() {
- this.closeMenu();
- this.setOpen(false);
- }
-
- renderMenuContent() {
- const renderedMenu = Dropdown.renderedMenu;
- if (!renderedMenu) {
- return;
- }
- if (renderedMenu.offsetHeight > 200) {
- renderedMenu.className = DROPDOWN_SCROLL_CLASSNAMES;
- } else {
- renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES;
- }
-
- const { options = [] } = this.props;
- const ops = options.map((option) => {
- let value, displayText;
-
- if (typeof option === 'string') {
- displayText = option;
- value = option;
- } else if (option !== null) {
- displayText = option.displayText;
- value = option.value;
+ /** 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;
- return (
- {
- this.setSelected(value);
- }}>
- {displayText}
-
+ let selectedIndex = options.findIndex(
+ (option) => getOptionValue(option) === selected,
);
- });
- const to_render = ops.length ? ops : 'No Options Found';
-
- render({to_render}
, renderedMenu, () => {
- let singletonPopper = Dropdown.singletonPopper;
- if (singletonPopper === undefined) {
- singletonPopper = createPopper(Dropdown.virtualElement, renderedMenu!, {
- ...DEFAULT_OPTIONS,
- placement: 'bottom-start',
- });
-
- Dropdown.singletonPopper = singletonPopper;
- } else {
- singletonPopper.setOptions({
- ...DEFAULT_OPTIONS,
- placement: 'bottom-start',
- });
-
- singletonPopper.update();
+ if (selectedIndex < 0) {
+ selectedIndex = direction === 'next' ? endIndex : startIndex;
}
- });
- }
- setOpen(open: boolean) {
- this.setState((state) => ({
- ...state,
- open,
- }));
- if (open) {
- setTimeout(() => {
- this.openMenu();
- window.addEventListener('click', this.handleClick);
- });
- } else {
- this.closeMenu();
- window.removeEventListener('click', this.handleClick);
- }
- }
+ let newIndex = selectedIndex;
+ if (direction === 'next') {
+ newIndex = selectedIndex === endIndex ? startIndex : selectedIndex++;
+ } else {
+ newIndex = selectedIndex === startIndex ? endIndex : selectedIndex--;
+ }
- setSelected(selected: string) {
- this.setState((state) => ({
- ...state,
- selected,
- }));
- this.setOpen(false);
- if (this.props.onSelected) {
- this.props.onSelected(selected);
- }
- }
+ onSelected?.(getOptionValue(options[newIndex]));
+ },
+ [disabled, onSelected, options, selected],
+ );
- getOptionValue(option): string {
- return typeof option === 'string' ? option : option.value;
- }
+ /** Allows the menu to be scrollable on open */
+ useEffect(() => {
+ if (!open) return;
- getSelectedIndex(): number {
- const selected = this.state.selected || this.props.selected;
- const { options = [] } = this.props;
+ innerRef.current?.focus();
+ }, [open]);
- return options.findIndex((option) => {
- return this.getOptionValue(option) === selected;
- });
- }
+ return (
+ setOpen(false)}
+ placement={over ? 'top-start' : 'bottom-start'}
+ content={
+
+ {options.length === 0 && (
+
No options
+ )}
- toPrevious(): void {
- if (this.props.options.length < 1) {
- return;
- }
+ {options.map((option, index) => {
+ const value = getOptionValue(option);
- let selectedIndex = this.getSelectedIndex();
- const startIndex = 0;
- const endIndex = this.props.options.length - 1;
-
- const hasSelected = selectedIndex >= 0;
- if (!hasSelected) {
- selectedIndex = startIndex;
- }
-
- const previousIndex =
- selectedIndex === startIndex ? endIndex : selectedIndex - 1;
-
- this.setSelected(this.getOptionValue(this.props.options[previousIndex]));
- }
-
- toNext(): void {
- if (this.props.options.length < 1) {
- return;
- }
-
- let selectedIndex = this.getSelectedIndex();
- const startIndex = 0;
- const endIndex = this.props.options.length - 1;
-
- const hasSelected = selectedIndex >= 0;
- if (!hasSelected) {
- selectedIndex = endIndex;
- }
-
- const nextIndex =
- selectedIndex === endIndex ? startIndex : selectedIndex + 1;
-
- this.setSelected(this.getOptionValue(this.props.options[nextIndex]));
- }
-
- render() {
- const { props } = this;
- const {
- icon,
- iconRotation,
- iconSpin,
- clipSelectedText = true,
- color = 'default',
- dropdownStyle,
- over,
- nochevron,
- width,
- onClick,
- onSelected,
- selected,
- disabled,
- displayText,
- buttons,
- ...boxProps
- } = props;
- const { className, ...rest } = boxProps;
-
- const adjustedOpen = over ? !this.state.open : this.state.open;
-
- return (
-
-
- {
+ setOpen(false);
+ onSelected?.(value);
+ }}
+ >
+ {typeof option === 'string' ? option : option.displayText}
+
+ );
+ })}
+
+ }
+ >
+