1
0
Fork 0

use a context to allow all menus to be in same parent on mobile

This commit is contained in:
Enoch Riese 2023-06-29 14:35:46 +00:00
parent 951eb57718
commit 012b0eb8d6
10 changed files with 199 additions and 50 deletions

View file

@ -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)}

View file

@ -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={`

View 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>
)
}

View file

@ -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>
</> </>
) )

View file

@ -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>
) )
} }

View file

@ -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>

View file

@ -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>

View file

@ -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>
) )

View file

@ -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]

View 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])
}