use a context to allow all menus to be in same parent on mobile
This commit is contained in:
parent
951eb57718
commit
012b0eb8d6
10 changed files with 199 additions and 50 deletions
|
@ -19,7 +19,7 @@ export const Tabs = ({ tabs = '', active = 0, children }) => {
|
||||||
{tablist.map((title, tabId) => (
|
{tablist.map((title, tabId) => (
|
||||||
<button
|
<button
|
||||||
key={tabId}
|
key={tabId}
|
||||||
className={`text-xl font-bold capitalize tab tab-bordered grow ${
|
className={`text-xl font-bold capitalize tab h-auto tab-bordered grow ${
|
||||||
activeTab === tabId ? 'tab-active' : ''
|
activeTab === tabId ? 'tab-active' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setActiveTab(tabId)}
|
onClick={() => setActiveTab(tabId)}
|
||||||
|
|
|
@ -171,6 +171,8 @@ export const WorkbenchHeader = ({ view, setView }) => {
|
||||||
${dense ? '-ml-52' : 'ml-0'}`}
|
${dense ? '-ml-52' : 'ml-0'}`}
|
||||||
buttonClass={`order-last bottom-16`}
|
buttonClass={`order-last bottom-16`}
|
||||||
keepOpenOnClick={false}
|
keepOpenOnClick={false}
|
||||||
|
order={0}
|
||||||
|
type="nav"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
className={`
|
className={`
|
||||||
|
|
75
sites/shared/components/workbench/menus/mobile-menubar.mjs
Normal file
75
sites/shared/components/workbench/menus/mobile-menubar.mjs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { useContext, useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
|
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||||
|
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||||
|
import { CloseIcon } from 'shared/components/icons.mjs'
|
||||||
|
import { MobileMenubarContext } from 'shared/context/mobile-menubar-context.mjs'
|
||||||
|
import { shownHeaderSelector } from 'shared/components/wrappers/header.mjs'
|
||||||
|
|
||||||
|
export const MobileMenubar = () => {
|
||||||
|
const { setModal, clearModal, modalContent } = useContext(ModalContext)
|
||||||
|
const { menus } = useContext(MobileMenubarContext)
|
||||||
|
const [selectedModal, setSelectedModal] = useState(false)
|
||||||
|
|
||||||
|
const selectedMenu = menus[selectedModal]
|
||||||
|
|
||||||
|
const Modal = useCallback(() => {
|
||||||
|
const closeModal = () => {
|
||||||
|
setSelectedModal(false)
|
||||||
|
clearModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalWrapper
|
||||||
|
slideFrom="right"
|
||||||
|
keepOpenOnClick={selectedMenu.keepOpenOnClick}
|
||||||
|
keepOpenOnSwipe
|
||||||
|
>
|
||||||
|
<div className="mb-16">{selectedMenu.MenuContent}</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-accent btn-circle fixed bottom-4 right-4 z-20"
|
||||||
|
onClick={closeModal}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</ModalWrapper>
|
||||||
|
)
|
||||||
|
}, [selectedMenu, clearModal])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedModal) return
|
||||||
|
|
||||||
|
setModal(Modal)
|
||||||
|
}, [selectedModal, Modal, setModal])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalContent === null) {
|
||||||
|
setSelectedModal(false)
|
||||||
|
}
|
||||||
|
}, [modalContent, setSelectedModal])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
lg:hidden
|
||||||
|
${shownHeaderSelector('bottom-16')}
|
||||||
|
sticky bottom-0 w-20 -ml-20 self-end
|
||||||
|
duration-300 transition-all flex flex-col-reverse
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{Object.keys(menus)
|
||||||
|
.sort((a, b) => menus[a].order - menus[b].order)
|
||||||
|
.map((m) => {
|
||||||
|
const Icon = menus[m].Icon
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
className="btn btn-accent btn-circle mx-4 my-2 z-20"
|
||||||
|
onClick={() => setSelectedModal(m)}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,58 +1,31 @@
|
||||||
import { useContext, useState, useEffect, useCallback } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
import { WrenchIcon } from 'shared/components/icons.mjs'
|
||||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
import { useMobileMenu } from 'shared/context/mobile-menubar-context.mjs'
|
||||||
import { CloseIcon, WrenchIcon } from 'shared/components/icons.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`
|
||||||
const defaultButtonClasses = 'bottom-24 mb-16'
|
|
||||||
export const MenuWrapper = ({
|
export const MenuWrapper = ({
|
||||||
children,
|
children,
|
||||||
wrapperClass = defaultClasses,
|
wrapperClass = defaultClasses,
|
||||||
buttonClass = defaultButtonClasses,
|
|
||||||
Icon = WrenchIcon,
|
Icon = WrenchIcon,
|
||||||
keepOpenOnClick = true,
|
keepOpenOnClick = true,
|
||||||
|
type = 'settings',
|
||||||
|
order = -1,
|
||||||
}) => {
|
}) => {
|
||||||
const { setModal, clearModal, modalContent } = useContext(ModalContext)
|
const menuProps = useMemo(
|
||||||
const [modalOpen, setModalOpen] = useState(false)
|
() => ({
|
||||||
|
Icon,
|
||||||
|
MenuContent: children,
|
||||||
|
keepOpenOnClick,
|
||||||
|
type,
|
||||||
|
order,
|
||||||
|
}),
|
||||||
|
[Icon, children, keepOpenOnClick, type]
|
||||||
|
)
|
||||||
|
|
||||||
const Modal = useCallback(() => {
|
useMobileMenu(type, menuProps)
|
||||||
const closeModal = () => {
|
|
||||||
setModalOpen(false)
|
|
||||||
clearModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalWrapper slideFrom="right" keepOpenOnClick={keepOpenOnClick} keepOpenOnSwipe>
|
|
||||||
<div className="mb-16">{children}</div>
|
|
||||||
<button className="btn btn-accent btn-circle fixed bottom-4 right-4" onClick={closeModal}>
|
|
||||||
<CloseIcon />
|
|
||||||
</button>
|
|
||||||
</ModalWrapper>
|
|
||||||
)
|
|
||||||
}, [children, clearModal, keepOpenOnClick])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!modalOpen) return
|
|
||||||
|
|
||||||
setModal(Modal)
|
|
||||||
}, [modalOpen, Modal, setModal])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (modalContent === null) setModalOpen(false)
|
|
||||||
}, [modalContent, setModalOpen])
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
setModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
|
||||||
className={`btn btn-accent btn-circle m-4 z-20 lg:hidden -ml-16 self-end ${buttonClass} sticky`}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<Icon />{' '}
|
|
||||||
</button>
|
|
||||||
<div className={`hidden lg:block ${wrapperClass}`}>{children}</div>
|
<div className={`hidden lg:block ${wrapperClass}`}>{children}</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useView } from 'shared/hooks/use-view.mjs'
|
import { useView } from 'shared/hooks/use-view.mjs'
|
||||||
import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs'
|
import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs'
|
||||||
|
@ -13,6 +13,7 @@ import { objUpdate, hasRequiredMeasurements } from 'shared/utils.mjs'
|
||||||
import { WorkbenchHeader } from './header.mjs'
|
import { WorkbenchHeader } from './header.mjs'
|
||||||
import { ErrorView } from 'shared/components/error/view.mjs'
|
import { ErrorView } from 'shared/components/error/view.mjs'
|
||||||
import { ModalSpinner } from 'shared/components/modal/spinner.mjs'
|
import { ModalSpinner } from 'shared/components/modal/spinner.mjs'
|
||||||
|
import { MobileMenubar } from './menus/mobile-menubar.mjs'
|
||||||
// Views
|
// Views
|
||||||
import { DraftView, ns as draftNs } from 'shared/components/workbench/views/draft/index.mjs'
|
import { DraftView, ns as draftNs } from 'shared/components/workbench/views/draft/index.mjs'
|
||||||
import { SaveView, ns as saveNs } from 'shared/components/workbench/views/save/index.mjs'
|
import { SaveView, ns as saveNs } from 'shared/components/workbench/views/save/index.mjs'
|
||||||
|
@ -66,13 +67,23 @@ export const Workbench = ({ design, Design, DynamicDocs }) => {
|
||||||
const controlState = useControlState()
|
const controlState = useControlState()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [view, setView] = useView()
|
const [view, _setView] = useView()
|
||||||
const [settings, setSettings] = usePatternSettings()
|
const [settings, setSettings] = usePatternSettings()
|
||||||
const [ui, setUi] = useState(defaultUi)
|
const [ui, setUi] = useState(defaultUi)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
const [missingMeasurements, setMissingMeasurements] = useState(false)
|
const [missingMeasurements, setMissingMeasurements] = useState(false)
|
||||||
|
|
||||||
|
const setView = useCallback(
|
||||||
|
(newView) => {
|
||||||
|
const endScroll = Math.min(window.scrollY, 21)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||||
|
_setView(newView)
|
||||||
|
window.scroll({ top: endScroll })
|
||||||
|
},
|
||||||
|
[_setView]
|
||||||
|
)
|
||||||
|
|
||||||
// set mounted on mount
|
// set mounted on mount
|
||||||
useEffect(() => setMounted(true), [setMounted])
|
useEffect(() => setMounted(true), [setMounted])
|
||||||
|
|
||||||
|
@ -126,6 +137,7 @@ export const Workbench = ({ design, Design, DynamicDocs }) => {
|
||||||
<>
|
<>
|
||||||
<WorkbenchHeader {...{ view, setView, update }} />
|
<WorkbenchHeader {...{ view, setView, update }} />
|
||||||
{error}
|
{error}
|
||||||
|
<MobileMenubar />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -196,6 +208,7 @@ export const Workbench = ({ design, Design, DynamicDocs }) => {
|
||||||
<div className="flex flex-row min-h-screen">
|
<div className="flex flex-row min-h-screen">
|
||||||
<WorkbenchHeader {...{ view, setView, update }} />
|
<WorkbenchHeader {...{ view, setView, update }} />
|
||||||
<div className="grow">{viewContent}</div>
|
<div className="grow">{viewContent}</div>
|
||||||
|
<MobileMenubar />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const PatternWithMenu = ({
|
||||||
{title}
|
{title}
|
||||||
{pattern}
|
{pattern}
|
||||||
</div>
|
</div>
|
||||||
{menu && <MenuWrapper>{menu}</MenuWrapper>}
|
{menu && <MenuWrapper order={1}>{menu}</MenuWrapper>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PanZoomContextProvider>
|
</PanZoomContextProvider>
|
||||||
|
|
|
@ -42,6 +42,7 @@ export const SampleItem = ({ name, passProps, t, updateFunc }) => {
|
||||||
type="radio"
|
type="radio"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
className="radio radio-primary mr-2 radio-sm"
|
className="radio radio-primary mr-2 radio-sm"
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
<span className="ml-2">{t([name + '.t', name])}</span>
|
<span className="ml-2">{t([name + '.t', name])}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { ModalContextProvider } from 'shared/context/modal-context.mjs'
|
import { ModalContextProvider } from 'shared/context/modal-context.mjs'
|
||||||
import { LoadingContextProvider } from 'shared/context/loading-context.mjs'
|
import { LoadingContextProvider } from 'shared/context/loading-context.mjs'
|
||||||
import { NavigationContextProvider } from 'shared/context/navigation-context.mjs'
|
import { NavigationContextProvider } from 'shared/context/navigation-context.mjs'
|
||||||
|
import { MobileMenubarContextProvider } from 'shared/context/mobile-menubar-context.mjs'
|
||||||
|
|
||||||
export const ContextWrapper = ({ children }) => (
|
export const ContextWrapper = ({ children }) => (
|
||||||
<ModalContextProvider>
|
<ModalContextProvider>
|
||||||
<LoadingContextProvider>
|
<LoadingContextProvider>
|
||||||
<NavigationContextProvider>{children}</NavigationContextProvider>
|
<NavigationContextProvider>
|
||||||
|
<MobileMenubarContextProvider>{children}</MobileMenubarContextProvider>
|
||||||
|
</NavigationContextProvider>
|
||||||
</LoadingContextProvider>
|
</LoadingContextProvider>
|
||||||
</ModalContextProvider>
|
</ModalContextProvider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,4 +25,8 @@ export const HeaderWrapper = ({ show, children }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shownHeaderSelector = 'group-[.header-shown]/layout:'
|
const shownHeaderClasses = {
|
||||||
|
'bottom-16': 'group-[.header-shown]/layout:bottom-16',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shownHeaderSelector = (cls) => shownHeaderClasses[cls]
|
||||||
|
|
78
sites/shared/context/mobile-menubar-context.mjs
Normal file
78
sites/shared/context/mobile-menubar-context.mjs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useState, useContext, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
export const MobileMenubarContext = React.createContext(null)
|
||||||
|
|
||||||
|
export const MobileMenubarContextProvider = ({ children }) => {
|
||||||
|
const [menus, setMenus] = useState({})
|
||||||
|
const [actions, setActions] = useState({})
|
||||||
|
|
||||||
|
const addMenu = useCallback(
|
||||||
|
(key, menuProps) => {
|
||||||
|
setMenus((oldMenus) => ({ ...oldMenus, [key]: menuProps }))
|
||||||
|
},
|
||||||
|
[setMenus]
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeMenu = useCallback(
|
||||||
|
(key) => {
|
||||||
|
setMenus((oldMenus) => {
|
||||||
|
const newMenus = { ...oldMenus }
|
||||||
|
delete newMenus[key]
|
||||||
|
return newMenus
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setMenus]
|
||||||
|
)
|
||||||
|
|
||||||
|
const addAction = useCallback(
|
||||||
|
(key, content) => {
|
||||||
|
setActions((oldActions) => ({
|
||||||
|
...oldActions,
|
||||||
|
[key]: content,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[setActions]
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeAction = useCallback(
|
||||||
|
(key) => {
|
||||||
|
setActions((oldActions) => {
|
||||||
|
const newActions = { ...oldActions }
|
||||||
|
delete newActions[key]
|
||||||
|
return newActions
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setActions]
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
menus,
|
||||||
|
addMenu,
|
||||||
|
removeMenu,
|
||||||
|
actions,
|
||||||
|
addAction,
|
||||||
|
removeAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MobileMenubarContext.Provider value={value}>{children}</MobileMenubarContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMobileMenu = (key, menuProps) => {
|
||||||
|
const { addMenu, removeMenu } = useContext(MobileMenubarContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addMenu(key, menuProps)
|
||||||
|
|
||||||
|
return () => removeMenu(key)
|
||||||
|
}, [menuProps, key, addMenu, removeMenu])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMobileAction = (key, content) => {
|
||||||
|
const { addAction, removeAction } = useContext(MobileMenubarContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addAction(key, content)
|
||||||
|
|
||||||
|
return () => removeAction(key)
|
||||||
|
}, [content, key, addAction, removeAction])
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue