Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APP-15727: Introduce the TogglePanel component #1427

Merged
merged 8 commits into from
Nov 5, 2020
2 changes: 1 addition & 1 deletion src/AutocompleteInput/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { AutocompleteInput } from './AutocompleteInput'

export { AutocompleteInputProps } from './AutocompleteInput.interface'
export type { AutocompleteInputProps } from './AutocompleteInput.interface'
5 changes: 3 additions & 2 deletions src/Layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Layout } from './Layout'

export { LayoutProps, LayoutChild } from './Layout.interface'
export { useParentLayout } from './Layout.context'
export { LayoutChild } from './Layout.interface'

export type { LayoutProps } from './Layout.interface'
17 changes: 4 additions & 13 deletions src/Menu/Menu.interface.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import { Modal } from '@delangle/use-modal'
import * as React from 'react'

import { TogglePanelProps } from '../TogglePanel'
import { WithTriggerElement } from '../withTriggerElement'

export interface MenuInstance {
open: boolean
onClose: () => void
}

export interface MenuInnerProps
extends React.HTMLAttributes<HTMLUListElement>,
export interface InnerMenuProps
extends Omit<TogglePanelProps, 'setStyle' | keyof MenuInstance>,
MenuInstance {
fullScreenOnMobile?: boolean
scrollable?: boolean
position?: 'horizontal' | 'vertical'
triggerRef?: React.RefObject<HTMLElement>
withOverlay?: boolean
setPosition?: (dimensions: {
triggerDimensions: DOMRect
menuHeight: number
menuWidth: number
}) => { top?: number; left?: number; right?: number; bottom?: number }
children?:
| React.ReactNode
| ((modal: Modal<HTMLUListElement>) => React.ReactNode)
}

export interface MenuProps
extends WithTriggerElement<MenuInnerProps, HTMLUListElement> {}
export type MenuProps = WithTriggerElement<InnerMenuProps, HTMLDivElement>
48 changes: 5 additions & 43 deletions src/Menu/Menu.style.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import styled from 'styled-components'

import { zIndex } from '../_internal/zIndex'
import { animations } from '../animations'
import { theme } from '../theme'

export const MenuTriggerContainer = styled.span`
position: relative;
align-self: flex-start;
`

export const MenuContent = styled.div`
background-color: ${theme.color('background', { useRootTheme: true })};
export const FloatingMenu = styled.ul`
margin: 0;
padding: 8px 0;
background-color: ${theme.color('background', { useRootTheme: true })};
box-shadow: ${theme.shadow()};
border-radius: 4px;

Expand All @@ -22,40 +16,8 @@ export const MenuContent = styled.div`
}
`

export const MenuOverlay = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: ${zIndex.dropDowns};
`

export const MenuContainer = styled.ul`
opacity: 1;
list-style-type: none;
padding: 0;
margin: 0;
position: fixed;

z-index: ${zIndex.dropDowns};

&:not([data-state='opened']) {
pointer-events: none;

opacity: 0;
}

&[data-state='opening'] {
animation: ${animations('emergeSlantFromBottom')};
}

&[data-state='closing'] {
animation: ${animations('diveSlant')};
}
`

export const MenuFullScreenContainer = styled.div`
export const FullScreenMenu = styled.ul`
margin: 0 calc(0px - var(--layout-right-padding)) 0
calc(0px - var(--layout-left-padding));
padding: 0;
`
184 changes: 69 additions & 115 deletions src/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,60 @@
import useModal, { Modal as ModalType } from '@delangle/use-modal'
import { Modal } from '@delangle/use-modal'
import * as React from 'react'
import * as ReactDOM from 'react-dom'

import { isFunction } from '../_internal/data'
import { isClientSide } from '../_internal/ssr'
import { buildUseOnlyOpenedInstanceHook } from '../_internal/useOnlyOpenedInstance'
import { useWindowSize } from '../_internal/useWindowSize'
import { ANIMATION_DURATIONS } from '../animations'
import { breakpoints } from '../breakpoints'
import { Modal } from '../Modal'
import { TogglePanel } from '../TogglePanel'
import { withTriggerElement } from '../withTriggerElement'

import { MenuContext } from './Menu.context'
import { MenuInstance, MenuInnerProps } from './Menu.interface'
import {
MenuContent,
MenuContainer,
MenuFullScreenContainer,
MenuOverlay,
} from './Menu.style'

const useOnlyOneMenuOpened = buildUseOnlyOpenedInstanceHook<MenuInstance>()
import { MenuInstance, InnerMenuProps } from './Menu.interface'
import { FloatingMenu, FullScreenMenu } from './Menu.style'

const TRIGGER_MARGIN = 12

const InnerMenu = React.forwardRef<HTMLUListElement, MenuInnerProps>(
(props, ref) => {
const {
const useOnlyOneMenuOpened = buildUseOnlyOpenedInstanceHook<MenuInstance>()

export const InnerMenu = React.forwardRef<HTMLDivElement, InnerMenuProps>(
(
{
children,
open,
onClose,
triggerRef,
fullScreenOnMobile = false,
scrollable = false,
onClose,
open,
position = 'vertical',
withOverlay = true,
scrollable = false,
setPosition,
...rest
} = props
...props
},
ref
) => {
useOnlyOneMenuOpened({ open, onClose })

const size = useWindowSize()
useOnlyOneMenuOpened({ open, onClose })
const [positionStyle, setPositionStyle] = React.useState<
React.CSSProperties
>()

const modal = useModal<HTMLUListElement>({
ref,
open,
onClose,
persistent: false,
animated: true,
animationDuration: ANIMATION_DURATIONS.m,
})

const content = isFunction(children)
? children(modal as ModalType<HTMLUListElement>)
: children

const updatePosition = React.useCallback(() => {
if (!triggerRef?.current || !modal.ref?.current) {
return
}

const triggerDimensions = triggerRef.current.getBoundingClientRect()
const menuHeight = modal.ref.current.clientHeight
const menuWidth = modal.ref.current.clientWidth

if (isFunction(setPosition)) {
setPositionStyle(
setPosition({ triggerDimensions, menuHeight, menuWidth })
const getChildren = React.useCallback(
(modal: Modal<HTMLDivElement>) => {
const content = isFunction(children) ? children(modal) : children

return fullScreenOnMobile && size.width < breakpoints.raw.phone ? (
<FullScreenMenu>{content}</FullScreenMenu>
) : (
<FloatingMenu data-scrollable={scrollable}>{content}</FloatingMenu>
)
} else {
},
[children]
)

const setStyle = React.useCallback(
(dimensions: DOMRect, triggerDimensions: DOMRect) => {
const menuHeight = triggerDimensions.height
const menuWidth = triggerDimensions.width

if (isFunction(setPosition)) {
return setPosition({ triggerDimensions, menuHeight, menuWidth })
}

if (position === 'vertical') {
let top = triggerDimensions.bottom + TRIGGER_MARGIN

Expand All @@ -89,72 +72,43 @@ const InnerMenu = React.forwardRef<HTMLUListElement, MenuInnerProps>(
? triggerDimensions.left - menuWidth + triggerDimensions.width
: triggerDimensions.left

setPositionStyle({ top, left, minWidth: triggerDimensions.width })
} else {
const top =
triggerDimensions.top + menuHeight > window.innerHeight
? triggerDimensions.top + triggerDimensions.height - menuHeight
: triggerDimensions.top
return { top, left, minWidth: triggerDimensions.width }
}

let left = triggerDimensions.right + TRIGGER_MARGIN
const top =
triggerDimensions.top + menuHeight > window.innerHeight
? triggerDimensions.top + triggerDimensions.height - menuHeight
: triggerDimensions.top

if (left + menuWidth > window.innerWidth) {
const leftWithMenuLeftOfTrigger =
triggerDimensions.left - menuWidth - TRIGGER_MARGIN
let left = triggerDimensions.right + TRIGGER_MARGIN

if (leftWithMenuLeftOfTrigger > 0) {
left = leftWithMenuLeftOfTrigger
}
}
if (left + menuWidth > window.innerWidth) {
const leftWithMenuLeftOfTrigger =
triggerDimensions.left - menuWidth - TRIGGER_MARGIN

setPositionStyle({ top, left })
if (leftWithMenuLeftOfTrigger > 0) {
left = leftWithMenuLeftOfTrigger
}
}
}
}, [triggerRef, modal.ref, setPosition, position])

React.useEffect(() => {
if (open) {
updatePosition()
}
}, [open, updatePosition])

React.useEffect(() => {
updatePosition()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [size, children])

if (!isClientSide) {
return null
}

if (fullScreenOnMobile && size.width < breakpoints.raw.phone) {
return (
<MenuContext.Provider value={modal}>
<Modal open={open} onClose={onClose}>
<MenuFullScreenContainer>{content}</MenuFullScreenContainer>
</Modal>
</MenuContext.Provider>
)
}

return ReactDOM.createPortal(
<MenuContext.Provider value={modal}>
{withOverlay && modal.state === 'opened' && <MenuOverlay />}
<MenuContainer
data-state={modal.state}
style={positionStyle}
ref={modal.ref}
data-testid="menu-container"
{...rest}
>
<MenuContent data-scrollable={scrollable}>{content}</MenuContent>
</MenuContainer>
</MenuContext.Provider>,
document.body

return { top, left }
},
[position, setPosition]
)

return (
<TogglePanel
children={getChildren}
data-testid="menu-container"
fullScreenOnMobile={fullScreenOnMobile}
open={open}
onClose={onClose}
ref={ref}
setStyle={setStyle}
{...props}
/>
)
}
)

export const Menu = withTriggerElement<HTMLUListElement>({ fowardRef: true })<
MenuInnerProps
>(InnerMenu)
export const Menu = withTriggerElement({ forwardRef: true })(InnerMenu)
2 changes: 1 addition & 1 deletion src/NavBar/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { NavBar } from './NavBar'

export { NavBarProps } from './NavBar.interface'
export type { NavBarProps } from './NavBar.interface'
8 changes: 0 additions & 8 deletions src/NavBarMenuItem/NavBarMenuItem.style.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import styled from 'styled-components'

import { MenuTriggerContainer } from '../Menu/Menu.style'

export const NavBarMenuItemContainer = styled.div`
& > ${MenuTriggerContainer} {
width: 100%;
height: 100%;
display: block;
}

&[data-bottom='true'] {
margin-top: auto;
}
Expand Down
4 changes: 2 additions & 2 deletions src/NavBarMenuItem/NavBarMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { NavBarItem } from '../NavBarItem'
import { NavBarMenuItemProps } from './NavBarMenuItem.interface'
import { NavBarMenuItemContainer } from './NavBarMenuItem.style'

const Content: React.FunctionComponent<{ modal: Modal<HTMLUListElement> }> = ({
const Content: React.FunctionComponent<{ modal: Modal<HTMLDivElement> }> = ({
modal,
children,
}) => {
Expand All @@ -24,7 +24,7 @@ const Content: React.FunctionComponent<{ modal: Modal<HTMLUListElement> }> = ({
}

export const NavBarMenuItem = React.forwardRef<
HTMLUListElement,
HTMLDivElement,
NavBarMenuItemProps
>(({ children, bottom, ...props }, ref) => (
<NavBarMenuItemContainer data-bottom={bottom}>
Expand Down
5 changes: 1 addition & 4 deletions src/Provider/Provider.context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import * as React from 'react'

export type ProviderContextValue = {
confirmLabel: string
cancelLabel: string
}
import { ProviderContextValue } from './Provider.interface'

export const ProviderContext = React.createContext<ProviderContextValue>({
confirmLabel: 'Valider',
Expand Down
5 changes: 5 additions & 0 deletions src/Provider/Provider.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export interface ProviderContextValue {
confirmLabel: string
cancelLabel: string
}

export interface ProviderProps {}

export type subscriptionCallback<Message, Options> = (
Expand Down
5 changes: 2 additions & 3 deletions src/Provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { Provider } from './Provider'
export { ProviderContext } from './Provider.context'

export { ProviderProps } from './Provider.interface'

export { ProviderContext, ProviderContextValue } from './Provider.context'
export type { ProviderContextValue, ProviderProps } from './Provider.interface'
Loading