diff --git a/src/AutocompleteInput/index.ts b/src/AutocompleteInput/index.ts index 4cae2ab76..2ea42642c 100644 --- a/src/AutocompleteInput/index.ts +++ b/src/AutocompleteInput/index.ts @@ -1,3 +1,3 @@ export { AutocompleteInput } from './AutocompleteInput' -export { AutocompleteInputProps } from './AutocompleteInput.interface' +export type { AutocompleteInputProps } from './AutocompleteInput.interface' diff --git a/src/Layout/index.ts b/src/Layout/index.ts index b4e238069..b7dec2029 100644 --- a/src/Layout/index.ts +++ b/src/Layout/index.ts @@ -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' diff --git a/src/Menu/Menu.interface.ts b/src/Menu/Menu.interface.ts index 139319fa2..41a0dc538 100644 --- a/src/Menu/Menu.interface.ts +++ b/src/Menu/Menu.interface.ts @@ -1,6 +1,4 @@ -import { Modal } from '@delangle/use-modal' -import * as React from 'react' - +import { TogglePanelProps } from '../TogglePanel' import { WithTriggerElement } from '../withTriggerElement' export interface MenuInstance { @@ -8,23 +6,16 @@ export interface MenuInstance { onClose: () => void } -export interface MenuInnerProps - extends React.HTMLAttributes, +export interface InnerMenuProps + extends Omit, MenuInstance { - fullScreenOnMobile?: boolean scrollable?: boolean position?: 'horizontal' | 'vertical' - triggerRef?: React.RefObject - withOverlay?: boolean setPosition?: (dimensions: { triggerDimensions: DOMRect menuHeight: number menuWidth: number }) => { top?: number; left?: number; right?: number; bottom?: number } - children?: - | React.ReactNode - | ((modal: Modal) => React.ReactNode) } -export interface MenuProps - extends WithTriggerElement {} +export type MenuProps = WithTriggerElement diff --git a/src/Menu/Menu.style.ts b/src/Menu/Menu.style.ts index 0d50ecdba..f02e5c888 100644 --- a/src/Menu/Menu.style.ts +++ b/src/Menu/Menu.style.ts @@ -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; @@ -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; ` diff --git a/src/Menu/Menu.tsx b/src/Menu/Menu.tsx index fc6c175f6..1add9c610 100644 --- a/src/Menu/Menu.tsx +++ b/src/Menu/Menu.tsx @@ -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() +import { MenuInstance, InnerMenuProps } from './Menu.interface' +import { FloatingMenu, FullScreenMenu } from './Menu.style' const TRIGGER_MARGIN = 12 -const InnerMenu = React.forwardRef( - (props, ref) => { - const { +const useOnlyOneMenuOpened = buildUseOnlyOpenedInstanceHook() + +export const InnerMenu = React.forwardRef( + ( + { 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({ - ref, - open, - onClose, - persistent: false, - animated: true, - animationDuration: ANIMATION_DURATIONS.m, - }) - - const content = isFunction(children) - ? children(modal as ModalType) - : 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) => { + const content = isFunction(children) ? children(modal) : children + + return fullScreenOnMobile && size.width < breakpoints.raw.phone ? ( + {content} + ) : ( + {content} ) - } 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 @@ -89,72 +72,43 @@ const InnerMenu = React.forwardRef( ? 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 ( - - - {content} - - - ) - } - - return ReactDOM.createPortal( - - {withOverlay && modal.state === 'opened' && } - - {content} - - , - document.body + + return { top, left } + }, + [position, setPosition] + ) + + return ( + ) } ) -export const Menu = withTriggerElement({ fowardRef: true })< - MenuInnerProps ->(InnerMenu) +export const Menu = withTriggerElement({ forwardRef: true })(InnerMenu) diff --git a/src/NavBar/index.ts b/src/NavBar/index.ts index 9a388688d..7c2b19beb 100644 --- a/src/NavBar/index.ts +++ b/src/NavBar/index.ts @@ -1,3 +1,3 @@ export { NavBar } from './NavBar' -export { NavBarProps } from './NavBar.interface' +export type { NavBarProps } from './NavBar.interface' diff --git a/src/NavBarMenuItem/NavBarMenuItem.style.ts b/src/NavBarMenuItem/NavBarMenuItem.style.ts index 49f3cd0bd..13fb848a1 100644 --- a/src/NavBarMenuItem/NavBarMenuItem.style.ts +++ b/src/NavBarMenuItem/NavBarMenuItem.style.ts @@ -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; } diff --git a/src/NavBarMenuItem/NavBarMenuItem.tsx b/src/NavBarMenuItem/NavBarMenuItem.tsx index 8d9fea9c8..d620991d2 100644 --- a/src/NavBarMenuItem/NavBarMenuItem.tsx +++ b/src/NavBarMenuItem/NavBarMenuItem.tsx @@ -8,7 +8,7 @@ import { NavBarItem } from '../NavBarItem' import { NavBarMenuItemProps } from './NavBarMenuItem.interface' import { NavBarMenuItemContainer } from './NavBarMenuItem.style' -const Content: React.FunctionComponent<{ modal: Modal }> = ({ +const Content: React.FunctionComponent<{ modal: Modal }> = ({ modal, children, }) => { @@ -24,7 +24,7 @@ const Content: React.FunctionComponent<{ modal: Modal }> = ({ } export const NavBarMenuItem = React.forwardRef< - HTMLUListElement, + HTMLDivElement, NavBarMenuItemProps >(({ children, bottom, ...props }, ref) => ( diff --git a/src/Provider/Provider.context.ts b/src/Provider/Provider.context.ts index 32ce48fa0..d6fd90fed 100644 --- a/src/Provider/Provider.context.ts +++ b/src/Provider/Provider.context.ts @@ -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({ confirmLabel: 'Valider', diff --git a/src/Provider/Provider.interface.ts b/src/Provider/Provider.interface.ts index 7f6a59ba2..01254a3de 100644 --- a/src/Provider/Provider.interface.ts +++ b/src/Provider/Provider.interface.ts @@ -1,3 +1,8 @@ +export interface ProviderContextValue { + confirmLabel: string + cancelLabel: string +} + export interface ProviderProps {} export type subscriptionCallback = ( diff --git a/src/Provider/index.ts b/src/Provider/index.ts index 36947fafb..10cf47a1e 100644 --- a/src/Provider/index.ts +++ b/src/Provider/index.ts @@ -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' diff --git a/src/Select/index.ts b/src/Select/index.ts index 66118830c..793306568 100644 --- a/src/Select/index.ts +++ b/src/Select/index.ts @@ -1,3 +1,3 @@ export { Select } from './Select' -export { SelectProps } from './Select.interface' +export type { SelectProps } from './Select.interface' diff --git a/src/SlideShow/index.ts b/src/SlideShow/index.ts index 846d415df..baa1d8727 100644 --- a/src/SlideShow/index.ts +++ b/src/SlideShow/index.ts @@ -1,3 +1,3 @@ export { SlideShow } from './SlideShow' -export { SlideShowProps } from './SlideShow.interface' +export type { SlideShowProps } from './SlideShow.interface' diff --git a/src/TogglePanel/TogglePanel.stories.tsx b/src/TogglePanel/TogglePanel.stories.tsx new file mode 100644 index 000000000..7896855ca --- /dev/null +++ b/src/TogglePanel/TogglePanel.stories.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' + +import { Background, BackgroundProps } from '../Background' +import { IconButton } from '../IconButton' +import { palette } from '../palette' +import { Text } from '../Text' + +import { TogglePanel } from '.' +import { TogglePanelProps } from './TogglePanel' + +export default { + title: 'Layouts/TogglePanel', + component: TogglePanel, +} + +const Component: React.FunctionComponent< + Partial & Pick +> = ({ setStyle, ...props }) => ( + } + > + + +) + +export const basic = () => ( + + Panel content + +) + +export const nested = () => ( + <> + + Panel content at default placement + + ({ + left: triggerDimensions.right, + top: triggerDimensions.top, + width: 300, + })} + > + Nested content panel placed at the right + + + +) diff --git a/src/TogglePanel/TogglePanel.style.ts b/src/TogglePanel/TogglePanel.style.ts new file mode 100644 index 000000000..e067bafbd --- /dev/null +++ b/src/TogglePanel/TogglePanel.style.ts @@ -0,0 +1,33 @@ +import styled from 'styled-components' + +import { zIndex } from '../_internal/zIndex' +import { animations } from '../animations' + +export const Container = styled.div` + position: fixed; + opacity: 1; + 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 Overlay = styled.div` + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: ${zIndex.dropDowns}; +` diff --git a/src/TogglePanel/TogglePanel.tsx b/src/TogglePanel/TogglePanel.tsx new file mode 100644 index 000000000..a1b59e136 --- /dev/null +++ b/src/TogglePanel/TogglePanel.tsx @@ -0,0 +1,124 @@ +import useModal, { Modal as ModalType } 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 { useWindowSize } from '../_internal/useWindowSize' +import { ANIMATION_DURATIONS } from '../animations' +import { breakpoints } from '../breakpoints' +import { Modal } from '../Modal' +import { WithTriggerElement, withTriggerElement } from '../withTriggerElement' + +import { Container, Overlay } from './TogglePanel.style' + +const Context = React.createContext | null>(null) + +const InnerTogglePanel = React.forwardRef< + HTMLDivElement, + InnerTogglePanelProps +>( + ( + { + children, + fullScreenOnMobile = false, + open, + onClose, + setStyle, + style, + triggerRef, + withOverlay = true, + ...props + }, + ref + ) => { + const [customStyle, setCustomStyle] = React.useState(style) + + const modal = useModal({ + ref, + open, + onClose, + persistent: false, + animated: true, + animationDuration: ANIMATION_DURATIONS.m, + }) + + const updateStyle = React.useCallback(() => { + if (!setStyle || !triggerRef?.current || !modal.ref?.current) { + return + } + + const dimensions = modal.ref.current.getBoundingClientRect() + const triggerDimensions = triggerRef.current.getBoundingClientRect() + + setCustomStyle({ ...style, ...setStyle(dimensions, triggerDimensions) }) + }, [modal.ref, style, triggerRef]) + + React.useEffect(() => { + if (open) { + updateStyle() + } + }, [open, updateStyle]) + + const size = useWindowSize() + + React.useEffect(updateStyle, [children, size]) // eslint-disable-line react-hooks/exhaustive-deps + + const parent = React.useContext(Context) + + if (!isClientSide) { + return null + } + + const content = isFunction(children) ? children(modal) : children + + return fullScreenOnMobile && size.width < breakpoints.raw.phone ? ( + + + {content} + + + ) : ( + ReactDOM.createPortal( + + {withOverlay && modal.state === 'opened' && } + + + {content} + + , + parent?.ref.current ?? document.body + ) + ) + } +) + +export const TogglePanel = withTriggerElement({ forwardRef: true })( + InnerTogglePanel +) + +interface InnerTogglePanelProps + extends React.HtmlHTMLAttributes { + children?: + | React.ReactNode + | ((modal: ModalType) => React.ReactNode) + fullScreenOnMobile?: boolean + onClose: () => void + open: boolean + setStyle?: ( + dimensions: DOMRect, + triggerDimensions: DOMRect + ) => React.CSSProperties + triggerRef?: React.RefObject + withOverlay?: boolean +} + +export type TogglePanelProps = WithTriggerElement< + InnerTogglePanelProps, + HTMLDivElement +> diff --git a/src/TogglePanel/index.ts b/src/TogglePanel/index.ts new file mode 100644 index 000000000..0db35b892 --- /dev/null +++ b/src/TogglePanel/index.ts @@ -0,0 +1,2 @@ +export { TogglePanel } from './TogglePanel' +export type { TogglePanelProps } from './TogglePanel' diff --git a/src/Tooltip/index.ts b/src/Tooltip/index.ts index fdaf6d1da..5f4dc2ca9 100644 --- a/src/Tooltip/index.ts +++ b/src/Tooltip/index.ts @@ -1,3 +1,3 @@ export { Tooltip } from './Tooltip' -export { TooltipProps } from './Tooltip.interface' +export type { TooltipProps } from './Tooltip.interface' diff --git a/src/index.ts b/src/index.ts index 9a3b64744..607615b99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,6 +88,7 @@ export { ExpansionPanelItem, ExpansionPanelItemProps, ControlledExpansionPanelIt export { Loader, LoaderProps } from './Loader' export { LoaderDots, LoaderDotsProps } from './LoaderDots' export { LoadingBar, LoadingBarProps } from './LoadingBar' +export { TogglePanel, TogglePanelProps } from './TogglePanel' export { SlideShow, SlideShowProps } from './SlideShow' export { Stepper, StepperProps, StepperStep } from './Stepper' diff --git a/src/withTriggerElement/withTriggerElement.interface.ts b/src/withTriggerElement/withTriggerElement.interface.ts index a784c5106..417aeac7e 100644 --- a/src/withTriggerElement/withTriggerElement.interface.ts +++ b/src/withTriggerElement/withTriggerElement.interface.ts @@ -4,12 +4,12 @@ type TriggerElement = ((state: TriggerState) => JSX.Element) | JSX.Element export interface TriggerReceivedProps { triggerElement?: TriggerElement - triggerRef?: React.RefObject + triggerRef?: React.RefObject onClose?: (e: React.SyntheticEvent) => void } export interface TriggerInjectedProps { - triggerRef?: React.RefObject + triggerRef?: React.RefObject open?: boolean } @@ -22,10 +22,9 @@ export type WithTriggerElement = Omit< BaseProps, 'open' | 'onClose' > & - TriggerReceivedProps & { - open?: boolean - } + TriggerInjectedProps & + TriggerReceivedProps export interface TriggerElementConfig { - fowardRef?: boolean + forwardRef?: boolean } diff --git a/src/withTriggerElement/withTriggerElement.tsx b/src/withTriggerElement/withTriggerElement.tsx index 7613b6826..c440c67d6 100644 --- a/src/withTriggerElement/withTriggerElement.tsx +++ b/src/withTriggerElement/withTriggerElement.tsx @@ -15,7 +15,7 @@ export const withTriggerElement = ( ) => ( WrappedComponent: React.ComponentType ) => { - const { fowardRef = false } = config + const { forwardRef = false } = config const Wrapper = React.forwardRef< RefElement, @@ -29,7 +29,7 @@ export const withTriggerElement = ( } = props as TriggerReceivedProps const [open, setOpen] = React.useState(false) - const triggerRef = useMergedRef(rawTriggerRef) + const triggerRef = useMergedRef(rawTriggerRef) const handleClose = React.useCallback( (e: React.SyntheticEvent) => { @@ -53,7 +53,7 @@ export const withTriggerElement = ( return triggerElement({ open, onClick: handleToggle, - ...(fowardRef ? { ref: triggerRef } : {}), + ...(forwardRef ? { ref: triggerRef } : {}), }) }