Basic main menus and submenus, floating elements improvements.
This commit is contained in:
parent
bc79307ff7
commit
97cf06bc7f
13 changed files with 304 additions and 7 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
16
src/Components/Menus/MainMenu.tsx
Normal file
16
src/Components/Menus/MainMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
35
src/Components/Menus/MainMenuItem.tsx
Normal file
35
src/Components/Menus/MainMenuItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
14
src/Components/Menus/Submenu.tsx
Normal file
14
src/Components/Menus/Submenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
24
src/Components/Menus/SubmenuFloat.tsx
Normal file
24
src/Components/Menus/SubmenuFloat.tsx
Normal 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>
|
||||
);
|
||||
}
|
33
src/Components/Menus/SubmenuItem.tsx
Normal file
33
src/Components/Menus/SubmenuItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
2
src/styles/components/_menus.less
Normal file
2
src/styles/components/_menus.less
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import "menus/_main-menu";
|
||||
@import "menus/_submenu";
|
74
src/styles/components/menus/_main-menu.less
Normal file
74
src/styles/components/menus/_main-menu.less
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
src/styles/components/menus/_submenu.less
Normal file
37
src/styles/components/menus/_submenu.less
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue