Skip to content

feat(site): add WorkspacesButton component #10011

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

Merged
merged 26 commits into from
Oct 5, 2023
Merged
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f0a2aae
chore: Add OverflowY component
Parkreiner Oct 3, 2023
2420167
chore: Add PopoverContainer component
Parkreiner Oct 3, 2023
88d73af
chore: Add SearchBox
Parkreiner Oct 3, 2023
209eed4
feat: add WorkspacesButton
Parkreiner Oct 3, 2023
9300b11
chore: Install MUI utils package
Parkreiner Oct 3, 2023
2b13f1e
chore: integrate WorkspacesButton
Parkreiner Oct 3, 2023
e86207e
chore: reorganize files
Parkreiner Oct 3, 2023
5b249d5
fix: resolve hover state visual glitch
Parkreiner Oct 3, 2023
b1e1271
chore: Add story for OverflowY
Parkreiner Oct 3, 2023
b367495
fix: remove dynamic name from OverflowY story
Parkreiner Oct 3, 2023
5a34769
chore: update stories again
Parkreiner Oct 3, 2023
b9f6cb8
fix: remove all references to icons (for now)
Parkreiner Oct 3, 2023
1996e2b
refactor: move flex shrink to be OverflowY concern
Parkreiner Oct 4, 2023
5ecbbe6
fix: remove needless render key
Parkreiner Oct 4, 2023
55ab10a
fix: make sure popover closes before navigation
Parkreiner Oct 5, 2023
1a01127
refactor: clean up WorkspacesButton to use more native MUI
Parkreiner Oct 5, 2023
80928c6
Merge branch 'main' into mes/workspace-button-2
Parkreiner Oct 5, 2023
55c6061
fix: update integration into rest of view
Parkreiner Oct 5, 2023
5aea06f
fix: remove JS security concern
Parkreiner Oct 5, 2023
2aee4fe
refactor: parameterize button language
Parkreiner Oct 5, 2023
12ac56f
revert: undo sql/go file change
Parkreiner Oct 5, 2023
99f2656
fix: remove permissions dependency
Parkreiner Oct 5, 2023
430e30c
fix: simplify button prop types
Parkreiner Oct 5, 2023
ff37ab5
fix: lift data dependencies to page component
Parkreiner Oct 5, 2023
1b934ab
refactor: clean up props
Parkreiner Oct 5, 2023
84c6642
fix: update dependencies again for Storybook
Parkreiner Oct 5, 2023
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
Prev Previous commit
Next Next commit
chore: Add PopoverContainer component
  • Loading branch information
Parkreiner committed Oct 3, 2023
commit 24201675bd9837fb5b79cfc1933544407466a8cb
142 changes: 142 additions & 0 deletions site/src/components/WorkspacesButton/PopoverContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
type KeyboardEvent,
type ReactElement,
useEffect,
useRef,
useState,
PropsWithChildren,
} from "react";

import { type Theme, type SystemStyleObject } from "@mui/system";
import Popover, { type PopoverOrigin } from "@mui/material/Popover";

type Props = PropsWithChildren<{
/**
* Does not require any hooks or refs to work. Also does not override any refs
* or event handlers attached to the button.
*/
anchorButton: ReactElement;
width?: number;
originX?: PopoverOrigin["horizontal"];
originY?: PopoverOrigin["vertical"];
sx?: SystemStyleObject<Theme>;
}>;

function getButton(container: HTMLElement) {
return (
container.querySelector("button") ??
container.querySelector('[aria-role="button"]')
);
}

export function PopoverContainer({
children,
anchorButton,
originX = 0,
originY = 0,
width = 320,
sx = {},
}: Props) {
const buttonContainerRef = useRef<HTMLDivElement>(null);

// Ref value is for effects and event listeners; state value is for React
// renders. Have to duplicate state because after the initial render, it's
// never safe to reference ref contents inside a render path, especially with
// React 18 concurrency. Duplication is a necessary evil because of MUI's
// weird, clunky APIs
const anchorButtonRef = useRef<HTMLButtonElement | null>(null);
const [loadedButton, setLoadedButton] = useState<HTMLButtonElement>();

// Makes container listen to changes in button. If this approach becomes
// untenable in the future, it can be replaced with React.cloneElement, but
// the trade-off there is that every single anchorButton will need to be
// wrapped inside React.forwardRef, making the abstraction leak a little more
useEffect(() => {
const buttonContainer = buttonContainerRef.current;
if (buttonContainer === null) {
throw new Error("Please attach container ref to button container");
}

const initialButton = getButton(buttonContainer);
if (initialButton === null) {
throw new Error("Initial ref query failed");
}
anchorButtonRef.current = initialButton;

const onContainerMutation: MutationCallback = () => {
const newButton = getButton(buttonContainer);
if (newButton === null) {
throw new Error("Semantic button removed after DOM update");
}

anchorButtonRef.current = newButton;
setLoadedButton((current) => {
return current === undefined ? undefined : newButton;
});
};

const observer = new MutationObserver(onContainerMutation);
observer.observe(buttonContainer, {
childList: true,
subtree: true,
});

return () => observer.disconnect();
}, []);

// Not using useInteractive because the container element is just meant to
// catch events from the inner button, not act as a button itself
const onInnerButtonInteraction = () => {
if (anchorButtonRef.current === null) {
throw new Error("Usable ref value is unavailable");
}

setLoadedButton(anchorButtonRef.current);
};

const onInnerButtonKeydown = (event: KeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") {
onInnerButtonInteraction();
}
};

return (
<>
{/* Cannot switch with Box component; breaks implementation */}
<div
// Disabling semantics for the container does not affect the button
// placed inside; the button should still be fully accessible
role="none"
tabIndex={-1}
ref={buttonContainerRef}
onClick={onInnerButtonInteraction}
onKeyDown={onInnerButtonKeydown}
// Only style that container should ever need
style={{ width: "fit-content" }}
>
{anchorButton}
</div>

<Popover
open={loadedButton !== undefined}
anchorEl={loadedButton}
onClose={() => setLoadedButton(undefined)}
anchorOrigin={{ horizontal: originX, vertical: originY }}
sx={{
"& .MuiPaper-root": {
overflowY: "hidden",
width,
paddingY: 0,
...sx,
},
}}
transitionDuration={{
enter: 300,
exit: 0,
}}
>
{children}
</Popover>
</>
);
}