Skip to content

Commit

Permalink
Add one more level to header dropdown (#107)
Browse files Browse the repository at this point in the history
* Add popout menu choice to sidebar, default to without it.

* Sidebar was mislabeled

* Update to use react-icons/fa to match previous,

* Add popout menu documentation and use it in the sidebar page

* Add example without popout menu.

Fix container to not have "fluid" in example

* Fix badge text to match example (remove fluid)

* Create Dropdown Component (undocumented yet) for use with sidebar mobile.

Use tailwindui+tailwindcss/forms

* Expose ref for usacebox (ref is not an attribute)

* Create custom hook for determining if in mobile state

* Bring everything together in the previous commits to dynamically change the sidebar to turn into a dropdown when below the `md` breakpoint used for hiding sidebar.

Do this in such a way that end users do not have to update their source. (i.e. use javascript to target parent elements)

* # Make it so nested links are more obvious in the dropdown on mobile.

Add extra children to example links for sidebar docs.

Create flattenlinks utility to recursively place all links in the same parent array and provide a path/level for them.

* Formatting

* Add nesting to popout menus and sidebar without popout.

* Limit popout nesting to 2 menus + 1 (with errors)

* Add id attribute for #94

* Add one more level in the dropdown for the header
  • Loading branch information
krowvin authored Oct 3, 2024
1 parent 09c1d0b commit 2107cd5
Show file tree
Hide file tree
Showing 15 changed files with 870 additions and 68 deletions.
104 changes: 104 additions & 0 deletions lib/components/buttons/popover.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { useEffect, useState } from "react";
import {
VscChevronRight,
VscChevronLeft,
VscChevronUp,
VscChevronDown,
} from "react-icons/vsc";

const AVAIL_DIRECTIONS = ["right", "left", "top", "bottom"];

function PopoutMenu({ title, children, direction = "right", className }) {
direction = direction.toLowerCase();

const [isOpen, setIsOpen] = useState(false);

// TODO: This does not handle clicking other popout buttons
function handleClickOutside() {
// Handle when a user clicks outside the popout menu
setIsOpen(false);
}

useEffect(() => {
if (isOpen) {
document.addEventListener("click", handleClickOutside);
} else {
document.removeEventListener("click", handleClickOutside);
}

return () => {
document.removeEventListener("click", handleClickOutside);
};
}, [isOpen]);

if (!AVAIL_DIRECTIONS.includes(direction)) {
throw new Error(
`Invalid direction in component <Popover direction="${direction}". Valid directions are: ${AVAIL_DIRECTIONS.join(
", "
)}`
);
}

// Map direction to position classes
const directionClasses = {
right:
"gw-left-full gw-top-1/2 gw-transform gw--translate-y-1/2 gw-translate-x-2",
left: "gw-right-full gw-top-1/2 gw-transform gw--translate-y-1/2 gw--translate-x-2",
top: "gw-bottom-full gw-left-1/2 gw-transform gw--translate-x-1/2 gw--translate-y-2",
bottom:
"gw-top-full gw-left-1/2 gw-transform gw--translate-x-1/2 gw-translate-y-2",
};

/// Map the icons to their appropriate directions
const ChevronIcon = {
right: VscChevronRight,
left: VscChevronLeft,
top: VscChevronUp,
bottom: VscChevronDown,
}[direction];

// Flip the direction for closure
const ChevronIconOpposite = {
right: VscChevronLeft,
left: VscChevronRight,
top: VscChevronDown,
bottom: VscChevronUp,
}[direction];

return (
<Popover
name="gw-popout-menu"
className={`gw-relative gw-cursor-not-allowed gw-select-none ${className}`}
onClose={() => setIsOpen(false)}
>
<PopoverButton
className="gw-inline-flex gw-w-full gw-items-center gw-justify-between gw-text-sm gw-leading-6 gw-ps-1 focus:gw-outline-none"
onClick={() => {
setIsOpen(!isOpen);
}}
>
<span>{title}</span>
{isOpen ? (
<ChevronIconOpposite
aria-hidden="true"
className="gw-h-5 gw-w-5"
size={16}
/>
) : (
<ChevronIcon aria-hidden="true" className="gw-h-5 gw-w-5" size={12} />
)}
</PopoverButton>

<PopoverPanel
transition="true"
className={`gw-absolute gw-ms-2 gw-pt-1 ${directionClasses[direction]} gw-z-10 gw-mt-2 gw-w-56 gw-shrink gw-rounded-xl gw-bg-white gw-text-sm gw-leading-6 gw-text-gray-900 gw-shadow-lg gw-ring-1 gw-ring-gray-900/5`}
>
{children}
</PopoverPanel>
</Popover>
);
}

export default PopoutMenu;
export { PopoutMenu };
37 changes: 37 additions & 0 deletions lib/components/form/dropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useState } from "react";

export default function Dropdown({
label,
options,
labelClassName,
className,
...props
}) {
const [selectedValue, setSelectedValue] = useState(options[0].props.value);
if (!options || options.length === 0) {
throw new Error(`Dropdown ${label} must have at least one option`);
}
return (
<>
<label
htmlFor={label + "-id"}
className={`block text-sm font-medium leading-6 text-gray-900 ${labelClassName}`}
>
{label}
</label>
<select
id={label ? label + "-id" : undefined}
name={label ? label + "-name" : undefined}
onChange={(e) => {
setSelectedValue(e.target.value);
props.onChange(e);
}}
className={`mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6 ${className}`}
value={selectedValue || options[0].props.value}
{...props}
>
{options}
</select>
</>
);
}
15 changes: 10 additions & 5 deletions lib/components/layout/usace-box.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useMemo } from "react";
import gwMerge from "../../gw-merge";

function UsaceBox({ children, className, title, customTitle }) {
function UsaceBox({
children,
className,
title,
customTitle,
id,
propRef: ref,
}) {
const CustomTitle = customTitle;
const boxClass = "gw-mb-10 gw-relative";
const beforeClass =
Expand All @@ -13,10 +20,8 @@ function UsaceBox({ children, className, title, customTitle }) {
return gwMerge(boxClass, beforeClass, afterClass, className);
}, [className]);
return (
<div className={usaceBoxClass}>
<h2
className="gw-font-bold gw-mb-5 gw-text-[1.3rem]"
>
<div ref={ref} className={usaceBoxClass} id={id}>
<h2 className="gw-font-bold gw-mb-5 gw-text-[1.3rem]">
{title}
{CustomTitle && <CustomTitle />}
</h2>
Expand Down
47 changes: 39 additions & 8 deletions lib/composite/header/navbar-links.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,51 @@ function NavbarLinkItem({ link, ...props }) {
<Menu.Items
static
as="ul"
className="gw-absolute gw-left-0 gw-top-13 gw-width-auto gw-bg-nav-dark-gray !gw-z-20"
className="gw-absolute gw-left-0 gw-top-13 gw-bg-nav-dark-gray !gw-z-20 gw-w-max gw-p-0" // Removed padding and added gw-p-0
>
{link.children.map((child) => {
return (
<Menu.Item key={child.id || child.text} as={Fragment}>
{link.children.map((child) => (
<Menu.Item key={child.id || child.text} as={Fragment}>
{child.children ? (
<div className="gw-relative gw-group">
<a
href={child.href}
className="after:gw-content-['►'] after:gw-ml-2 after:gw-text-[10px] gw-block gw-text-sm gw-border-b gw-border-nav-black gw-bg-nav-dark-gray gw-hover:gw-bg-nav-translucent-gray gw-text-nav-light-gray gw-hover:gw-text-white gw-text-nowrap gw-font-semibold gw-px-3 gw-py-2 gw-bg-none"
>
{child.text}
</a>
<Menu.Items
static
as="ul"
className="gw-absolute gw-left-full gw-top-0 gw-bg-nav-dark-gray !gw-z-30 gw-w-max gw-p-0 gw-shadow-lg gw-hidden group-hover:gw-block" // Removed padding and added gw-p-0
>
{child.children.map((grandChild) => {
if (!grandChild.children)
console.warn(
"Header items can only be 2 levels deep. Please reorganize your header links. This helps to avoid CSS issues."
);
return (
<Menu.Item key={grandChild.id || grandChild.text}>
<a
href={grandChild.href}
className="gw-block gw-text-sm gw-border-b gw-border-nav-black gw-bg-nav-dark-gray gw-hover:gw-bg-nav-translucent-gray gw-text-nav-light-gray gw-hover:gw-text-white gw-text-nowrap gw-font-semibold gw-px-3 gw-py-2 gw-bg-none"
>
{grandChild.text}
</a>
</Menu.Item>
);
})}
</Menu.Items>
</div>
) : (
<a
href={child.href}
className="gw-block gw-text-sm gw-border-b gw-border-nav-black gw-bg-nav-dark-gray gw-hover:gw-bg-nav-translucent-gray gw-text-nav-light-gray gw-hover:gw-text-white gw-text-nowrap gw-font-semibold gw-px-[16px] gw-py-[8px] gw-bg-none"
className="gw-block gw-text-sm gw-border-b gw-border-nav-black gw-bg-nav-dark-gray gw-hover:gw-bg-nav-translucent-gray gw-text-nav-light-gray gw-hover:gw-text-white gw-text-nowrap gw-font-semibold gw-px-3 gw-py-2 gw-bg-none"
>
{child.text}
</a>
</Menu.Item>
);
})}
)}
</Menu.Item>
))}
</Menu.Items>
)}
</Menu>
Expand Down
Loading

0 comments on commit 2107cd5

Please sign in to comment.