From e6c791bd2c5cc4229ec3d74be8be1ccd67fd2866 Mon Sep 17 00:00:00 2001 From: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:05:22 -0800 Subject: [PATCH] Partially reverts popper component [no gbp] (#80930) ## About The Pull Request As requested by @mothblocks I implemented react-popper instead. Perks: 1. Seemingly handles the positioning bug that I wrestled with in #80883. 2. Performance seems just as good. 3. Should fix issue presented in [NovaSector#434](https://github.com/NovaSector/NovaSector/issues/434) Cons: 1. Tiny amount more code ## Why It's Good For The Game Bug fixes Mothblocks asked for it Fixes #80929 ## Changelog :cl: fix: Poppers (like the prefs menu options) shouldn't be hidden beneath the character preview anymore. /:cl: --- tgui/docs/component-reference.md | 9 +- tgui/packages/tgui/components/Dropdown.tsx | 28 ++---- tgui/packages/tgui/components/Popper.tsx | 96 +++++++++++++++++++ tgui/packages/tgui/components/index.ts | 1 + .../interfaces/PreferencesMenu/MainPage.tsx | 16 ++-- .../interfaces/PreferencesMenu/QuirksPage.tsx | 9 +- .../tgui/interfaces/RequestManager.jsx | 9 +- tgui/packages/tgui/package.json | 2 +- tgui/packages/tgui/stories/Popper.stories.tsx | 58 +++++++++++ tgui/yarn.lock | 36 +++++-- 10 files changed, 214 insertions(+), 50 deletions(-) create mode 100644 tgui/packages/tgui/components/Popper.tsx create mode 100644 tgui/packages/tgui/stories/Popper.stories.tsx diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md index 18a5507d9fe..6084b64082f 100644 --- a/tgui/docs/component-reference.md +++ b/tgui/docs/component-reference.md @@ -735,7 +735,14 @@ 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:** + +- `content: ReactNode` - The content that will be put inside the popper. +- `isOpen: boolean` - Whether or not the popper is open. +- `onClickOutside?: (e) => void` - A function that will be called when the user clicks outside of the popper. +- `placement?: string` - The placement of the popper. See [https://popper.js.org/docs/v2/constructors/#placement] ### `ProgressBar` diff --git a/tgui/packages/tgui/components/Dropdown.tsx b/tgui/packages/tgui/components/Dropdown.tsx index f241c32ba76..4fabfbf0bbf 100644 --- a/tgui/packages/tgui/components/Dropdown.tsx +++ b/tgui/packages/tgui/components/Dropdown.tsx @@ -1,10 +1,10 @@ import { classes } from 'common/react'; import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; -import { Popover } from 'react-tiny-popover'; import { BoxProps, unit } from './Box'; import { Button } from './Button'; import { Icon } from './Icon'; +import { Popper } from './Popper'; type DropdownEntry = { displayText: ReactNode; @@ -74,8 +74,7 @@ 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(null); @@ -108,21 +107,6 @@ export function Dropdown(props: Props) { [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; @@ -131,14 +115,14 @@ export function Dropdown(props: Props) { }, [open]); return ( - setOpen(false)} - positions={over ? 'top' : 'bottom'} + placement={over ? 'top-start' : 'bottom-start'} content={
{options.length === 0 && ( @@ -231,6 +215,6 @@ export function Dropdown(props: Props) { )}
-
+ ); } diff --git a/tgui/packages/tgui/components/Popper.tsx b/tgui/packages/tgui/components/Popper.tsx new file mode 100644 index 00000000000..dbc7c18f7b6 --- /dev/null +++ b/tgui/packages/tgui/components/Popper.tsx @@ -0,0 +1,96 @@ +import { Placement } from '@popperjs/core'; +import { + PropsWithChildren, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; +import { usePopper } from 'react-popper'; + +type RequiredProps = { + /** The content to display in the popper */ + content: ReactNode; + /** Whether the popper is open */ + isOpen: boolean; +}; + +type OptionalProps = Partial<{ + /** Called when the user clicks outside the popper */ + onClickOutside: () => void; + /** Where to place the popper relative to the reference element */ + placement: Placement; +}>; + +type Props = RequiredProps & OptionalProps; + +/** + * ## Popper + * Popper lets you position elements so that they don't go out of the bounds of the window. + * @url https://popper.js.org/react-popper/ for more information. + */ +export function Popper(props: PropsWithChildren) { + const { children, content, isOpen, onClickOutside, placement } = props; + + const [referenceElement, setReferenceElement] = + useState(null); + const [popperElement, setPopperElement] = useState( + null, + ); + + // One would imagine we could just use useref here, but it's against react-popper documentation and causes a positioning bug + // We still need them to call focus and clickoutside events :( + const popperRef = useRef(null); + const parentRef = useRef(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + }); + + /** Close the popper when the user clicks outside */ + function handleClickOutside(event: MouseEvent) { + if ( + !popperRef.current?.contains(event.target as Node) && + !parentRef.current?.contains(event.target as Node) + ) { + onClickOutside?.(); + } + } + + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + return ( + <> +
{ + setReferenceElement(node); + parentRef.current = node; + }} + > + {children} +
+ {isOpen && ( +
{ + setPopperElement(node); + popperRef.current = node; + }} + style={{ ...styles.popper, zIndex: 5 }} + {...attributes.popper} + > + {content} +
+ )} + + ); +} diff --git a/tgui/packages/tgui/components/index.ts b/tgui/packages/tgui/components/index.ts index 3b8abd8a44c..1a5f477d256 100644 --- a/tgui/packages/tgui/components/index.ts +++ b/tgui/packages/tgui/components/index.ts @@ -34,6 +34,7 @@ export { MenuBar } from './MenuBar'; export { Modal } from './Modal'; export { NoticeBox } from './NoticeBox'; export { NumberInput } from './NumberInput'; +export { Popper } from './Popper'; export { ProgressBar } from './ProgressBar'; export { RestrictedInput } from './RestrictedInput'; export { RoundGauge } from './RoundGauge'; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx index 08b4fac9c9c..8815037b62e 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx @@ -1,7 +1,6 @@ import { filterMap, sortBy } from 'common/collections'; import { classes } from 'common/react'; import { useState } from 'react'; -import { Popover } from 'react-tiny-popover'; import { sendAct, useBackend } from '../../backend'; import { @@ -10,6 +9,7 @@ import { Button, Flex, LabeledList, + Popper, Stack, } from '../../components'; import { CharacterPreview } from '../common/CharacterPreview'; @@ -193,10 +193,10 @@ const GenderButton = (props: { const [genderMenuOpen, setGenderMenuOpen] = useState(false); return ( - setGenderMenuOpen(false)} - positions="right" + placement="right-end" content={ {[Gender.Male, Gender.Female, Gender.Other, Gender.Other2].map( @@ -230,7 +230,7 @@ const GenderButton = (props: { tooltip="Gender" tooltipPosition="top" /> - + ); }; @@ -263,10 +263,10 @@ const MainFeature = (props: { const supplementalFeature = catalog.supplemental_feature; return ( - handleClose()} + )} - + ); }; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx index d9773fb3ced..e7e2290e3cf 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx @@ -1,9 +1,8 @@ import { filterMap } from 'common/collections'; import { useState } from 'react'; -import { Popover } from 'react-tiny-popover'; import { useBackend } from '../../backend'; -import { Box, Button, Icon, Stack, Tooltip } from '../../components'; +import { Box, Button, Icon, Popper, Stack, Tooltip } from '../../components'; import { PreferencesMenuData, Quirk, RandomSetting, ServerData } from './data'; import { getRandomization, PreferenceList } from './MainPage'; import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; @@ -212,8 +211,8 @@ function QuirkPopper(props: QuirkPopperProps) { Object.entries(customization_options).length > 0; return ( - setCustomizationExpanded(false)} isOpen={customizationExpanded} content={ @@ -274,7 +273,7 @@ function QuirkPopper(props: QuirkPopperProps) { /> )} - + ); } diff --git a/tgui/packages/tgui/interfaces/RequestManager.jsx b/tgui/packages/tgui/interfaces/RequestManager.jsx index 25e2b25885a..b12d8285e80 100644 --- a/tgui/packages/tgui/interfaces/RequestManager.jsx +++ b/tgui/packages/tgui/interfaces/RequestManager.jsx @@ -5,10 +5,9 @@ */ import { decodeHtmlEntities } from 'common/string'; import { useState } from 'react'; -import { Popover } from 'react-tiny-popover'; import { useBackend, useLocalState } from '../backend'; -import { Button, Input, Section, Table } from '../components'; +import { Button, Input, Popper, Section, Table } from '../components'; import { Window } from '../layouts'; export const RequestManager = (props) => { @@ -140,8 +139,8 @@ const FilterPanel = (props) => { ); return ( - { Type Filter - + ); }; diff --git a/tgui/packages/tgui/package.json b/tgui/packages/tgui/package.json index cf427ffa98b..03b997b7d2a 100644 --- a/tgui/packages/tgui/package.json +++ b/tgui/packages/tgui/package.json @@ -15,7 +15,7 @@ "marked": "^4.2.12", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-tiny-popover": "^8.0.4", + "react-popper": "^2.3.0", "tgui-dev-server": "workspace:*", "tgui-polyfill": "workspace:*" } diff --git a/tgui/packages/tgui/stories/Popper.stories.tsx b/tgui/packages/tgui/stories/Popper.stories.tsx new file mode 100644 index 00000000000..cb7fd6634d7 --- /dev/null +++ b/tgui/packages/tgui/stories/Popper.stories.tsx @@ -0,0 +1,58 @@ +import { Box, Popper } from '../components'; + +export const meta = { + title: 'Popper', + render: () => , +}; + +const Story = () => { + return ( + <> + + Loogatme! + + } + placement="bottom" + > + + + + + I am on the right! + + } + placement="right" + > + + + + ); +}; diff --git a/tgui/yarn.lock b/tgui/yarn.lock index 69ce37ea86b..93f3307eafd 100644 --- a/tgui/yarn.lock +++ b/tgui/yarn.lock @@ -6632,7 +6632,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -7762,6 +7762,13 @@ __metadata: languageName: node linkType: hard +"react-fast-compare@npm:^3.0.1": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 + languageName: node + linkType: hard + "react-is@npm:^16.13.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -7776,13 +7783,17 @@ __metadata: languageName: node linkType: hard -"react-tiny-popover@npm:^8.0.4": - version: 8.0.4 - resolution: "react-tiny-popover@npm:8.0.4" +"react-popper@npm:^2.3.0": + version: 2.3.0 + resolution: "react-popper@npm:2.3.0" + dependencies: + react-fast-compare: ^3.0.1 + warning: ^4.0.2 peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 1c7bcd0227e616326240cc97f112def0cd6e6edc94a56f763b20e82bf9ef1efdcc3e9bfff4bc9417a16e948aa3f2cfb960504135760b8f9e55fc73851d177bc3 + "@popperjs/core": ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: 837111c98738011c69b3069a464ea5bdcbf487105b6148e8faf90cb7337e134edb1b98b8824322941c378756cca30a15c18c25f558e53b85ed5762fa0dc8e6b2 languageName: node linkType: hard @@ -9162,7 +9173,7 @@ resolve@^1.20.0: marked: ^4.2.12 react: ^18.2.0 react-dom: ^18.2.0 - react-tiny-popover: ^8.0.4 + react-popper: ^2.3.0 tgui-dev-server: "workspace:*" tgui-polyfill: "workspace:*" languageName: unknown @@ -9673,6 +9684,15 @@ resolve@^1.20.0: languageName: node linkType: hard +"warning@npm:^4.0.2": + version: 4.0.3 + resolution: "warning@npm:4.0.3" + dependencies: + loose-envify: ^1.0.0 + checksum: 4f2cb6a9575e4faf71ddad9ad1ae7a00d0a75d24521c193fa464f30e6b04027bd97aa5d9546b0e13d3a150ab402eda216d59c1d0f2d6ca60124d96cd40dfa35c + languageName: node + linkType: hard + "watchpack@npm:^2.4.0": version: 2.4.0 resolution: "watchpack@npm:2.4.0"