Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
}
},
"dependencies": {
"@dolthub/react-contexts": "^0.3.1",
"@dolthub/react-contexts": "^0.3.2",
"@dolthub/react-hooks": "^0.3.2",
"@dolthub/web-utils": "^0.3.2",
"classnames": "^2.5.1",
Expand Down
49 changes: 49 additions & 0 deletions packages/components/src/Navbar/ForDesktop/NavDropdown.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* Static, so a full-width panel anchors to the header. */
.dropdown {
@apply flex self-stretch items-center;
}

.triggerWrap {
@apply flex self-stretch items-center;
}

.anchor {
@apply relative;
}

.trigger {
@apply relative flex h-full items-center gap-2;
}

/* A pseudo-element, so the box is unchanged and the label does not shift. */
.triggerOpen::after {
@apply absolute inset-x-0 bottom-0 h-0.5 bg-sky-400;
content: "";
}

.caret {
@apply shrink-0 text-sm;
}

.scrim {
@apply absolute left-0 right-0 top-full h-screen bg-[rgba(0,0,0,0.56)];
}

.panel {
@apply absolute left-0 right-0 top-full text-left;
}

/* Must follow `.panel` to win the left/right tie. */
.panelLeft {
@apply right-auto;
}

.panelRight {
@apply left-auto right-0;
}

/* Two classes, to outrank the navbar's `.inner a` / `.left a` styling. */
.dropdown .panel a,
.dropdown .panel button {
@apply mx-0 text-base font-normal tracking-normal text-white hover:text-white;
}
127 changes: 127 additions & 0 deletions packages/components/src/Navbar/ForDesktop/NavDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useOnClickOutside } from "@dolthub/react-hooks";
import { FaCaretDown, FaCaretUp } from "react-icons/fa";
import cx from "classnames";
import React, { ReactNode, useEffect, useRef, useState } from "react";
import Btn from "../../Btn";
import css from "./NavDropdown.module.css";

type Props = {
label: ReactNode;
children: ReactNode;
// "full" spans the page width; "left"/"right" anchor it to the trigger.
align?: "full" | "left" | "right";
hideCaret?: boolean;
underlineOnOpen?: boolean;
// Click still toggles, keeping the menu reachable by keyboard and on touch.
openOnHover?: boolean;
defaultOpen?: boolean;
className?: string;
triggerClassName?: string;
panelClassName?: string;
isOpen?: boolean;
setIsOpen?: (o: boolean) => void;
["data-cy"]?: string;
};

// A nav item whose panel dims the page beneath it. Must be rendered inside
// DesktopNavbar, which a full-width panel anchors to.
export default function NavDropdown({
label,
children,
align = "full",
hideCaret = false,
underlineOnOpen = false,
openOnHover = false,
defaultOpen = false,
isOpen,
setIsOpen,
...props
}: Props) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
const controlled = isOpen !== undefined && setIsOpen !== undefined;
const open = controlled ? isOpen : uncontrolledOpen;
const ref = useRef<HTMLDivElement>(null);

const setOpen = (o: boolean) => {
if (controlled) {
setIsOpen(o);
} else {
setUncontrolledOpen(o);
}
};

useOnClickOutside(ref, () => setOpen(false));

// Delayed, so the pointer can cross the gap to the panel without it closing.
const closeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
const cancelClose = () => {
if (closeTimer.current) {
clearTimeout(closeTimer.current);
closeTimer.current = undefined;
}
};
useEffect(() => cancelClose, []);

// On the wrapper, not the root: the root's scrim would pin the menu open.
const hoverProps = openOnHover
? {
onMouseEnter: () => {
cancelClose();
setOpen(true);
},
onMouseLeave: () => {
cancelClose();
closeTimer.current = setTimeout(() => setOpen(false), 150);
},
}
: {};

return (
<div className={cx(css.dropdown, props.className)} ref={ref}>
{/* Outside the anchor below, so it always covers the full page. */}
{open && (
<div className={css.scrim} aria-hidden onClick={() => setOpen(false)} />
)}
<div
className={cx(css.triggerWrap, { [css.anchor]: align !== "full" })}
{...hoverProps}
>
<Btn
className={cx(
css.trigger,
{ [css.triggerOpen]: underlineOnOpen && open },
props.triggerClassName,
)}
aria-expanded={open}
onClick={() => setOpen(!open)}
data-cy={props["data-cy"]}
>
<span>{label}</span>
{!hideCaret &&
(open ? (
<FaCaretUp className={css.caret} aria-hidden />
) : (
<FaCaretDown className={css.caret} aria-hidden />
))}
</Btn>
{open && (
<div
className={cx(
css.panel,
{
[css.panelLeft]: align === "left",
[css.panelRight]: align === "right",
},
props.panelClassName,
)}
aria-label="nav dropdown panel"
>
{children}
</div>
)}
</div>
</div>
);
}
17 changes: 8 additions & 9 deletions packages/components/src/Navbar/ForDesktop/index.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* `relative` so a dropdown panel anchors here and can span the full width. */
.container {
@apply hidden lg:block w-full text-white pt-2 h-12 z-50 mx-auto pb-2;
@apply hidden lg:block relative w-full text-white h-12 z-50 mx-auto;
}

.inner {
Expand Down Expand Up @@ -27,12 +28,9 @@
}
}

/* `gap`, not margins on `a`: those skipped the div-wrapped dropdown triggers. */
.left {
@apply flex w-1/3 ml-2 order-first;

a {
@apply ml-2 mr-10;
}
@apply flex self-stretch w-1/3 ml-2 order-first items-center gap-12;
}

.logo {
Expand Down Expand Up @@ -62,11 +60,12 @@
}
}

/* Not `relative`: that would make this, not the header, the panel's anchor. */
.logoLeft {
@apply justify-start relative;
@apply justify-start;

.left {
@apply order-2 w-auto ml-6;
@apply order-2 w-auto ml-16;
}

.logo {
Expand All @@ -78,6 +77,6 @@
}

.right {
@apply order-3 w-auto absolute right-0 mr-8;
@apply order-3 w-auto ml-auto mr-8;
}
}
15 changes: 15 additions & 0 deletions packages/components/src/Navbar/ForMobile/NavDropdown.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.dropdown {
@apply flex flex-col items-stretch w-full;
}

.trigger {
@apply w-full justify-between;
}

.caret {
@apply shrink-0 flex px-2 py-1 rounded bg-white/10 text-xl;
}

.panel {
@apply flex flex-col items-stretch w-full border-b border-white/10;
}
61 changes: 61 additions & 0 deletions packages/components/src/Navbar/ForMobile/NavDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FiChevronDown, FiChevronUp } from "react-icons/fi";
import cx from "classnames";
import React, { ReactNode, useState } from "react";
import Btn from "../../Btn";
import css from "./NavDropdown.module.css";

type Props = {
label: ReactNode;
children: ReactNode;
defaultOpen?: boolean;
className?: string;
isOpen?: boolean;
setIsOpen?: (o: boolean) => void;
["data-cy"]?: string;
};

// A nav item that expands in place, pushing the items below it down.
export default function NavDropdown({
label,
children,
defaultOpen = false,
isOpen,
setIsOpen,
...props
}: Props) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
const controlled = isOpen !== undefined && setIsOpen !== undefined;
const open = controlled ? isOpen : uncontrolledOpen;
const toggle = () => {
if (controlled) {
setIsOpen(!isOpen);
} else {
setUncontrolledOpen(!uncontrolledOpen);
}
};

return (
<div className={cx(css.dropdown, props.className)}>
<Btn
className={css.trigger}
aria-expanded={open}
onClick={toggle}
data-cy={props["data-cy"]}
>
<span>{label}</span>
<span className={css.caret} aria-hidden>
{open ? <FiChevronUp /> : <FiChevronDown />}
</span>
</Btn>
{open && (
<div
className={css.panel}
aria-label="nav dropdown panel"
data-nav-panel
>
{children}
</div>
)}
</div>
);
}
49 changes: 39 additions & 10 deletions packages/components/src/Navbar/ForMobile/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
}

.topContainer {
@apply w-full text-white px-5 py-2 h-14;
@apply w-full text-white px-5 py-2 h-[62px] shrink-0;
}

.menuTop {
@apply sticky top-0 z-10;
}

.top {
@apply flex justify-between items-center;
@apply flex h-full justify-between items-center;

button {
@apply text-xl p-2.5 text-white hover:text-white/70;
@apply text-xl p-2 rounded bg-white/10 text-white hover:text-white/70;
}
}

.darkTop {
button {
@apply text-background-acc-1 hover:text-primary;
@apply text-background-acc-1 bg-transparent hover:text-primary;
}
}

Expand All @@ -31,28 +35,53 @@
}

.openMenu {
@apply fixed top-0 bottom-0 left-0 right-0 z-100;
@apply fixed top-0 bottom-0 left-0 right-0 z-100 flex flex-col overflow-y-auto overscroll-contain;
}

/* Descendant, not `> a`: sign-out buttons arrive wrapped in a div. */
.links {
@apply flex flex-col items-center mx-0 mt-10;
@apply flex flex-col items-stretch w-full border-t border-white/10;

a:not([data-nav-panel] *),
button:not([data-nav-panel] *) {
@apply flex items-center h-[60px] px-6 text-base font-semibold tracking-[0.16px] text-white/90 border-b border-white/10 hover:text-sky-100;
}
}

.account {
@apply shrink-0 px-6 pt-5 pb-2;
}

.actions {
@apply mt-auto shrink-0 px-6 pt-8 md:mx-auto md:w-[375px];

a,
button {
@apply text-xl my-4 mx-0 font-semibold tracking-widest text-white/90 hover:text-sky-100;
@apply flex items-center justify-center w-full h-[45px] px-12 rounded border border-white bg-white/10 text-[15px] font-semibold tracking-[0.3px] text-white hover:bg-white/20;
}
}

.pillActions {
a,
button {
@apply rounded-full;
}
}

.bottomLinks {
@apply absolute left-0 right-0 bottom-8 flex justify-between mx-auto px-4 max-w-xs;
@apply mt-auto flex justify-center gap-6 px-6 py-8;

img,
svg {
@apply text-white h-9 w-9;
@apply text-white h-6 w-6;
}
}

.links a {
.actions + .bottomLinks {
@apply mt-0;
}

.links a:not([data-nav-panel] *) {
@apply flex items-center;
> svg {
@apply mr-3;
Expand Down
Loading
Loading