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 {SpinningLoader} from "../src/Components/Loaders/SpinningLoader";
|
||||||
import {ListLoader} from "../src/Components/Loaders/ListLoader";
|
import {ListLoader} from "../src/Components/Loaders/ListLoader";
|
||||||
import {GenericLoader} from "../src/Components/Loaders/GenericLoader";
|
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()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
|
@ -23,6 +28,31 @@ export function DemoApp()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={"app"}>
|
<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>
|
<h1>KernelUI</h1>
|
||||||
|
|
||||||
<h2>TODO</h2>
|
<h2>TODO</h2>
|
||||||
|
@ -263,6 +293,25 @@ export function DemoApp()
|
||||||
Sample content.
|
Sample content.
|
||||||
</Card>
|
</Card>
|
||||||
</GenericLoader>
|
</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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, {useCallback, useMemo, useState} from "react";
|
import React, {useCallback, useMemo, useState} from "react";
|
||||||
import {Card} from "../Card";
|
import {Card} from "../Card";
|
||||||
import {
|
import {
|
||||||
|
flip,
|
||||||
shift,
|
shift,
|
||||||
useClick,
|
useClick, useDismiss,
|
||||||
useFloating,
|
useFloating,
|
||||||
useFocus,
|
useFocus,
|
||||||
useHover,
|
useHover,
|
||||||
|
@ -41,6 +42,7 @@ export interface FloatProperties
|
||||||
content?: React.ReactNode|Managed<React.ReactNode>;
|
content?: React.ReactNode|Managed<React.ReactNode>;
|
||||||
className?: string;
|
className?: string;
|
||||||
mode?: FloatingMode;
|
mode?: FloatingMode;
|
||||||
|
dismissible?: boolean;
|
||||||
role?: FloatRole;
|
role?: FloatRole;
|
||||||
floatingOptions?: UseFloatingOptions;
|
floatingOptions?: UseFloatingOptions;
|
||||||
}
|
}
|
||||||
|
@ -48,9 +50,10 @@ export interface FloatProperties
|
||||||
/**
|
/**
|
||||||
* A component to show something floating next to an element.
|
* 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.
|
// By default, use "always" mode.
|
||||||
if (!mode) mode = "always";
|
if (!mode) mode = "always";
|
||||||
|
if (dismissible === undefined && (mode != "always" && mode != "managed")) dismissible = true;
|
||||||
|
|
||||||
// Followed show status.
|
// Followed show status.
|
||||||
const [shown, setShown] = useState(false);
|
const [shown, setShown] = useState(false);
|
||||||
|
@ -68,11 +71,11 @@ export const Float = React.forwardRef(({children, content, className, mode, role
|
||||||
}
|
}
|
||||||
|
|
||||||
// Floating initialization.
|
// Floating initialization.
|
||||||
const { refs, floatingStyles, context } = useFloating(
|
const { refs, floatingStyles, context, placement } = useFloating(
|
||||||
useMemo(() => (Object.assign({
|
useMemo(() => (Object.assign({
|
||||||
open: shown,
|
open: shown,
|
||||||
onOpenChange: setShown,
|
onOpenChange: setShown,
|
||||||
middleware: [shift()],
|
middleware: [shift(), flip()],
|
||||||
} as UseFloatingOptions, floatingOptions)), [floatingOptions, shown, setShown])
|
} 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 hover = useHover(context, useMemo(() => ({ enabled: mode == "hover" }), [mode]));
|
||||||
const focus = useFocus(context, useMemo(() => ({ enabled: mode == "focus", visibleOnly: false }), [mode]));
|
const focus = useFocus(context, useMemo(() => ({ enabled: mode == "focus", visibleOnly: false }), [mode]));
|
||||||
const click = useClick(context, useMemo(() => ({ enabled: mode == "click" }), [mode]));
|
const click = useClick(context, useMemo(() => ({ enabled: mode == "click" }), [mode]));
|
||||||
|
const dismiss = useDismiss(context, useMemo(() => ({ enabled: !!dismissible }), [dismissible]));
|
||||||
const roleProps = useRole(context, {
|
const roleProps = useRole(context, {
|
||||||
role: role,
|
role: role,
|
||||||
enabled: !!role,
|
enabled: !!role,
|
||||||
});
|
});
|
||||||
const {getReferenceProps, getFloatingProps} = useInteractions([roleProps, hover, focus, click]);
|
const {getReferenceProps, getFloatingProps} = useInteractions([roleProps, dismiss, hover, focus, click]);
|
||||||
|
|
||||||
// Transition configuration.
|
// Transition configuration.
|
||||||
const {isMounted, styles: transitionStyles} = useTransitionStyles(context, {
|
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.
|
{ // Showing floating element if the state says to do so.
|
||||||
isMounted &&
|
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)}>
|
<Card style={transitionStyles} className={classes("floating", className)}>
|
||||||
{floatingContent}
|
{floatingContent}
|
||||||
</Card>
|
</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";
|
mode = mode ?? "focus";
|
||||||
|
|
||||||
return (
|
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}
|
{children}
|
||||||
</Float>
|
</Float>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
@blue-darker: #0657C5; --blue-darker: @blue-darker;
|
@blue-darker: #0657C5; --blue-darker: @blue-darker;
|
||||||
@blue-background: #9DC8FF; --blue-background: @blue-background;
|
@blue-background: #9DC8FF; --blue-background: @blue-background;
|
||||||
@blue-background-darker: #7EA9E1; --blue-background-darker: @blue-background-darker;
|
@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-lighter: #E77220; --orange-lighter: @orange-lighter;
|
||||||
@orange: #D06112; --orange: @orange;
|
@orange: #D06112; --orange: @orange;
|
||||||
|
@ -72,9 +73,16 @@
|
||||||
@primary: @blue; --primary: @primary;
|
@primary: @blue; --primary: @primary;
|
||||||
@primary-darker: @blue-darker; --primary-darker: @primary-darker;
|
@primary-darker: @blue-darker; --primary-darker: @primary-darker;
|
||||||
@primary-background: @blue-background; --primary-background: @primary-background;
|
@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-lighter: @orange-lighter; --secondary-lighter: @secondary-lighter;
|
||||||
@secondary: @orange; --secondary: @secondary;
|
@secondary: @orange; --secondary: @secondary;
|
||||||
@secondary-darker: @orange-darker; --secondary-darker: @secondary-darker;
|
@secondary-darker: @orange-darker; --secondary-darker: @secondary-darker;
|
||||||
@secondary-background: @orange-background; --secondary-background: @secondary-background;
|
@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/_link";
|
||||||
@import "components/_list";
|
@import "components/_list";
|
||||||
@import "components/_loaders";
|
@import "components/_loaders";
|
||||||
|
@import "components/_menus";
|
||||||
@import "components/_select";
|
@import "components/_select";
|
||||||
@import "components/_steps";
|
@import "components/_steps";
|
||||||
@import "components/_table";
|
@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