diff --git a/dist/style.scss b/dist/style.scss index 44e86e87..4d7e9ef3 100644 --- a/dist/style.scss +++ b/dist/style.scss @@ -1,5 +1,5 @@ /*! - * Blue React v9.11.0 (https://bruegmann.github.io/blue-react) + * Blue React v9.12.0 (https://bruegmann.github.io/blue-react) * Licensed under GNU General Public License v3.0 (https://github.com/bruegmann/blue-react/blob/master/LICENSE). */ diff --git a/package-lock.json b/package-lock.json index a3470c0a..21d4a6bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "blue-react", - "version": "9.11.0", + "version": "9.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "blue-react", - "version": "9.11.0", + "version": "9.12.0", "license": "LGPL-3.0-or-later", "dependencies": { "@popperjs/core": "^2.11.5", - "blue-web": "^1.1.0", + "blue-web": "^1.2.0", "bootstrap": "~5.3.3", "clsx": "^1.1.1" }, @@ -6282,9 +6282,9 @@ } }, "node_modules/blue-web": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/blue-web/-/blue-web-1.1.0.tgz", - "integrity": "sha512-QKnWPwtGnl8n5hvqmlxU9Yg8schK7Og1ZC+OBNhmeejpBsNfqW+B4ekRTNwdQRR7GhgoOjDNbQFag+z++qISZQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/blue-web/-/blue-web-1.2.0.tgz", + "integrity": "sha512-we/4yTTD2SCUyXZdO+3Byvo+dnko8YX5IqIvAf4r0tUQkF19R5VahOhpA89QRD82pkFWoi63mR/JqHeO7OqbTg==", "dependencies": { "@popperjs/core": "^2.11.5", "bootstrap": "~5.3.3" @@ -28315,9 +28315,9 @@ "dev": true }, "blue-web": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/blue-web/-/blue-web-1.1.0.tgz", - "integrity": "sha512-QKnWPwtGnl8n5hvqmlxU9Yg8schK7Og1ZC+OBNhmeejpBsNfqW+B4ekRTNwdQRR7GhgoOjDNbQFag+z++qISZQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/blue-web/-/blue-web-1.2.0.tgz", + "integrity": "sha512-we/4yTTD2SCUyXZdO+3Byvo+dnko8YX5IqIvAf4r0tUQkF19R5VahOhpA89QRD82pkFWoi63mR/JqHeO7OqbTg==", "requires": { "@popperjs/core": "^2.11.5", "bootstrap": "~5.3.3" diff --git a/package.json b/package.json index 43fc6379..716525bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blue-react", - "version": "9.11.0", + "version": "9.12.0", "description": "Blue React Components", "license": "LGPL-3.0-or-later", "main": "index.js", @@ -32,7 +32,7 @@ }, "dependencies": { "@popperjs/core": "^2.11.5", - "blue-web": "^1.1.0", + "blue-web": "^1.2.0", "bootstrap": "~5.3.3", "clsx": "^1.1.1" }, diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 68296245..3997ad5a 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -142,13 +142,15 @@ export default class Layout extends Component { this.defaultMatch = ["home"] + const expandSidebar = props.hideSidebarMenu + ? false + : props.expandSidebar !== undefined + ? props.expandSidebar + : localStorage.getItem("blueLayoutShrinkSidebar") === null + this.state = { sidebarIn: props.sidebarIn || false, - expandSidebar: props.hideSidebarMenu - ? false - : props.expandSidebar !== undefined - ? props.expandSidebar - : localStorage.getItem("blueLayoutShrinkSidebar") === null, + expandSidebar, match: null, history: [], hash: window.location.hash, @@ -160,6 +162,8 @@ export default class Layout extends Component { this.toggleExpandSidebar = this.toggleExpandSidebar.bind(this) this.eventListeners = [] + + if (this.props.onChangeExpandSidebar) this.props.onChangeExpandSidebar(expandSidebar) } onHashChange() { diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 00e32371..9fef5f97 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,8 +1,15 @@ import clsx from "clsx" -import React, { CSSProperties, createElement, useEffect, useState } from "react" +import React, { CSSProperties, createElement, useEffect, useRef, useState } from "react" import Outside from "./Outside" import Chevron from "./Chevron" +function findParentWithClass(element: HTMLElement | null, className: string) { + while (element && !element.classList.contains(className)) { + element = element.parentElement + } + return element +} + export interface MenuItemProps { /** * Sets `to` prop, e.g. when you use the `NavLink` component from React Router. @@ -107,6 +114,13 @@ export interface MenuItemProps { */ supportOutside?: boolean + /** + * Overrides default class list to be ignored on click outside. + * Hint: If you want this menu item to stay open when others will open, set: + * `outsideIgnoreClasses={["blue-menu-item-wrapper"]}`. + */ + outsideIgnoreClasses?: string[] + /** * By default, MenuItem is a `"button"`. If you set a `href`, it's a `"a"`. * If you want to have it another type, you can pass a component reference with this prop (e.g. `Link`). @@ -179,9 +193,13 @@ export interface MenuItemProps { * Link, button or custom component for Sidebar, Actions or ActionMenu */ export default function MenuItem(props: MenuItemProps) { + const id = `blue-menu-item-wrapper-${Math.random().toString(36).substring(7)}` + const [showDropdown, setShowDropdown] = useState(false) const [active, setActive] = useState(false) + const menuRef = useRef(null) + const checkActive = () => { setActive( (props.href && window.location.hash.indexOf(props.href) > -1) || @@ -210,13 +228,20 @@ export default function MenuItem(props: MenuItemProps) { } const onClickOutside = ({ target }: MouseEvent) => { - // Don't trigger when clicking on MenuItem - if ( - !(target as HTMLElement | null)?.classList.contains("blue-menu-item-dropdown-toggle") && - !(target as HTMLElement | null)?.classList.contains("blue-menu-item-label") - ) { - setShowDropdown(false) + const ignoreClasses = props.outsideIgnoreClasses || [id] + + if (ignoreClasses && target) { + for (let i = 0; i < ignoreClasses.length; i++) { + if ( + (target as HTMLElement | null)?.classList.contains(ignoreClasses[i]) || + findParentWithClass(target as HTMLElement, ignoreClasses[i]) + ) { + return + } + } } + + setShowDropdown(false) } useEffect(() => { @@ -236,6 +261,14 @@ export default function MenuItem(props: MenuItemProps) { } }, [props.onShowDropdown, showDropdown]) + useEffect(() => { + if (menuRef && menuRef.current) { + const el = menuRef.current + const rect = el.getBoundingClientRect() + el.style.setProperty("--offset-top", Math.round(rect.top) + "px") + } + }, [menuRef, showDropdown]) + const className = "blue-menu-item btn" + (props.isActive ? " active" : "") + @@ -264,7 +297,7 @@ export default function MenuItem(props: MenuItemProps) { } return ( - <> +
{createElement( props.elementType || (props.href ? "a" : "button"), { @@ -340,6 +373,7 @@ export default function MenuItem(props: MenuItemProps) { {showDropdown && (props.supportOutside ? ( ) : (
{props.children}
))} - +
) } diff --git a/src/components/Outside.tsx b/src/components/Outside.tsx index 319c76bf..1b3541ec 100644 --- a/src/components/Outside.tsx +++ b/src/components/Outside.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, MouseEventHandler, MutableRefObject, useEffect, useRef } from "react" +import React, { CSSProperties, MouseEventHandler, MutableRefObject, RefObject, useEffect, useRef } from "react" /** * Hook that alerts clicks outside of the passed ref @@ -28,17 +28,18 @@ export interface OutsideProps { onClickOutside?: (event: MouseEvent) => void onClick?: MouseEventHandler | undefined style?: CSSProperties + wrapperRef?: RefObject } /** * Component that fires an event if you click outside of it */ -export default function Outside({ children, className, onClickOutside, onClick, style }: OutsideProps) { - const wrapperRef = useRef(null) - useOutside(wrapperRef, onClickOutside) +export default function Outside({ children, className, onClickOutside, onClick, style, wrapperRef }: OutsideProps) { + const ref = useRef(null) + useOutside(wrapperRef || ref, onClickOutside) return ( -
+
{children}
) diff --git a/src/components/SidebarMenuItem.tsx b/src/components/SidebarMenuItem.tsx index 1ef9bd09..b49b9866 100644 --- a/src/components/SidebarMenuItem.tsx +++ b/src/components/SidebarMenuItem.tsx @@ -6,15 +6,8 @@ export interface SidebarMenuItemProps extends MenuItemProps { outerClass?: string } -function getOffset(el: HTMLElement) { - const rect = el.getBoundingClientRect() - return { - left: Math.round(rect.left + window.scrollX), - top: Math.round(rect.top + window.scrollY) - } -} - /** + * @deprecated `MenuItem` now has all the features of `SidebarMenuItem`. Use `MenuItem` instead. * Extends `MenuItem` with following features: * * Shows provided label as tooltip if sidebar is closed. * * Children will be displayed on the right side of the parent item. @@ -43,8 +36,7 @@ export default function SidebarMenuItem({ outerClass = "", children, onClick, .. useEffect(() => { if (menuRef && menuRef.current) { - const offset = getOffset(menuRef.current) - setOffsetTop(offset.top) + setOffsetTop(menuRef.current.getBoundingClientRect().top) } }, [menuRef, open]) @@ -69,7 +61,6 @@ export default function SidebarMenuItem({ outerClass = "", children, onClick, .. "ms-1", "rounded", "w-bla-sidebar-width", - "blue-menu-item-dropdown", "blue-menu-item-dropdown-from-start" ].join(" ")} style={{ diff --git a/src/docs/App.tsx b/src/docs/App.tsx index 347edbb1..748ef149 100644 --- a/src/docs/App.tsx +++ b/src/docs/App.tsx @@ -20,8 +20,7 @@ import { StickiesFill, Rss, RssFill, - Eye, - BoxArrowUpRight + Eye } from "react-bootstrap-icons" import { ComponentPage } from "./pages/ComponentPage" @@ -33,7 +32,6 @@ import LicenseReportPage from "./pages/LicenseReportPage" import { useEffect } from "react" import DemoApp from "./components/DemoApp" import SidebarMenu from "../components/SidebarMenu" -import SidebarMenuItem from "../components/SidebarMenuItem" function App() { const onHashChange = () => { @@ -127,14 +125,9 @@ function App() { menuClass="overflow-visible" bottomContent={ <> - } - label="Demo App" - /> + } label="Demo App" /> - } label="Code on GitHub" @@ -144,7 +137,7 @@ function App() { } > - } iconForActive={} label="Start" @@ -152,7 +145,7 @@ function App() { exact to="/" /> - } iconForActive={} label="Blog" @@ -160,14 +153,14 @@ function App() { to="/blog" /> - } iconForActive={} label="React Components" elementType={NavLink} to="/component" /> - } iconForActive={} label="Recipes" diff --git a/src/docs/data/docs.json b/src/docs/data/docs.json index f7c1d780..d4ee04b3 100644 --- a/src/docs/data/docs.json +++ b/src/docs/data/docs.json @@ -1 +1 @@ -{"src\\components\\A.tsx":{"description":"The `` element automatically sets `rel=\"noreferrer\"` for external links with `target=\"_blank\"`.\\\r\n`A` allows all props of the `` element.\\\r\n`Example` โžก๏ธ `Example`","displayName":"A","methods":[]},"src\\components\\ActionMenu.tsx":{"description":"The Action Menu on the top right of a page. You can place Actions here which are in context of the current page.","displayName":"ActionMenu","methods":[],"props":{"hideToggleAction":{"required":false,"tsType":{"name":"boolean"},"description":"Hides the toggle button in mobile view. Can be useful when using multiple ActionMenus and not show the toggle button for each menu."},"children":{"required":false,"tsType":{"name":"any"},"description":""},"toggleIcon":{"required":false,"tsType":{"name":"any"},"description":"Icon component for the toggle icon."},"className":{"required":false,"tsType":{"name":"string"},"description":""},"break":{"required":false,"tsType":{"name":"union","raw":"breakOption | \"none\"","elements":[{"name":"breakOption"},{"name":"literal","value":"\"none\""}]},"description":"\"sm\" | \"md\" | \"lg\" | \"xl\" | \"none\". Default is \"lg\". The responsive breakpoint at which the menu will be shown as a dropdown."}},"exampleCode":"import { useState } from \"react\"\r\nimport { Link } from \"react-router-dom\"\r\n\r\nexport default function ActionMenuExample() {\r\n const [isChecked, setIsChecked] = useState(false)\r\n const toggleIsChecked = () => setIsChecked(!isChecked)\r\n\r\n const style = {\r\n width: isChecked && window.innerWidth > 600 ? \"600px\" : \"\"\r\n }\r\n\r\n return (\r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n \r\n Open in full view\r\n {\" \"}\r\n \r\n Open in new tab\r\n \r\n
\r\n
\r\n