(chore) document new mobile menu stuff
This commit is contained in:
parent
49c14f1446
commit
f6fcda8ca5
6 changed files with 92 additions and 14 deletions
|
@ -5,18 +5,25 @@ import { CloseIcon } from 'shared/components/icons.mjs'
|
||||||
import { MobileMenubarContext } from 'shared/context/mobile-menubar-context.mjs'
|
import { MobileMenubarContext } from 'shared/context/mobile-menubar-context.mjs'
|
||||||
import { shownHeaderSelector } from 'shared/components/wrappers/header.mjs'
|
import { shownHeaderSelector } from 'shared/components/wrappers/header.mjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component to display menu buttons and actions in mobile.
|
||||||
|
* Draws its contents from items added to the {@link MobileMenubarContext}
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const MobileMenubar = () => {
|
export const MobileMenubar = () => {
|
||||||
const { setModal, clearModal, modalContent } = useContext(ModalContext)
|
const { setModal, clearModal, modalContent } = useContext(ModalContext)
|
||||||
const { menus, actions } = useContext(MobileMenubarContext)
|
const { menus, actions } = useContext(MobileMenubarContext)
|
||||||
const [selectedModal, setSelectedModal] = useState(false)
|
const [selectedModal, setSelectedModal] = useState(false)
|
||||||
|
|
||||||
|
// get the content of the selected modal because this is what will be changing if there are updates
|
||||||
const selectedMenu = menus[selectedModal]
|
const selectedMenu = menus[selectedModal]
|
||||||
|
|
||||||
|
// when the content changes, or the selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// there's no selected modal, we're in the clear
|
// there's no selected modal, we're in the clear
|
||||||
if (!selectedModal) return
|
if (!selectedModal) return
|
||||||
|
|
||||||
// otherwise, set the modal and keep an internal record of having opened it
|
// generate a new modal with the content
|
||||||
const Modal = () => {
|
const Modal = () => {
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setSelectedModal(false)
|
setSelectedModal(false)
|
||||||
|
@ -40,9 +47,11 @@ export const MobileMenubar = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set it
|
||||||
setModal(Modal)
|
setModal(Modal)
|
||||||
}, [selectedMenu, selectedModal, clearModal, setModal])
|
}, [selectedMenu, selectedModal, clearModal, setModal])
|
||||||
|
|
||||||
|
// clear the selection if the modal was cleared externally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalContent === null) {
|
if (modalContent === null) {
|
||||||
setSelectedModal(false)
|
setSelectedModal(false)
|
||||||
|
@ -52,14 +61,14 @@ export const MobileMenubar = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
lg:hidden
|
lg:hidden
|
||||||
${shownHeaderSelector('bottom-16')}
|
${shownHeaderSelector('bottom-16')}
|
||||||
sticky bottom-0 w-20 -ml-20 self-end
|
sticky bottom-0 w-20 -ml-20 self-end
|
||||||
duration-300 transition-all
|
duration-300 transition-all
|
||||||
flex flex-col-reverse gap-4
|
flex flex-col-reverse gap-4
|
||||||
z-20
|
z-20
|
||||||
mobile-menubar
|
mobile-menubar
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{Object.keys(menus)
|
{Object.keys(menus)
|
||||||
.sort((a, b) => menus[a].order - menus[b].order)
|
.sort((a, b) => menus[a].order - menus[b].order)
|
||||||
|
|
|
@ -3,6 +3,17 @@ import { WrenchIcon } from 'shared/components/icons.mjs'
|
||||||
import { useMobileMenu } from 'shared/context/mobile-menubar-context.mjs'
|
import { useMobileMenu } from 'shared/context/mobile-menubar-context.mjs'
|
||||||
|
|
||||||
const defaultClasses = `w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-full overflow-scroll`
|
const defaultClasses = `w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-full overflow-scroll`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a wrapper that displays its contents normally on larger screens
|
||||||
|
* and adds its contents as a menu on the {@link MobileMenubar} for smaller screens
|
||||||
|
* @param {ReactChildren} children
|
||||||
|
* @param {String} wrapperClass - classes to add to the wrapper for larger screens
|
||||||
|
* @param {ReactComponent} Icon - Icon for the menu button on smaller screens
|
||||||
|
* @param {Boolean} [keepOpenOnClick=true] - should the modal for this menu stay open when clicked?
|
||||||
|
* @param {String} [type='settings'] - the type of menu this is. will be used to key the menu in the MobileMenubar
|
||||||
|
* @param {Number} [order=-1] - the order of this menu in the MobileMenubar
|
||||||
|
*/
|
||||||
export const MenuWrapper = ({
|
export const MenuWrapper = ({
|
||||||
children,
|
children,
|
||||||
wrapperClass = defaultClasses,
|
wrapperClass = defaultClasses,
|
||||||
|
@ -16,10 +27,9 @@ export const MenuWrapper = ({
|
||||||
Icon,
|
Icon,
|
||||||
menuContent: children,
|
menuContent: children,
|
||||||
keepOpenOnClick,
|
keepOpenOnClick,
|
||||||
type,
|
|
||||||
order,
|
order,
|
||||||
}),
|
}),
|
||||||
[Icon, children, keepOpenOnClick, type]
|
[Icon, children, keepOpenOnClick, order]
|
||||||
)
|
)
|
||||||
|
|
||||||
useMobileMenu(type, menuProps)
|
useMobileMenu(type, menuProps)
|
||||||
|
|
|
@ -76,6 +76,7 @@ export const Workbench = ({ design, Design, DynamicDocs }) => {
|
||||||
|
|
||||||
const setView = useCallback(
|
const setView = useCallback(
|
||||||
(newView) => {
|
(newView) => {
|
||||||
|
// hacky little way to scroll to the top but keep the menu hidden if it was hidden
|
||||||
const endScroll = Math.min(window.scrollY, 21)
|
const endScroll = Math.min(window.scrollY, 21)
|
||||||
window.scrollTo({ top: 0, behavior: 'instant' })
|
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||||
_setView(newView)
|
_setView(newView)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { MenuWrapper } from 'shared/components/workbench/menus/shared/menu-wrapp
|
||||||
|
|
||||||
export const ns = headerNs
|
export const ns = headerNs
|
||||||
|
|
||||||
|
/** a layout for views that include a drafted pattern, a sidebar menu, and the draft view header */
|
||||||
export const PatternWithMenu = ({
|
export const PatternWithMenu = ({
|
||||||
settings,
|
settings,
|
||||||
ui,
|
ui,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext, useEffect, useMemo } from 'react'
|
import { useContext, useMemo } from 'react'
|
||||||
import { PanZoomContext } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
|
import { PanZoomContext } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
|
||||||
import { useMobileAction } from 'shared/context/mobile-menubar-context.mjs'
|
import { useMobileAction } from 'shared/context/mobile-menubar-context.mjs'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
@ -80,10 +80,12 @@ export const ViewHeader = ({ update, settings, ui, control, setSettings }) => {
|
||||||
const { t } = useTranslation(ns)
|
const { t } = useTranslation(ns)
|
||||||
const { zoomFunctions, zoomed } = useContext(PanZoomContext)
|
const { zoomFunctions, zoomed } = useContext(PanZoomContext)
|
||||||
|
|
||||||
|
// make the zoom buttons so we can pass them to the mobile menubar
|
||||||
const headerZoomButtons = useMemo(
|
const headerZoomButtons = useMemo(
|
||||||
() => <ZoomButtons {...{ t, zoomFunctions, zoomed }} />,
|
() => <ZoomButtons {...{ t, zoomFunctions, zoomed }} />,
|
||||||
[zoomed, t, zoomFunctions]
|
[zoomed, t, zoomFunctions]
|
||||||
)
|
)
|
||||||
|
// add the zoom buttons as an action on the mobile menubar
|
||||||
useMobileAction('zoom', { order: 0, actionContent: headerZoomButtons })
|
useMobileAction('zoom', { order: 0, actionContent: headerZoomButtons })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,11 +1,34 @@
|
||||||
import React, { useState, useContext, useEffect, useCallback } from 'react'
|
import React, { useState, useContext, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context for holding elements from various places that should be presented
|
||||||
|
* cohesively in mobile, e.g. sidebar menus and toolbars
|
||||||
|
*
|
||||||
|
* There are two ways of presenting menus on the menubar:
|
||||||
|
* 1) Menus: These are larger interfaces that should be placed in a modal on mobile
|
||||||
|
* They will be presented as buttons on the menubar which will open a modal when clicked
|
||||||
|
* example: workbench nav menu
|
||||||
|
* 2) Actions: These are smaller and will be presented directly on the menubar
|
||||||
|
* example: zoom buttons
|
||||||
|
*/
|
||||||
export const MobileMenubarContext = React.createContext(null)
|
export const MobileMenubarContext = React.createContext(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A provider for the {@link MobileMenubarContext}
|
||||||
|
* */
|
||||||
export const MobileMenubarContextProvider = ({ children }) => {
|
export const MobileMenubarContextProvider = ({ children }) => {
|
||||||
const [menus, setMenus] = useState({})
|
const [menus, setMenus] = useState({})
|
||||||
const [actions, setActions] = useState({})
|
const [actions, setActions] = useState({})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a menu to the menubar
|
||||||
|
* @type Function
|
||||||
|
* @param {String} key - the key for this menu in the menus object
|
||||||
|
* @param {Object} menuProps - the properties of this menu
|
||||||
|
* @param {React.Component} menuProps.Icon - the icon for the menu button
|
||||||
|
* @param {ReactElement} menuProps.menuContent - the content of the menu to be displayed in a modal
|
||||||
|
* @param {Number} menuProps.order - the sort order of this menu
|
||||||
|
* */
|
||||||
const addMenu = useCallback(
|
const addMenu = useCallback(
|
||||||
(key, menuProps) => {
|
(key, menuProps) => {
|
||||||
setMenus((oldMenus) => ({ ...oldMenus, [key]: menuProps }))
|
setMenus((oldMenus) => ({ ...oldMenus, [key]: menuProps }))
|
||||||
|
@ -13,6 +36,11 @@ export const MobileMenubarContextProvider = ({ children }) => {
|
||||||
[setMenus]
|
[setMenus]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a menu from the menubar
|
||||||
|
* @type Function
|
||||||
|
* @param {String} key - the key that was used to add the menu to the menubar
|
||||||
|
*/
|
||||||
const removeMenu = useCallback(
|
const removeMenu = useCallback(
|
||||||
(key) => {
|
(key) => {
|
||||||
setMenus((oldMenus) => {
|
setMenus((oldMenus) => {
|
||||||
|
@ -24,16 +52,28 @@ export const MobileMenubarContextProvider = ({ children }) => {
|
||||||
[setMenus]
|
[setMenus]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an action to the menubar
|
||||||
|
* @param {String} key - the key for this action in the actions object
|
||||||
|
* @param {Object} actionProps - the properties of this action
|
||||||
|
* @param {ReactElement} actionProps.actionContent - the content of the action to be displayed in a modal
|
||||||
|
* @param {Number} actionProps.order - the sort order of this action
|
||||||
|
*/
|
||||||
const addAction = useCallback(
|
const addAction = useCallback(
|
||||||
(key, content) => {
|
(key, actionProps) => {
|
||||||
setActions((oldActions) => ({
|
setActions((oldActions) => ({
|
||||||
...oldActions,
|
...oldActions,
|
||||||
[key]: content,
|
[key]: actionProps,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
[setActions]
|
[setActions]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an action from the menubar
|
||||||
|
* @type Function
|
||||||
|
* @param {String} key - the key that was used to add the action to the menubar
|
||||||
|
*/
|
||||||
const removeAction = useCallback(
|
const removeAction = useCallback(
|
||||||
(key) => {
|
(key) => {
|
||||||
setActions((oldActions) => {
|
setActions((oldActions) => {
|
||||||
|
@ -57,6 +97,14 @@ export const MobileMenubarContextProvider = ({ children }) => {
|
||||||
return <MobileMenubarContext.Provider value={value}>{children}</MobileMenubarContext.Provider>
|
return <MobileMenubarContext.Provider value={value}>{children}</MobileMenubarContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook to add content as a menu in the mobile menubar and handle remove on unmount
|
||||||
|
* @param {String} key - the key for this menu in the menus object
|
||||||
|
* @param {Object} menuProps - the properties of this menu
|
||||||
|
* @param {React.Component} menuProps.Icon - the icon for the menu button
|
||||||
|
* @param {ReactElement} menuProps.menuContent - the content of the menu to be displayed in a modal
|
||||||
|
* @param {Number} menuProps.order - the sort order of this menu
|
||||||
|
* */
|
||||||
export const useMobileMenu = (key, menuProps) => {
|
export const useMobileMenu = (key, menuProps) => {
|
||||||
const { addMenu, removeMenu } = useContext(MobileMenubarContext)
|
const { addMenu, removeMenu } = useContext(MobileMenubarContext)
|
||||||
|
|
||||||
|
@ -67,6 +115,13 @@ export const useMobileMenu = (key, menuProps) => {
|
||||||
}, [menuProps, key, addMenu, removeMenu])
|
}, [menuProps, key, addMenu, removeMenu])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook to add content as an action to the mobile menubar and handle removal on unmount
|
||||||
|
* @param @param {String} key - the key for this action in the actions object
|
||||||
|
* @param {Object} actionProps - the properties of this action
|
||||||
|
* @param {ReactElement} actionProps.actionContent - the content of the action to be displayed in a modal
|
||||||
|
* @param {Number} actionProps.order - the sort order of this action
|
||||||
|
*/
|
||||||
export const useMobileAction = (key, content) => {
|
export const useMobileAction = (key, content) => {
|
||||||
const { addAction, removeAction } = useContext(MobileMenubarContext)
|
const { addAction, removeAction } = useContext(MobileMenubarContext)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue