From 56e578be990844e50ea6dac42a09f5ea24961c23 Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Wed, 29 Mar 2023 14:38:52 -0400 Subject: [PATCH] Replace popper with floating-ui This has the nice benefit of better accessibility, hooray! --- ui/frontend/.eslintrc.js | 1 + ui/frontend/.prettierignore | 1 + ui/frontend/PopButton.module.css | 20 +----- ui/frontend/PopButton.tsx | 105 +++++++++++++++---------------- ui/frontend/package.json | 3 +- ui/frontend/yarn.lock | 69 ++++++++++++-------- 6 files changed, 100 insertions(+), 99 deletions(-) diff --git a/ui/frontend/.eslintrc.js b/ui/frontend/.eslintrc.js index 9ebf7bb0d..197f5b732 100644 --- a/ui/frontend/.eslintrc.js +++ b/ui/frontend/.eslintrc.js @@ -62,6 +62,7 @@ module.exports = { { files: [ '.eslintrc.js', + 'PopButton.tsx', 'editor/AceEditor.tsx', 'editor/SimpleEditor.tsx', 'websocketMiddleware.ts', diff --git a/ui/frontend/.prettierignore b/ui/frontend/.prettierignore index d41bf88d9..d3e1193c4 100644 --- a/ui/frontend/.prettierignore +++ b/ui/frontend/.prettierignore @@ -11,6 +11,7 @@ node_modules # Slowly migrate files that we've touched !.eslintrc.js +!PopButton.tsx !editor/AceEditor.tsx !editor/SimpleEditor.tsx !websocketMiddleware.ts diff --git a/ui/frontend/PopButton.module.css b/ui/frontend/PopButton.module.css index a6d587907..4acbb0800 100644 --- a/ui/frontend/PopButton.module.css +++ b/ui/frontend/PopButton.module.css @@ -1,14 +1,10 @@ $fg-color: #222; $bg-color: white; -$arrow-size: 10px; $vertical-border-color: #cacaca; +$arrow-height: 10px; +$arrow-width: 20px; .container { - /* Prevents the popper from shifting when adding it to the DOM - * triggers showing the scrollbars. - * https://github.com/FezVrasta/popper.js/issues/457#issuecomment-367692177 - */ - top: 0; z-index: 10; font-size: 12px; @@ -17,18 +13,8 @@ $vertical-border-color: #cacaca; } } -.arrow { - border: $arrow-size solid transparent; -} - -.container[data-popper-placement^='bottom'] .arrow { - margin-top: 0; - border-top-width: 0; - border-bottom-color: $bg-color; -} - .content { - margin: $arrow-size; + margin: 0 $arrow-height $arrow-height $arrow-height; box-shadow: 0 1px 4px -2px rgb(0 0 0 / 60%), inset 0 1px 0 white; border-right: 1px solid $vertical-border-color; border-bottom: 1px solid var(--header-accent-border); diff --git a/ui/frontend/PopButton.tsx b/ui/frontend/PopButton.tsx index 28529de8d..7abbc4c40 100644 --- a/ui/frontend/PopButton.tsx +++ b/ui/frontend/PopButton.tsx @@ -1,75 +1,74 @@ -import React, { useCallback, useState, useEffect } from 'react'; -import { usePopper } from 'react-popper'; -import { Portal } from 'react-portal'; +import { + FloatingArrow, + FloatingFocusManager, + arrow, + autoUpdate, + flip, + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + useRole, +} from '@floating-ui/react'; +import React, { useCallback, useRef, useState } from 'react'; import styles from './PopButton.module.css'; interface NewPopProps { - Button: React.ComponentType<{ - toggle: () => void; - } & React.RefAttributes>; + Button: React.ComponentType< + { + toggle: () => void; + } & React.RefAttributes + >; Menu: React.ComponentType<{ close: () => void }>; } const PopButton: React.FC = ({ Button, Menu }) => { - const [isOpen, setOpen] = useState(false); - const toggle = useCallback(() => setOpen(v => !v), []); - const close = useCallback(() => setOpen(false), []); + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => setIsOpen((v) => !v), []); + const close = useCallback(() => setIsOpen(false), []); - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - const [arrowElement, setArrowElement] = useState(null); + const arrowRef = useRef(null); - const { styles: popperStyles, attributes: popperAttributes } = usePopper(referenceElement, popperElement, { - modifiers: [ - { name: 'arrow', options: { element: arrowElement } }, - // Issue #303 - { name: 'computeStyles', options: { gpuAcceleration: false } }, - ], + const { x, y, refs, strategy, context } = useFloating({ + open: isOpen, + onOpenChange: setIsOpen, + middleware: [offset(10), flip(), shift(), arrow({ element: arrowRef })], + whileElementsMounted: autoUpdate, }); - useEffect(() => { - if (!isOpen) { - return; - } + const click = useClick(context); + const dismiss = useDismiss(context); + const role = useRole(context); - const handleClickOutside = (event: MouseEvent) => { - if (!(event.target instanceof Node)) { return; } - - if (referenceElement && referenceElement.contains(event.target)) { - // They are clicking on the button, so let that go ahead and close us. - return; - } - - if (popperElement && !popperElement.contains(event.target)) { - close(); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isOpen, referenceElement, popperElement, close]); + const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]); return ( <> -