Basic main menus and submenus, floating elements improvements.

This commit is contained in:
Madeorsk 2024-07-07 13:15:28 +02:00
parent bc79307ff7
commit 97cf06bc7f
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
13 changed files with 304 additions and 7 deletions

View file

@ -14,6 +14,11 @@ import {Select} from "../src/Components/Select/Select";
import {SpinningLoader} from "../src/Components/Loaders/SpinningLoader";
import {ListLoader} from "../src/Components/Loaders/ListLoader";
import {GenericLoader} from "../src/Components/Loaders/GenericLoader";
import {MainMenu} from "../src/Components/Menus/MainMenu";
import {SubmenuFloat} from "../src/Components/Menus/SubmenuFloat";
import {Submenu} from "../src/Components/Menus/Submenu";
import {SubmenuItem, SubmenuItemSubmenu} from "../src/Components/Menus/SubmenuItem";
import {MainMenuItem, MainMenuItemSubmenu} from "../src/Components/Menus/MainMenuItem";
export function DemoApp()
{
@ -23,6 +28,31 @@ export function DemoApp()
return (
<main className={"app"}>
<MainMenu>
<MainMenuItem href={"/"}>Home</MainMenuItem>
<MainMenuItem href={"#"}>Test</MainMenuItem>
<MainMenuItemSubmenu submenu={
<Submenu>
<SubmenuItem>Test 1</SubmenuItem>
<SubmenuItem>Test 2</SubmenuItem>
<SubmenuItemSubmenu submenu={
<Submenu>
<SubmenuItem>Test A</SubmenuItem>
<SubmenuItem>Test B</SubmenuItem>
<SubmenuItemSubmenu submenu={
<Submenu>
<SubmenuItem href={"#first-last-choice"}>First last choice</SubmenuItem>
<SubmenuItem>Another last choice</SubmenuItem>
</Submenu>
}>Submenu in submenu</SubmenuItemSubmenu>
</Submenu>
}>Submenu</SubmenuItemSubmenu>
</Submenu>
}>
Submenu
</MainMenuItemSubmenu>
</MainMenu>
<h1>KernelUI</h1>
<h2>TODO</h2>
@ -263,6 +293,25 @@ export function DemoApp()
Sample content.
</Card>
</GenericLoader>
<h2>Menus</h2>
<SubmenuFloat submenu={
<Submenu>
<SubmenuItem>Test 1</SubmenuItem>
<SubmenuItem>Test 2</SubmenuItem>
<SubmenuItemSubmenu submenu={
<Submenu>
<SubmenuItem>Test A</SubmenuItem>
<SubmenuItem>Test B</SubmenuItem>
</Submenu>
}>
Submenu
</SubmenuItemSubmenu>
</Submenu>
} floatingOptions={{ placement: "right-start" }}>
<button>Submenu on a button</button>
</SubmenuFloat>
</main>
);
}

View file

@ -1,8 +1,9 @@
import React, {useCallback, useMemo, useState} from "react";
import {Card} from "../Card";
import {
flip,
shift,
useClick,
useClick, useDismiss,
useFloating,
useFocus,
useHover,
@ -41,6 +42,7 @@ export interface FloatProperties
content?: React.ReactNode|Managed<React.ReactNode>;
className?: string;
mode?: FloatingMode;
dismissible?: boolean;
role?: FloatRole;
floatingOptions?: UseFloatingOptions;
}
@ -48,9 +50,10 @@ export interface FloatProperties
/**
* A component to show something floating next to an element.
*/
export const Float = React.forwardRef(({children, content, className, mode, role, floatingOptions}: FloatProperties, ref): React.ReactElement => {
export const Float = React.forwardRef(({children, content, className, mode, dismissible, role, floatingOptions}: FloatProperties, ref): React.ReactElement => {
// By default, use "always" mode.
if (!mode) mode = "always";
if (dismissible === undefined && (mode != "always" && mode != "managed")) dismissible = true;
// Followed show status.
const [shown, setShown] = useState(false);
@ -68,11 +71,11 @@ export const Float = React.forwardRef(({children, content, className, mode, role
}
// Floating initialization.
const { refs, floatingStyles, context } = useFloating(
const { refs, floatingStyles, context, placement } = useFloating(
useMemo(() => (Object.assign({
open: shown,
onOpenChange: setShown,
middleware: [shift()],
middleware: [shift(), flip()],
} as UseFloatingOptions, floatingOptions)), [floatingOptions, shown, setShown])
);
@ -80,11 +83,12 @@ export const Float = React.forwardRef(({children, content, className, mode, role
const hover = useHover(context, useMemo(() => ({ enabled: mode == "hover" }), [mode]));
const focus = useFocus(context, useMemo(() => ({ enabled: mode == "focus", visibleOnly: false }), [mode]));
const click = useClick(context, useMemo(() => ({ enabled: mode == "click" }), [mode]));
const dismiss = useDismiss(context, useMemo(() => ({ enabled: !!dismissible }), [dismissible]));
const roleProps = useRole(context, {
role: role,
enabled: !!role,
});
const {getReferenceProps, getFloatingProps} = useInteractions([roleProps, hover, focus, click]);
const {getReferenceProps, getFloatingProps} = useInteractions([roleProps, dismiss, hover, focus, click]);
// Transition configuration.
const {isMounted, styles: transitionStyles} = useTransitionStyles(context, {
@ -132,7 +136,7 @@ export const Float = React.forwardRef(({children, content, className, mode, role
{ // Showing floating element if the state says to do so.
isMounted &&
<div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} className={"floating"}>
<div ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} className={"floating"} data-placement={placement}>
<Card style={transitionStyles} className={classes("floating", className)}>
{floatingContent}
</Card>

View file

@ -0,0 +1,16 @@
import React from "react";
import {classes} from "../../Utils";
/**
* Main component of a main menu.
*/
export function MainMenu({children, className, ...props}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>)
{
return (
<nav className={classes("main", "menu", className)} {...props}>
<ul>
{children}
</ul>
</nav>
);
}

View file

@ -0,0 +1,35 @@
import React from "react";
import {classes, Modify} from "../../Utils";
import {SubmenuFloat} from "./SubmenuFloat";
export type MainMenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, {
}>>;
/**
* Main component of a main menu item.
*/
export const MainMenuItem = React.forwardRef<HTMLAnchorElement, MainMenuItemProperties>(function SubmenuItem({children, ...props}: MainMenuItemProperties, ref)
{
return (
<li>
<a ref={ref} {...props}>{children}</a>
</li>
);
});
/**
* A main menu item that open a submenu.
*/
export function MainMenuItemSubmenu({submenu, className, children, ...props}: Modify<MainMenuItemProperties, {
/**
* The submenu content.
*/
submenu: React.ReactNode;
}>)
{
return (
<SubmenuFloat submenu={submenu} role={"menu"} floatingOptions={{ placement: "bottom-start" }}>
<MainMenuItem className={classes("submenu", className)} {...props}>{children}</MainMenuItem>
</SubmenuFloat>
);
}

View file

@ -0,0 +1,14 @@
import React from "react";
import {classes} from "../../Utils";
/**
* Main component of a submenu.
*/
export function Submenu({className, children, ...props}: React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>)
{
return (
<div className={classes("submenu", className)} {...props}>
{children}
</div>
);
}

View file

@ -0,0 +1,24 @@
import React from "react";
import {Float, FloatProperties} from "../Floating/Float";
import {classes, Modify} from "../../Utils";
/**
* Add a submenu which opens on click on the child.
*/
export function SubmenuFloat({submenu, className, mode, children, content, ...props}: Modify<FloatProperties, {
/**
* The submenu content.
*/
submenu: React.ReactNode;
// Ignored overridden properties.
content?: never;
}>)
{
return (
<Float mode={mode ?? "click"} className={classes("submenu", className)}
content={submenu} {...props}>
{children}
</Float>
);
}

View file

@ -0,0 +1,33 @@
import React from "react";
import {classes, Modify} from "../../Utils";
import {SubmenuFloat} from "./SubmenuFloat";
export type SubmenuItemProperties = React.PropsWithChildren<Modify<React.AnchorHTMLAttributes<HTMLAnchorElement>, {
}>>;
/**
* Main component of a submenu item.
*/
export const SubmenuItem = React.forwardRef<HTMLAnchorElement, SubmenuItemProperties>(function SubmenuItem({className, children, ...props}: SubmenuItemProperties, ref)
{
return (
<a ref={ref} className={classes("item", className)} {...props}>{children}</a>
);
});
/**
* A submenu item that open a submenu.
*/
export function SubmenuItemSubmenu({submenu, className, children, ...props}: Modify<SubmenuItemProperties, {
/**
* The submenu content.
*/
submenu: React.ReactNode;
}>)
{
return (
<SubmenuFloat submenu={submenu} role={"menu"} floatingOptions={{ placement: "right-start" }}>
<SubmenuItem className={classes("submenu", className)} {...props}>{children}</SubmenuItem>
</SubmenuFloat>
);
}

View file

@ -16,7 +16,7 @@ export function Suggestible({className, suggestions, mode, content, role, childr
mode = mode ?? "focus";
return (
<Float className={classes("suggestions", className)} role={"select"} content={suggestions} mode={mode} {...props}>
<Float className={classes("suggestions", className)} role={"select"} dismissible={false} content={suggestions} mode={mode} {...props}>
{children}
</Float>
);

View file

@ -35,6 +35,7 @@
@blue-darker: #0657C5; --blue-darker: @blue-darker;
@blue-background: #9DC8FF; --blue-background: @blue-background;
@blue-background-darker: #7EA9E1; --blue-background-darker: @blue-background-darker;
@blue-gradient: linear-gradient(33deg, @blue 0%, @blue-lighter 100%), @blue; --blue-gradient: @blue-gradient;
@orange-lighter: #E77220; --orange-lighter: @orange-lighter;
@orange: #D06112; --orange: @orange;
@ -72,9 +73,16 @@
@primary: @blue; --primary: @primary;
@primary-darker: @blue-darker; --primary-darker: @primary-darker;
@primary-background: @blue-background; --primary-background: @primary-background;
@primary-background-darker: @blue-background-darker; --primary-background-darker: @primary-background-darker;
@primary-gradient: @blue-gradient; --primary-gradient: @primary-gradient;
@secondary-lighter: @orange-lighter; --secondary-lighter: @secondary-lighter;
@secondary: @orange; --secondary: @secondary;
@secondary-darker: @orange-darker; --secondary-darker: @secondary-darker;
@secondary-background: @orange-background; --secondary-background: @secondary-background;
@secondary-background-darker: @orange-background-darker; --secondary-background-darker: @secondary-background-darker;
@menu-hover: rgba(255, 255, 255, 0.15); --menu-hover: @menu-hover;
}

View file

@ -8,6 +8,7 @@
@import "components/_link";
@import "components/_list";
@import "components/_loaders";
@import "components/_menus";
@import "components/_select";
@import "components/_steps";
@import "components/_table";

View file

@ -0,0 +1,2 @@
@import "menus/_main-menu";
@import "menus/_submenu";

View file

@ -0,0 +1,74 @@
nav.main.menu
{
background: var(--primary-gradient);
color: var(--background);
.floating
{
justify-content: flex-start;
}
.floating[data-placement="left-start"]
{
justify-content: flex-end;
}
> ul
{
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
margin: 0;
padding: 0;
list-style: none;
> li
{
margin: 0;
padding: 0;
> a
{
transition: background 0.2s ease;
display: block;
padding: 0.75em 1em;
background: transparent;
color: var(--background);
text-align: center;
text-decoration: none;
cursor: pointer;
&:hover
{
background: var(--menu-hover);
}
}
}
}
.submenu.card
{
box-shadow: 0 0 0.15em 0 var(--foreground-shadow);
border: none;
background: var(--primary-gradient);
.submenu
{
> .item
{
color: var(--background);
&:hover
{
background: var(--menu-hover);
}
}
}
}
}

View file

@ -0,0 +1,37 @@
.submenu.card
{
margin: -1px!important;
padding: 0;
.floating
{
justify-content: flex-start;
}
.floating[data-placement="left-start"]
{
justify-content: flex-end;
}
.submenu
{
display: flex;
flex-direction: column;
> .item
{
transition: background 0.2s ease;
padding: 0.75em;
color: var(--foreground);
text-decoration: none;
cursor: pointer;
&:hover
{
background: var(--background-darker);
}
}
}
}