feat: Added a conflict resolution dialog and a Conflicts view for easier management of conflicts (#2337)
This commit is contained in:
@@ -7,13 +7,18 @@ import { getAppRect, getPopoverMaxHeight, getPositionedPopoverRect } from './Uti
|
||||
|
||||
const percentOf = (percent: number, value: number) => (percent / 100) * value
|
||||
|
||||
export type PopoverCSSProperties = CSSProperties & {
|
||||
'--translate-x': string
|
||||
'--translate-y': string
|
||||
}
|
||||
|
||||
const getStylesFromRect = (
|
||||
rect: DOMRect,
|
||||
options: {
|
||||
disableMobileFullscreenTakeover?: boolean
|
||||
maxHeight?: number | 'none'
|
||||
},
|
||||
): CSSProperties => {
|
||||
): PopoverCSSProperties => {
|
||||
const { disableMobileFullscreenTakeover = false, maxHeight = 'none' } = options
|
||||
|
||||
const canApplyMaxHeight = maxHeight !== 'none' && (!isMobileScreen() || disableMobileFullscreenTakeover)
|
||||
@@ -22,7 +27,9 @@ const getStylesFromRect = (
|
||||
|
||||
return {
|
||||
willChange: 'transform',
|
||||
transform: `translate(${shouldApplyMobileWidth ? marginForMobile / 2 : rect.x}px, ${rect.y}px)`,
|
||||
'--translate-x': `${shouldApplyMobileWidth ? marginForMobile / 2 : rect.x}px`,
|
||||
'--translate-y': `${rect.y}px`,
|
||||
transform: 'translate(var(--translate-x), var(--translate-y))',
|
||||
visibility: 'visible',
|
||||
...(canApplyMaxHeight && {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
@@ -53,7 +60,7 @@ export const getPositionedPopoverStyles = ({
|
||||
disableMobileFullscreenTakeover,
|
||||
maxHeightFunction,
|
||||
offset,
|
||||
}: Options): CSSProperties | null => {
|
||||
}: Options): PopoverCSSProperties | null => {
|
||||
if (!popoverRect || !anchorRect) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ const Popover = ({
|
||||
disableMobileFullscreenTakeover,
|
||||
maxHeight,
|
||||
portal,
|
||||
offset,
|
||||
hideOnClickInModal,
|
||||
}: Props) => {
|
||||
const popoverId = useRef(UuidGenerator.GenerateUuid())
|
||||
|
||||
@@ -126,6 +128,8 @@ const Popover = ({
|
||||
title={title}
|
||||
togglePopover={togglePopover}
|
||||
portal={portal}
|
||||
offset={offset}
|
||||
hideOnClickInModal={hideOnClickInModal}
|
||||
>
|
||||
{children}
|
||||
</PositionedPopoverContent>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useDocumentRect } from '@/Hooks/useDocumentRect'
|
||||
import { useAutoElementRect } from '@/Hooks/useElementRect'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useCallback, useLayoutEffect, useState } from 'react'
|
||||
import { CSSProperties, useCallback, useLayoutEffect, useState } from 'react'
|
||||
import Portal from '../Portal/Portal'
|
||||
import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
|
||||
import { PopoverCSSProperties, getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
|
||||
import { PopoverContentProps } from './Types'
|
||||
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { getAdjustedStylesForNonPortalPopover } from './Utils/getAdjustedStylesForNonPortal'
|
||||
|
||||
const PositionedPopoverContent = ({
|
||||
align = 'end',
|
||||
@@ -25,6 +26,8 @@ const PositionedPopoverContent = ({
|
||||
disableMobileFullscreenTakeover,
|
||||
maxHeight,
|
||||
portal = true,
|
||||
offset,
|
||||
hideOnClickInModal = false,
|
||||
}: PopoverContentProps) => {
|
||||
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
|
||||
const popoverRect = useAutoElementRect(popoverElement)
|
||||
@@ -47,13 +50,21 @@ const PositionedPopoverContent = ({
|
||||
side,
|
||||
disableMobileFullscreenTakeover: disableMobileFullscreenTakeover,
|
||||
maxHeightFunction: maxHeight,
|
||||
offset,
|
||||
})
|
||||
|
||||
let adjustedStyles: PopoverCSSProperties | undefined = undefined
|
||||
|
||||
if (!portal && popoverElement && styles) {
|
||||
adjustedStyles = getAdjustedStylesForNonPortalPopover(popoverElement, styles)
|
||||
}
|
||||
|
||||
usePopoverCloseOnClickOutside({
|
||||
popoverElement,
|
||||
anchorElement,
|
||||
togglePopover,
|
||||
childPopovers,
|
||||
hideOnClickInModal,
|
||||
disabled: disableClickOutside,
|
||||
})
|
||||
|
||||
@@ -83,9 +94,12 @@ const PositionedPopoverContent = ({
|
||||
isDesktopScreen || disableMobileFullscreenTakeover ? 'invisible' : '',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...styles,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
...styles,
|
||||
...adjustedStyles,
|
||||
} as CSSProperties
|
||||
}
|
||||
ref={setPopoverElement}
|
||||
data-popover={id}
|
||||
onKeyDown={(event) => {
|
||||
|
||||
@@ -42,6 +42,8 @@ type CommonPopoverProps = {
|
||||
disableMobileFullscreenTakeover?: boolean
|
||||
title: string
|
||||
portal?: boolean
|
||||
offset?: number
|
||||
hideOnClickInModal?: boolean
|
||||
}
|
||||
|
||||
export type PopoverContentProps = CommonPopoverProps & {
|
||||
|
||||
@@ -95,25 +95,25 @@ export const getPositionedPopoverRect = (
|
||||
if (side === 'top' || side === 'bottom') {
|
||||
switch (align) {
|
||||
case 'start':
|
||||
positionPopoverRect.x = buttonRect.left - finalOffset
|
||||
positionPopoverRect.x = buttonRect.left
|
||||
break
|
||||
case 'center':
|
||||
positionPopoverRect.x = buttonRect.left - width / 2 + buttonRect.width / 2 - finalOffset
|
||||
positionPopoverRect.x = buttonRect.left - width / 2 + buttonRect.width / 2
|
||||
break
|
||||
case 'end':
|
||||
positionPopoverRect.x = buttonRect.right - width + finalOffset
|
||||
positionPopoverRect.x = buttonRect.right - width
|
||||
break
|
||||
}
|
||||
} else {
|
||||
switch (align) {
|
||||
case 'start':
|
||||
positionPopoverRect.y = buttonRect.top - finalOffset
|
||||
positionPopoverRect.y = buttonRect.top
|
||||
break
|
||||
case 'center':
|
||||
positionPopoverRect.y = buttonRect.top - height / 2 + buttonRect.height / 2 - finalOffset
|
||||
positionPopoverRect.y = buttonRect.top - height / 2 + buttonRect.height / 2
|
||||
break
|
||||
case 'end':
|
||||
positionPopoverRect.y = buttonRect.bottom - height + finalOffset
|
||||
positionPopoverRect.y = buttonRect.bottom - height
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function getAbsolutePositionedParent(element: HTMLElement | null): HTMLElement | null {
|
||||
if (!element) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parent = element.parentElement
|
||||
|
||||
if (!parent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const position = window.getComputedStyle(parent).getPropertyValue('position')
|
||||
|
||||
if (position === 'absolute') {
|
||||
return parent
|
||||
}
|
||||
|
||||
return getAbsolutePositionedParent(parent)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { PopoverCSSProperties } from '../GetPositionedPopoverStyles'
|
||||
import { getAbsolutePositionedParent } from './getAbsolutePositionedParent'
|
||||
|
||||
export const getAdjustedStylesForNonPortalPopover = (popoverElement: HTMLElement, styles: PopoverCSSProperties) => {
|
||||
const absoluteParent = getAbsolutePositionedParent(popoverElement)
|
||||
const translateXProperty = styles?.['--translate-x']
|
||||
const translateYProperty = styles?.['--translate-y']
|
||||
|
||||
const parsedTranslateX = translateXProperty ? parseInt(translateXProperty) : 0
|
||||
const parsedTranslateY = translateYProperty ? parseInt(translateYProperty) : 0
|
||||
|
||||
if (!absoluteParent) {
|
||||
return styles
|
||||
}
|
||||
|
||||
const parentRect = absoluteParent.getBoundingClientRect()
|
||||
|
||||
const adjustedTranslateX = parsedTranslateX - parentRect.left
|
||||
const adjustedTranslateY = parsedTranslateY - parentRect.top
|
||||
|
||||
return {
|
||||
...styles,
|
||||
'--translate-x': `${adjustedTranslateX}px`,
|
||||
'--translate-y': `${adjustedTranslateY}px`,
|
||||
} as PopoverCSSProperties
|
||||
}
|
||||
@@ -6,6 +6,7 @@ type Options = {
|
||||
togglePopover?: () => void
|
||||
childPopovers: Set<string>
|
||||
disabled?: boolean
|
||||
hideOnClickInModal?: boolean
|
||||
}
|
||||
|
||||
export const usePopoverCloseOnClickOutside = ({
|
||||
@@ -14,6 +15,7 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
togglePopover,
|
||||
childPopovers,
|
||||
disabled,
|
||||
hideOnClickInModal = false,
|
||||
}: Options) => {
|
||||
useEffect(() => {
|
||||
const closeIfClickedOutside = (event: MouseEvent) => {
|
||||
@@ -26,7 +28,12 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
const isPopoverInModal = popoverElement?.closest('[data-dialog]')
|
||||
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[data-dialog]')
|
||||
|
||||
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfChildPopover && !isDescendantOfModal) {
|
||||
if (
|
||||
!isDescendantOfMenu &&
|
||||
!isAnchorElement &&
|
||||
!isDescendantOfChildPopover &&
|
||||
(!isDescendantOfModal || (isDescendantOfModal && hideOnClickInModal))
|
||||
) {
|
||||
if (!disabled) {
|
||||
togglePopover?.()
|
||||
}
|
||||
@@ -39,5 +46,5 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
document.removeEventListener('click', closeIfClickedOutside, { capture: true })
|
||||
document.removeEventListener('contextmenu', closeIfClickedOutside, { capture: true })
|
||||
}
|
||||
}, [anchorElement, childPopovers, popoverElement, togglePopover, disabled])
|
||||
}, [anchorElement, childPopovers, popoverElement, togglePopover, disabled, hideOnClickInModal])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user