@chayns-components/core 5.4.6 → 5.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/components/dropdown-body-wrapper/DropdownBodyWrapper.js +2 -5
- package/lib/cjs/components/dropdown-body-wrapper/DropdownBodyWrapper.js.map +1 -1
- package/lib/cjs/hooks/dropdown.js +40 -11
- package/lib/cjs/hooks/dropdown.js.map +1 -1
- package/lib/esm/components/dropdown-body-wrapper/DropdownBodyWrapper.js +2 -5
- package/lib/esm/components/dropdown-body-wrapper/DropdownBodyWrapper.js.map +1 -1
- package/lib/esm/hooks/dropdown.js +41 -12
- package/lib/esm/hooks/dropdown.js.map +1 -1
- package/package.json +2 -2
|
@@ -54,13 +54,10 @@ const DropdownBodyWrapper = /*#__PURE__*/(0, _react.forwardRef)(({
|
|
|
54
54
|
shouldShowDropdown
|
|
55
55
|
});
|
|
56
56
|
(0, _react.useEffect)(() => {
|
|
57
|
-
|
|
58
|
-
// the dropdown is closed would trigger unnecessary re-renders of the consumer (e.g. the
|
|
59
|
-
// ComboBox) during the initial layout, which can cause visible layout shifts.
|
|
60
|
-
if (shouldShowDropdown && typeof onAvailableMaxHeightChange === 'function') {
|
|
57
|
+
if (typeof onAvailableMaxHeightChange === 'function') {
|
|
61
58
|
onAvailableMaxHeightChange(availableMaxHeight);
|
|
62
59
|
}
|
|
63
|
-
}, [availableMaxHeight, onAvailableMaxHeightChange
|
|
60
|
+
}, [availableMaxHeight, onAvailableMaxHeightChange]);
|
|
64
61
|
const handleClose = (0, _react.useCallback)(() => {
|
|
65
62
|
if (typeof onClose === 'function') {
|
|
66
63
|
onClose();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownBodyWrapper.js","names":["_react","_interopRequireWildcard","require","_DropdownBodyWrapper","_reactDom","_dropdown","_DelayedDropdownContent","_interopRequireDefault","_dropdown2","_container","e","__esModule","default","t","WeakMap","r","n","o","i","f","__proto__","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","DropdownBodyWrapper","forwardRef","anchorElement","bodyWidth","children","container","containerProp","contentHeight","direction","DropdownDirection","BOTTOM_RIGHT","maxHeight","minBodyWidth","onAvailableMaxHeightChange","onClose","onOutsideClick","onMeasure","shouldCaptureEvents","shouldShowDropdown","ref","isInChaynsWalletRef","useRef","measuredContentHeight","setMeasuredContentHeight","useState","measuredContentWidth","setMeasuredContentWidth","portal","setPortal","contentRef","shouldPreventClickRef","touchTimeoutRef","undefined","useContainer","transform","width","coordinates","availableMaxHeight","useDropdown","contentWidth","useEffect","handleClose","useCallback","handleClick","event","current","contains","target","preventDefault","stopPropagation","shouldPreventCloseOnClick","handleContentMeasure","measurements","height","handleTouchEnd","clearTimeout","handleTouchStart","window","setTimeout","useDropdownListener","onClick","onTouchEnd","onTouchStart","isBottomDirection","BOTTOM","BOTTOM_LEFT","includes","reservationWrapperElement","closest","ContainerAnchor","RESERVATION_WRAPPER","availableHeight","innerHeight","getBoundingClientRect","bottom","additionalNeededSpace","style","marginBottom","createPortal","createElement","shouldShowContent","StyledDropdownBodyWrapperContent","$width","$minWidth","$maxHeight","$direction","className","tabIndex","useImperativeHandle","StyledDropdownBodyWrapper","displayName","_default","exports"],"sources":["../../../../src/components/dropdown-body-wrapper/DropdownBodyWrapper.tsx"],"sourcesContent":["import React, {\n forwardRef,\n ReactNode,\n ReactPortal,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport {\n StyledDropdownBodyWrapper,\n StyledDropdownBodyWrapperContent,\n} from './DropdownBodyWrapper.styles';\nimport { createPortal } from 'react-dom';\nimport { DropdownDirection, DropdownMeasurements } from '../../types/dropdown';\nimport DelayedDropdownContent, {\n DelayedDropdownContentProps,\n} from './delayed-dropdown-content/DelayedDropdownContent';\nimport { useDropdown, useDropdownListener } from '../../hooks/dropdown';\nimport { ContainerAnchor, useContainer } from '../../hooks/container';\n\ninterface DropdownBodyWrapperProps {\n /**\n * The anchor element of the dropdown.\n */\n anchorElement: Element;\n /**\n * The width of the Body.\n */\n bodyWidth?: number;\n /**\n * The content of the dropdown body.\n */\n children: ReactNode;\n /**\n * The element where the content should be rendered via React Portal.\n */\n container?: Element;\n /**\n * The height of the content\n */\n contentHeight?: number;\n /**\n * The direction of the dropdown.\n */\n direction?: DropdownDirection;\n /**\n * The max height of the dropdown.\n */\n maxHeight?: number;\n /**\n * The minimum width of the body.\n */\n minBodyWidth?: number;\n /**\n * Function to be executed when the available maximum height of the dropdown changes. The\n * available maximum height is the space that is available for the dropdown content until the\n * edge of the container is reached (minus a small spacing). This can be used to limit the\n * content height, so the dropdown is not cut off when there is not enough space.\n */\n onAvailableMaxHeightChange?: (availableMaxHeight: number) => void;\n /**\n * Function to be executed when the body is closed.\n */\n onClose?: VoidFunction;\n /**\n * Function to be executed when the user clicks outside the dropdown.\n * If the function returns `true`, the dropdown will not be closed.\n */\n onOutsideClick?: () => boolean | void;\n /**\n * Function to be executed when the content is measured.\n */\n onMeasure?: DelayedDropdownContentProps['onMeasure'];\n /**\n * Whether the dropdown should be visible.\n */\n shouldShowDropdown: boolean;\n /**\n * Whether the outside events should be captured.\n */\n shouldCaptureEvents?: boolean;\n}\n\nconst DropdownBodyWrapper = forwardRef<HTMLDivElement, DropdownBodyWrapperProps>(\n (\n {\n anchorElement,\n bodyWidth,\n children,\n container: containerProp,\n contentHeight = 0,\n direction = DropdownDirection.BOTTOM_RIGHT,\n maxHeight,\n minBodyWidth = 0,\n onAvailableMaxHeightChange,\n onClose,\n onOutsideClick,\n onMeasure,\n shouldCaptureEvents = true,\n shouldShowDropdown,\n },\n ref,\n ) => {\n const isInChaynsWalletRef = useRef(false);\n\n const [measuredContentHeight, setMeasuredContentHeight] = useState<number>(0);\n const [measuredContentWidth, setMeasuredContentWidth] = useState<number>(0);\n const [portal, setPortal] = useState<ReactPortal>();\n\n const contentRef = useRef<HTMLDivElement>(null);\n const shouldPreventClickRef = useRef<boolean>(false);\n const touchTimeoutRef = useRef<number | undefined>(undefined);\n\n const container = useContainer({ anchorElement, container: containerProp });\n\n const { transform, width, coordinates, availableMaxHeight } = useDropdown({\n anchorElement,\n container,\n contentHeight,\n contentWidth: bodyWidth ?? measuredContentWidth,\n direction,\n shouldShowDropdown,\n });\n\n useEffect(() => {\n // Only report the available max height while the dropdown is shown. Reporting it while\n // the dropdown is closed would trigger unnecessary re-renders of the consumer (e.g. the\n // ComboBox) during the initial layout, which can cause visible layout shifts.\n if (shouldShowDropdown && typeof onAvailableMaxHeightChange === 'function') {\n onAvailableMaxHeightChange(availableMaxHeight);\n }\n }, [availableMaxHeight, onAvailableMaxHeightChange, shouldShowDropdown]);\n\n const handleClose = useCallback(() => {\n if (typeof onClose === 'function') {\n onClose();\n }\n }, [onClose]);\n\n /**\n * This function closes the body\n */\n const handleClick = useCallback(\n (event: MouseEvent) => {\n if (\n contentRef.current &&\n shouldShowDropdown &&\n !anchorElement.contains(event.target as Node) &&\n !contentRef.current.contains(event.target as Node)\n ) {\n event.preventDefault();\n event.stopPropagation();\n\n const shouldPreventCloseOnClick = onOutsideClick?.() ?? false;\n\n if (!shouldPreventClickRef.current && !shouldPreventCloseOnClick) {\n handleClose();\n }\n }\n\n shouldPreventClickRef.current = false;\n },\n [anchorElement, handleClose, onOutsideClick, shouldShowDropdown],\n );\n\n const handleContentMeasure = useCallback(\n (measurements: DropdownMeasurements) => {\n // Measurements are only needed if the content is shown in the chayns wallet. To prevent\n // unnecessary renders, we only set the height if the content is shown in the wallet.\n if (isInChaynsWalletRef.current) {\n setMeasuredContentHeight(measurements.height);\n }\n\n setMeasuredContentWidth(measurements.width);\n\n if (typeof onMeasure === 'function') {\n onMeasure(measurements);\n }\n },\n [onMeasure],\n );\n\n const handleTouchEnd = useCallback(() => {\n clearTimeout(touchTimeoutRef.current);\n }, []);\n\n const handleTouchStart = useCallback(() => {\n touchTimeoutRef.current = window.setTimeout(() => {\n shouldPreventClickRef.current = true;\n }, 500);\n }, []);\n\n /**\n * This hook listens for clicks\n */\n useDropdownListener({\n onClick: handleClick,\n onClose: handleClose,\n onTouchEnd: handleTouchEnd,\n onTouchStart: handleTouchStart,\n shouldCaptureEvents,\n });\n\n useEffect(() => {\n const isBottomDirection = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n const reservationWrapperElement = anchorElement.closest<HTMLDivElement>(\n ContainerAnchor.RESERVATION_WRAPPER,\n );\n\n isInChaynsWalletRef.current =\n !!(\n reservationWrapperElement && reservationWrapperElement.contains(anchorElement)\n ) || true;\n\n // This effect checks if additional space is needed to show dropdown content in chayns cards.\n if (\n isBottomDirection &&\n isInChaynsWalletRef.current &&\n measuredContentHeight > 0 &&\n reservationWrapperElement &&\n shouldShowDropdown\n ) {\n const availableHeight =\n window.innerHeight - anchorElement.getBoundingClientRect().bottom;\n\n // If the content height is greater than the available height, we need to add additional space.\n // This is to ensure that the dropdown content is fully visible. The 16 pixels are a buffer for shadows.\n const additionalNeededSpace = measuredContentHeight + 16 - availableHeight;\n\n if (additionalNeededSpace > 0) {\n // Add margin bottom to the reservation wrapper to ensure the dropdown content is fully visible.\n reservationWrapperElement.style.marginBottom = `${additionalNeededSpace}px`;\n } else {\n // Reset the margin bottom if no additional space is needed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n }\n\n if (isInChaynsWalletRef.current && reservationWrapperElement && !shouldShowDropdown) {\n // Reset the margin bottom when the dropdown is closed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n\n return () => {\n if (reservationWrapperElement) {\n reservationWrapperElement.style.marginBottom = '0px';\n }\n };\n }, [anchorElement, direction, measuredContentHeight, shouldShowDropdown]);\n\n useEffect(() => {\n if (!container) return;\n\n setPortal(() =>\n createPortal(\n <DelayedDropdownContent\n coordinates={coordinates}\n onMeasure={handleContentMeasure}\n shouldShowContent={shouldShowDropdown}\n transform={transform}\n >\n <StyledDropdownBodyWrapperContent\n $width={width}\n $minWidth={minBodyWidth}\n $maxHeight={maxHeight}\n $direction={direction}\n ref={contentRef}\n className={\n typeof maxHeight === 'number' ? 'chayns-scrollbar' : undefined\n }\n tabIndex={0}\n >\n {children}\n </StyledDropdownBodyWrapperContent>\n </DelayedDropdownContent>,\n container,\n ),\n );\n }, [\n children,\n container,\n coordinates,\n direction,\n handleContentMeasure,\n maxHeight,\n minBodyWidth,\n shouldShowDropdown,\n transform,\n width,\n ]);\n\n useImperativeHandle(ref, () => contentRef.current!, []);\n\n return <StyledDropdownBodyWrapper>{portal}</StyledDropdownBodyWrapper>;\n },\n);\n\nDropdownBodyWrapper.displayName = 'DropdownBodyWrapper';\n\nexport default DropdownBodyWrapper;\n"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AAUA,IAAAC,oBAAA,GAAAD,OAAA;AAIA,IAAAE,SAAA,GAAAF,OAAA;AACA,IAAAG,SAAA,GAAAH,OAAA;AACA,IAAAI,uBAAA,GAAAC,sBAAA,CAAAL,OAAA;AAGA,IAAAM,UAAA,GAAAN,OAAA;AACA,IAAAO,UAAA,GAAAP,OAAA;AAAsE,SAAAK,uBAAAG,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAAA,SAAAT,wBAAAS,CAAA,EAAAG,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAb,uBAAA,YAAAA,CAAAS,CAAA,EAAAG,CAAA,SAAAA,CAAA,IAAAH,CAAA,IAAAA,CAAA,CAAAC,UAAA,SAAAD,CAAA,MAAAO,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAR,OAAA,EAAAF,CAAA,iBAAAA,CAAA,uBAAAA,CAAA,yBAAAA,CAAA,SAAAS,CAAA,MAAAF,CAAA,GAAAJ,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAE,CAAA,CAAAI,GAAA,CAAAX,CAAA,UAAAO,CAAA,CAAAK,GAAA,CAAAZ,CAAA,GAAAO,CAAA,CAAAM,GAAA,CAAAb,CAAA,EAAAS,CAAA,gBAAAN,CAAA,IAAAH,CAAA,gBAAAG,CAAA,OAAAW,cAAA,CAAAC,IAAA,CAAAf,CAAA,EAAAG,CAAA,OAAAK,CAAA,IAAAD,CAAA,GAAAS,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAG,CAAA,OAAAK,CAAA,CAAAI,GAAA,IAAAJ,CAAA,CAAAK,GAAA,IAAAN,CAAA,CAAAE,CAAA,EAAAN,CAAA,EAAAK,CAAA,IAAAC,CAAA,CAAAN,CAAA,IAAAH,CAAA,CAAAG,CAAA,WAAAM,CAAA,KAAAT,CAAA,EAAAG,CAAA;AAiEtE,MAAMgB,mBAAmB,gBAAG,IAAAC,iBAAU,EAClC,CACI;EACIC,aAAa;EACbC,SAAS;EACTC,QAAQ;EACRC,SAAS,EAAEC,aAAa;EACxBC,aAAa,GAAG,CAAC;EACjBC,SAAS,GAAGC,2BAAiB,CAACC,YAAY;EAC1CC,SAAS;EACTC,YAAY,GAAG,CAAC;EAChBC,0BAA0B;EAC1BC,OAAO;EACPC,cAAc;EACdC,SAAS;EACTC,mBAAmB,GAAG,IAAI;EAC1BC;AACJ,CAAC,EACDC,GAAG,KACF;EACD,MAAMC,mBAAmB,GAAG,IAAAC,aAAM,EAAC,KAAK,CAAC;EAEzC,MAAM,CAACC,qBAAqB,EAAEC,wBAAwB,CAAC,GAAG,IAAAC,eAAQ,EAAS,CAAC,CAAC;EAC7E,MAAM,CAACC,oBAAoB,EAAEC,uBAAuB,CAAC,GAAG,IAAAF,eAAQ,EAAS,CAAC,CAAC;EAC3E,MAAM,CAACG,MAAM,EAAEC,SAAS,CAAC,GAAG,IAAAJ,eAAQ,EAAc,CAAC;EAEnD,MAAMK,UAAU,GAAG,IAAAR,aAAM,EAAiB,IAAI,CAAC;EAC/C,MAAMS,qBAAqB,GAAG,IAAAT,aAAM,EAAU,KAAK,CAAC;EACpD,MAAMU,eAAe,GAAG,IAAAV,aAAM,EAAqBW,SAAS,CAAC;EAE7D,MAAM3B,SAAS,GAAG,IAAA4B,uBAAY,EAAC;IAAE/B,aAAa;IAAEG,SAAS,EAAEC;EAAc,CAAC,CAAC;EAE3E,MAAM;IAAE4B,SAAS;IAAEC,KAAK;IAAEC,WAAW;IAAEC;EAAmB,CAAC,GAAG,IAAAC,sBAAW,EAAC;IACtEpC,aAAa;IACbG,SAAS;IACTE,aAAa;IACbgC,YAAY,EAAEpC,SAAS,IAAIsB,oBAAoB;IAC/CjB,SAAS;IACTU;EACJ,CAAC,CAAC;EAEF,IAAAsB,gBAAS,EAAC,MAAM;IACZ;IACA;IACA;IACA,IAAItB,kBAAkB,IAAI,OAAOL,0BAA0B,KAAK,UAAU,EAAE;MACxEA,0BAA0B,CAACwB,kBAAkB,CAAC;IAClD;EACJ,CAAC,EAAE,CAACA,kBAAkB,EAAExB,0BAA0B,EAAEK,kBAAkB,CAAC,CAAC;EAExE,MAAMuB,WAAW,GAAG,IAAAC,kBAAW,EAAC,MAAM;IAClC,IAAI,OAAO5B,OAAO,KAAK,UAAU,EAAE;MAC/BA,OAAO,CAAC,CAAC;IACb;EACJ,CAAC,EAAE,CAACA,OAAO,CAAC,CAAC;;EAEb;AACR;AACA;EACQ,MAAM6B,WAAW,GAAG,IAAAD,kBAAW,EAC1BE,KAAiB,IAAK;IACnB,IACIf,UAAU,CAACgB,OAAO,IAClB3B,kBAAkB,IAClB,CAAChB,aAAa,CAAC4C,QAAQ,CAACF,KAAK,CAACG,MAAc,CAAC,IAC7C,CAAClB,UAAU,CAACgB,OAAO,CAACC,QAAQ,CAACF,KAAK,CAACG,MAAc,CAAC,EACpD;MACEH,KAAK,CAACI,cAAc,CAAC,CAAC;MACtBJ,KAAK,CAACK,eAAe,CAAC,CAAC;MAEvB,MAAMC,yBAAyB,GAAG,CAAAnC,cAAc,aAAdA,cAAc,uBAAdA,cAAc,CAAG,CAAC,KAAI,KAAK;MAE7D,IAAI,CAACe,qBAAqB,CAACe,OAAO,IAAI,CAACK,yBAAyB,EAAE;QAC9DT,WAAW,CAAC,CAAC;MACjB;IACJ;IAEAX,qBAAqB,CAACe,OAAO,GAAG,KAAK;EACzC,CAAC,EACD,CAAC3C,aAAa,EAAEuC,WAAW,EAAE1B,cAAc,EAAEG,kBAAkB,CACnE,CAAC;EAED,MAAMiC,oBAAoB,GAAG,IAAAT,kBAAW,EACnCU,YAAkC,IAAK;IACpC;IACA;IACA,IAAIhC,mBAAmB,CAACyB,OAAO,EAAE;MAC7BtB,wBAAwB,CAAC6B,YAAY,CAACC,MAAM,CAAC;IACjD;IAEA3B,uBAAuB,CAAC0B,YAAY,CAACjB,KAAK,CAAC;IAE3C,IAAI,OAAOnB,SAAS,KAAK,UAAU,EAAE;MACjCA,SAAS,CAACoC,YAAY,CAAC;IAC3B;EACJ,CAAC,EACD,CAACpC,SAAS,CACd,CAAC;EAED,MAAMsC,cAAc,GAAG,IAAAZ,kBAAW,EAAC,MAAM;IACrCa,YAAY,CAACxB,eAAe,CAACc,OAAO,CAAC;EACzC,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMW,gBAAgB,GAAG,IAAAd,kBAAW,EAAC,MAAM;IACvCX,eAAe,CAACc,OAAO,GAAGY,MAAM,CAACC,UAAU,CAAC,MAAM;MAC9C5B,qBAAqB,CAACe,OAAO,GAAG,IAAI;IACxC,CAAC,EAAE,GAAG,CAAC;EACX,CAAC,EAAE,EAAE,CAAC;;EAEN;AACR;AACA;EACQ,IAAAc,8BAAmB,EAAC;IAChBC,OAAO,EAAEjB,WAAW;IACpB7B,OAAO,EAAE2B,WAAW;IACpBoB,UAAU,EAAEP,cAAc;IAC1BQ,YAAY,EAAEN,gBAAgB;IAC9BvC;EACJ,CAAC,CAAC;EAEF,IAAAuB,gBAAS,EAAC,MAAM;IACZ,MAAMuB,iBAAiB,GAAG,CACtBtD,2BAAiB,CAACuD,MAAM,EACxBvD,2BAAiB,CAACwD,WAAW,EAC7BxD,2BAAiB,CAACC,YAAY,CACjC,CAACwD,QAAQ,CAAC1D,SAAS,CAAC;IAErB,MAAM2D,yBAAyB,GAAGjE,aAAa,CAACkE,OAAO,CACnDC,0BAAe,CAACC,mBACpB,CAAC;IAEDlD,mBAAmB,CAACyB,OAAO,GACvB,CAAC,EACGsB,yBAAyB,IAAIA,yBAAyB,CAACrB,QAAQ,CAAC5C,aAAa,CAAC,CACjF,IAAI,IAAI;;IAEb;IACA,IACI6D,iBAAiB,IACjB3C,mBAAmB,CAACyB,OAAO,IAC3BvB,qBAAqB,GAAG,CAAC,IACzB6C,yBAAyB,IACzBjD,kBAAkB,EACpB;MACE,MAAMqD,eAAe,GACjBd,MAAM,CAACe,WAAW,GAAGtE,aAAa,CAACuE,qBAAqB,CAAC,CAAC,CAACC,MAAM;;MAErE;MACA;MACA,MAAMC,qBAAqB,GAAGrD,qBAAqB,GAAG,EAAE,GAAGiD,eAAe;MAE1E,IAAII,qBAAqB,GAAG,CAAC,EAAE;QAC3B;QACAR,yBAAyB,CAACS,KAAK,CAACC,YAAY,GAAG,GAAGF,qBAAqB,IAAI;MAC/E,CAAC,MAAM;QACH;QACAR,yBAAyB,CAACS,KAAK,CAACC,YAAY,GAAG,KAAK;MACxD;IACJ;IAEA,IAAIzD,mBAAmB,CAACyB,OAAO,IAAIsB,yBAAyB,IAAI,CAACjD,kBAAkB,EAAE;MACjF;MACAiD,yBAAyB,CAACS,KAAK,CAACC,YAAY,GAAG,KAAK;IACxD;IAEA,OAAO,MAAM;MACT,IAAIV,yBAAyB,EAAE;QAC3BA,yBAAyB,CAACS,KAAK,CAACC,YAAY,GAAG,KAAK;MACxD;IACJ,CAAC;EACL,CAAC,EAAE,CAAC3E,aAAa,EAAEM,SAAS,EAAEc,qBAAqB,EAAEJ,kBAAkB,CAAC,CAAC;EAEzE,IAAAsB,gBAAS,EAAC,MAAM;IACZ,IAAI,CAACnC,SAAS,EAAE;IAEhBuB,SAAS,CAAC,mBACN,IAAAkD,sBAAY,eACR3G,MAAA,CAAAY,OAAA,CAAAgG,aAAA,CAACtG,uBAAA,CAAAM,OAAsB;MACnBqD,WAAW,EAAEA,WAAY;MACzBpB,SAAS,EAAEmC,oBAAqB;MAChC6B,iBAAiB,EAAE9D,kBAAmB;MACtCgB,SAAS,EAAEA;IAAU,gBAErB/D,MAAA,CAAAY,OAAA,CAAAgG,aAAA,CAACzG,oBAAA,CAAA2G,gCAAgC;MAC7BC,MAAM,EAAE/C,KAAM;MACdgD,SAAS,EAAEvE,YAAa;MACxBwE,UAAU,EAAEzE,SAAU;MACtB0E,UAAU,EAAE7E,SAAU;MACtBW,GAAG,EAAEU,UAAW;MAChByD,SAAS,EACL,OAAO3E,SAAS,KAAK,QAAQ,GAAG,kBAAkB,GAAGqB,SACxD;MACDuD,QAAQ,EAAE;IAAE,GAEXnF,QAC6B,CACd,CAAC,EACzBC,SACJ,CACJ,CAAC;EACL,CAAC,EAAE,CACCD,QAAQ,EACRC,SAAS,EACT+B,WAAW,EACX5B,SAAS,EACT2C,oBAAoB,EACpBxC,SAAS,EACTC,YAAY,EACZM,kBAAkB,EAClBgB,SAAS,EACTC,KAAK,CACR,CAAC;EAEF,IAAAqD,0BAAmB,EAACrE,GAAG,EAAE,MAAMU,UAAU,CAACgB,OAAQ,EAAE,EAAE,CAAC;EAEvD,oBAAO1E,MAAA,CAAAY,OAAA,CAAAgG,aAAA,CAACzG,oBAAA,CAAAmH,yBAAyB,QAAE9D,MAAkC,CAAC;AAC1E,CACJ,CAAC;AAED3B,mBAAmB,CAAC0F,WAAW,GAAG,qBAAqB;AAAC,IAAAC,QAAA,GAAAC,OAAA,CAAA7G,OAAA,GAEzCiB,mBAAmB","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"DropdownBodyWrapper.js","names":["_react","_interopRequireWildcard","require","_DropdownBodyWrapper","_reactDom","_dropdown","_DelayedDropdownContent","_interopRequireDefault","_dropdown2","_container","e","__esModule","default","t","WeakMap","r","n","o","i","f","__proto__","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","DropdownBodyWrapper","forwardRef","anchorElement","bodyWidth","children","container","containerProp","contentHeight","direction","DropdownDirection","BOTTOM_RIGHT","maxHeight","minBodyWidth","onAvailableMaxHeightChange","onClose","onOutsideClick","onMeasure","shouldCaptureEvents","shouldShowDropdown","ref","isInChaynsWalletRef","useRef","measuredContentHeight","setMeasuredContentHeight","useState","measuredContentWidth","setMeasuredContentWidth","portal","setPortal","contentRef","shouldPreventClickRef","touchTimeoutRef","undefined","useContainer","transform","width","coordinates","availableMaxHeight","useDropdown","contentWidth","useEffect","handleClose","useCallback","handleClick","event","current","contains","target","preventDefault","stopPropagation","shouldPreventCloseOnClick","handleContentMeasure","measurements","height","handleTouchEnd","clearTimeout","handleTouchStart","window","setTimeout","useDropdownListener","onClick","onTouchEnd","onTouchStart","isBottomDirection","BOTTOM","BOTTOM_LEFT","includes","reservationWrapperElement","closest","ContainerAnchor","RESERVATION_WRAPPER","availableHeight","innerHeight","getBoundingClientRect","bottom","additionalNeededSpace","style","marginBottom","createPortal","createElement","shouldShowContent","StyledDropdownBodyWrapperContent","$width","$minWidth","$maxHeight","$direction","className","tabIndex","useImperativeHandle","StyledDropdownBodyWrapper","displayName","_default","exports"],"sources":["../../../../src/components/dropdown-body-wrapper/DropdownBodyWrapper.tsx"],"sourcesContent":["import React, {\n forwardRef,\n ReactNode,\n ReactPortal,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport {\n StyledDropdownBodyWrapper,\n StyledDropdownBodyWrapperContent,\n} from './DropdownBodyWrapper.styles';\nimport { createPortal } from 'react-dom';\nimport { DropdownDirection, DropdownMeasurements } from '../../types/dropdown';\nimport DelayedDropdownContent, {\n DelayedDropdownContentProps,\n} from './delayed-dropdown-content/DelayedDropdownContent';\nimport { useDropdown, useDropdownListener } from '../../hooks/dropdown';\nimport { ContainerAnchor, useContainer } from '../../hooks/container';\n\ninterface DropdownBodyWrapperProps {\n /**\n * The anchor element of the dropdown.\n */\n anchorElement: Element;\n /**\n * The width of the Body.\n */\n bodyWidth?: number;\n /**\n * The content of the dropdown body.\n */\n children: ReactNode;\n /**\n * The element where the content should be rendered via React Portal.\n */\n container?: Element;\n /**\n * The height of the content\n */\n contentHeight?: number;\n /**\n * The direction of the dropdown.\n */\n direction?: DropdownDirection;\n /**\n * The max height of the dropdown.\n */\n maxHeight?: number;\n /**\n * The minimum width of the body.\n */\n minBodyWidth?: number;\n /**\n * Function to be executed when the available maximum height of the dropdown changes. The\n * available maximum height is the space that is available for the dropdown content until the\n * edge of the container is reached (minus a small spacing). This can be used to limit the\n * content height, so the dropdown is not cut off when there is not enough space.\n */\n onAvailableMaxHeightChange?: (availableMaxHeight: number) => void;\n /**\n * Function to be executed when the body is closed.\n */\n onClose?: VoidFunction;\n /**\n * Function to be executed when the user clicks outside the dropdown.\n * If the function returns `true`, the dropdown will not be closed.\n */\n onOutsideClick?: () => boolean | void;\n /**\n * Function to be executed when the content is measured.\n */\n onMeasure?: DelayedDropdownContentProps['onMeasure'];\n /**\n * Whether the dropdown should be visible.\n */\n shouldShowDropdown: boolean;\n /**\n * Whether the outside events should be captured.\n */\n shouldCaptureEvents?: boolean;\n}\n\nconst DropdownBodyWrapper = forwardRef<HTMLDivElement, DropdownBodyWrapperProps>(\n (\n {\n anchorElement,\n bodyWidth,\n children,\n container: containerProp,\n contentHeight = 0,\n direction = DropdownDirection.BOTTOM_RIGHT,\n maxHeight,\n minBodyWidth = 0,\n onAvailableMaxHeightChange,\n onClose,\n onOutsideClick,\n onMeasure,\n shouldCaptureEvents = true,\n shouldShowDropdown,\n },\n ref,\n ) => {\n const isInChaynsWalletRef = useRef(false);\n\n const [measuredContentHeight, setMeasuredContentHeight] = useState<number>(0);\n const [measuredContentWidth, setMeasuredContentWidth] = useState<number>(0);\n const [portal, setPortal] = useState<ReactPortal>();\n\n const contentRef = useRef<HTMLDivElement>(null);\n const shouldPreventClickRef = useRef<boolean>(false);\n const touchTimeoutRef = useRef<number | undefined>(undefined);\n\n const container = useContainer({ anchorElement, container: containerProp });\n\n const { transform, width, coordinates, availableMaxHeight } = useDropdown({\n anchorElement,\n container,\n contentHeight,\n contentWidth: bodyWidth ?? measuredContentWidth,\n direction,\n shouldShowDropdown,\n });\n\n useEffect(() => {\n if (typeof onAvailableMaxHeightChange === 'function') {\n onAvailableMaxHeightChange(availableMaxHeight);\n }\n }, [availableMaxHeight, onAvailableMaxHeightChange]);\n\n const handleClose = useCallback(() => {\n if (typeof onClose === 'function') {\n onClose();\n }\n }, [onClose]);\n\n /**\n * This function closes the body\n */\n const handleClick = useCallback(\n (event: MouseEvent) => {\n if (\n contentRef.current &&\n shouldShowDropdown &&\n !anchorElement.contains(event.target as Node) &&\n !contentRef.current.contains(event.target as Node)\n ) {\n event.preventDefault();\n event.stopPropagation();\n\n const shouldPreventCloseOnClick = onOutsideClick?.() ?? false;\n\n if (!shouldPreventClickRef.current && !shouldPreventCloseOnClick) {\n handleClose();\n }\n }\n\n shouldPreventClickRef.current = false;\n },\n [anchorElement, handleClose, onOutsideClick, shouldShowDropdown],\n );\n\n const handleContentMeasure = useCallback(\n (measurements: DropdownMeasurements) => {\n // Measurements are only needed if the content is shown in the chayns wallet. To prevent\n // unnecessary renders, we only set the height if the content is shown in the wallet.\n if (isInChaynsWalletRef.current) {\n setMeasuredContentHeight(measurements.height);\n }\n\n setMeasuredContentWidth(measurements.width);\n\n if (typeof onMeasure === 'function') {\n onMeasure(measurements);\n }\n },\n [onMeasure],\n );\n\n const handleTouchEnd = useCallback(() => {\n clearTimeout(touchTimeoutRef.current);\n }, []);\n\n const handleTouchStart = useCallback(() => {\n touchTimeoutRef.current = window.setTimeout(() => {\n shouldPreventClickRef.current = true;\n }, 500);\n }, []);\n\n /**\n * This hook listens for clicks\n */\n useDropdownListener({\n onClick: handleClick,\n onClose: handleClose,\n onTouchEnd: handleTouchEnd,\n onTouchStart: handleTouchStart,\n shouldCaptureEvents,\n });\n\n useEffect(() => {\n const isBottomDirection = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n const reservationWrapperElement = anchorElement.closest<HTMLDivElement>(\n ContainerAnchor.RESERVATION_WRAPPER,\n );\n\n isInChaynsWalletRef.current =\n !!(\n reservationWrapperElement && reservationWrapperElement.contains(anchorElement)\n ) || true;\n\n // This effect checks if additional space is needed to show dropdown content in chayns cards.\n if (\n isBottomDirection &&\n isInChaynsWalletRef.current &&\n measuredContentHeight > 0 &&\n reservationWrapperElement &&\n shouldShowDropdown\n ) {\n const availableHeight =\n window.innerHeight - anchorElement.getBoundingClientRect().bottom;\n\n // If the content height is greater than the available height, we need to add additional space.\n // This is to ensure that the dropdown content is fully visible. The 16 pixels are a buffer for shadows.\n const additionalNeededSpace = measuredContentHeight + 16 - availableHeight;\n\n if (additionalNeededSpace > 0) {\n // Add margin bottom to the reservation wrapper to ensure the dropdown content is fully visible.\n reservationWrapperElement.style.marginBottom = `${additionalNeededSpace}px`;\n } else {\n // Reset the margin bottom if no additional space is needed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n }\n\n if (isInChaynsWalletRef.current && reservationWrapperElement && !shouldShowDropdown) {\n // Reset the margin bottom when the dropdown is closed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n\n return () => {\n if (reservationWrapperElement) {\n reservationWrapperElement.style.marginBottom = '0px';\n }\n };\n }, [anchorElement, direction, measuredContentHeight, shouldShowDropdown]);\n\n useEffect(() => {\n if (!container) return;\n\n setPortal(() =>\n createPortal(\n <DelayedDropdownContent\n coordinates={coordinates}\n onMeasure={handleContentMeasure}\n shouldShowContent={shouldShowDropdown}\n transform={transform}\n >\n <StyledDropdownBodyWrapperContent\n $width={width}\n $minWidth={minBodyWidth}\n $maxHeight={maxHeight}\n $direction={direction}\n ref={contentRef}\n className={\n typeof maxHeight === 'number' ? 'chayns-scrollbar' : undefined\n }\n tabIndex={0}\n >\n {children}\n </StyledDropdownBodyWrapperContent>\n </DelayedDropdownContent>,\n container,\n ),\n );\n }, [\n children,\n container,\n coordinates,\n direction,\n handleContentMeasure,\n maxHeight,\n minBodyWidth,\n shouldShowDropdown,\n transform,\n width,\n ]);\n\n useImperativeHandle(ref, () => contentRef.current!, []);\n\n return <StyledDropdownBodyWrapper>{portal}</StyledDropdownBodyWrapper>;\n },\n);\n\nDropdownBodyWrapper.displayName = 'DropdownBodyWrapper';\n\nexport default DropdownBodyWrapper;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -76,7 +76,22 @@ const useDropdownPosition = ({
|
|
|
76
76
|
});
|
|
77
77
|
const [shouldUseTopAlignment, setShouldUseTopAlignment] = (0, _react.useState)(false);
|
|
78
78
|
const [availableMaxHeight, setAvailableMaxHeight] = (0, _react.useState)(0);
|
|
79
|
+
|
|
80
|
+
// Stores the alignment decision (top/bottom) that was made when the dropdown opened. The
|
|
81
|
+
// decision depends on the content height, but the content height in turn is limited by the
|
|
82
|
+
// available height (which depends on the alignment). Recalculating the alignment on every
|
|
83
|
+
// change would create a feedback loop that makes the alignment flip and the height oscillate.
|
|
84
|
+
// We therefore lock the alignment while the dropdown stays open and reset it on close.
|
|
85
|
+
const lockedTopAlignmentRef = (0, _react.useRef)(null);
|
|
79
86
|
const calculateCoordinates = (0, _react.useCallback)(() => {
|
|
87
|
+
// While the dropdown is closing (or closed) we must not recalculate the position and the
|
|
88
|
+
// available height. Otherwise layout changes underneath the dropdown (e.g. content that
|
|
89
|
+
// appears after a selection) would move or resize the dropdown body while it is still fading
|
|
90
|
+
// out, causing it to visibly jump. Freezing the last calculated values keeps the closing
|
|
91
|
+
// animation stable.
|
|
92
|
+
if (!shouldShowDropdown) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
80
95
|
if (container) {
|
|
81
96
|
const {
|
|
82
97
|
left: anchorLeft,
|
|
@@ -92,33 +107,47 @@ const useDropdownPosition = ({
|
|
|
92
107
|
const y = anchorTop - top + container.scrollTop;
|
|
93
108
|
let useTopAlignment = [_dropdown.DropdownDirection.TOP, _dropdown.DropdownDirection.TOP_LEFT, _dropdown.DropdownDirection.TOP_RIGHT].includes(direction);
|
|
94
109
|
const hasBottomAlignment = [_dropdown.DropdownDirection.BOTTOM, _dropdown.DropdownDirection.BOTTOM_LEFT, _dropdown.DropdownDirection.BOTTOM_RIGHT].includes(direction);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
|
|
111
|
+
// The available space above and below the anchor within the container.
|
|
112
|
+
const spaceToTop = y;
|
|
113
|
+
const spaceToBottom = height - (y + anchorHeight);
|
|
114
|
+
if (lockedTopAlignmentRef.current !== null) {
|
|
115
|
+
// Keep the alignment that was decided when the dropdown opened.
|
|
116
|
+
useTopAlignment = lockedTopAlignmentRef.current;
|
|
117
|
+
} else if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) {
|
|
118
|
+
// The content does not fit below the anchor. Only flip to the top when there is
|
|
119
|
+
// actually more space above than below. Otherwise the content would be cut off less
|
|
120
|
+
// when opened downwards (the content is limited to the available height anyway), so
|
|
121
|
+
// we keep the downward alignment. This avoids opening upwards for a content that,
|
|
122
|
+
// once limited to the available space, would fit below just fine.
|
|
123
|
+
useTopAlignment = spaceToTop > spaceToBottom;
|
|
100
124
|
}
|
|
125
|
+
lockedTopAlignmentRef.current = useTopAlignment;
|
|
126
|
+
setShouldUseTopAlignment(useTopAlignment);
|
|
101
127
|
|
|
102
128
|
// Calculate the space that is available for the dropdown content. When the dropdown is
|
|
103
129
|
// opened to the top, the available space reaches from the anchor to the top edge of the
|
|
104
130
|
// container. When it is opened to the bottom, it reaches from the bottom of the anchor
|
|
105
131
|
// to the bottom edge of the container. A small spacing is subtracted so the content does
|
|
106
132
|
// not touch the container edge (e.g. to leave room for shadows).
|
|
107
|
-
|
|
108
|
-
const spaceToBottom = height - (y + anchorHeight);
|
|
133
|
+
|
|
109
134
|
const nextAvailableMaxHeight = Math.max(Math.round((useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING), 0);
|
|
110
135
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// re-renders of the consumer, which could otherwise cause a layout shift loop.
|
|
136
|
+
// Ignore sub-pixel fluctuations so tiny changes from getBoundingClientRect do not
|
|
137
|
+
// repeatedly trigger re-renders that could keep the height oscillating.
|
|
114
138
|
setAvailableMaxHeight(currentAvailableMaxHeight => Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1 ? currentAvailableMaxHeight : nextAvailableMaxHeight);
|
|
115
139
|
setCoordinates({
|
|
116
140
|
x,
|
|
117
141
|
y: useTopAlignment ? y : y + anchorHeight
|
|
118
142
|
});
|
|
119
143
|
}
|
|
120
|
-
}, [anchorElement, container, contentHeight, direction]);
|
|
144
|
+
}, [anchorElement, container, contentHeight, direction, shouldShowDropdown]);
|
|
121
145
|
useIsomorphicLayoutEffect(() => {
|
|
146
|
+
// Reset the locked alignment whenever the dropdown is closed, so the next time it opens the
|
|
147
|
+
// alignment (top/bottom) is decided freshly based on the then-available space.
|
|
148
|
+
if (!shouldShowDropdown) {
|
|
149
|
+
lockedTopAlignmentRef.current = null;
|
|
150
|
+
}
|
|
122
151
|
const handleResize = () => {
|
|
123
152
|
calculateCoordinates();
|
|
124
153
|
setTimeout(calculateCoordinates, 300);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown.js","names":["_react","require","_dropdown","useIsomorphicLayoutEffect","window","useLayoutEffect","useEffect","useDropdownListener","onClick","onClose","onTouchEnd","onTouchStart","shouldCaptureEvents","document","addEventListener","removeEventListener","exports","useDropdownAlignment","anchorElement","contentWidth","direction","shouldUseTopAlignment","translateX","setTranslateX","useState","translateY","setTranslateY","DropdownDirection","BOTTOM_LEFT","TOP_LEFT","LEFT","includes","difference","clientWidth","useTopAlignment","TOP","TOP_RIGHT","useMemo","x","y","AVAILABLE_HEIGHT_SPACING","useDropdownPosition","container","contentHeight","shouldShowDropdown","coordinates","setCoordinates","setShouldUseTopAlignment","availableMaxHeight","setAvailableMaxHeight","calculateCoordinates","useCallback","left","anchorLeft","top","anchorTop","height","anchorHeight","getBoundingClientRect","scrollLeft","scrollTop","hasBottomAlignment","BOTTOM","BOTTOM_RIGHT","spaceToTop","spaceToBottom","nextAvailableMaxHeight","Math","max","round","currentAvailableMaxHeight","abs","handleResize","setTimeout","useDropdown","transform","width"],"sources":["../../../src/hooks/dropdown.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';\nimport { DropdownCoordinates, DropdownDirection } from '../types/dropdown';\n\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\ninterface UseDropdownListenerOptions {\n onClick: (event: MouseEvent) => void;\n onClose: () => void;\n onTouchEnd: (event: TouchEvent) => void;\n onTouchStart: (event: TouchEvent) => void;\n shouldCaptureEvents?: boolean;\n}\n\nexport const useDropdownListener = ({\n onClick,\n onClose,\n onTouchEnd,\n onTouchStart,\n shouldCaptureEvents,\n}: UseDropdownListenerOptions) => {\n useEffect(() => {\n document.addEventListener('click', onClick, shouldCaptureEvents);\n document.addEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.addEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.addEventListener('blur', onClose);\n\n return () => {\n document.removeEventListener('click', onClick, shouldCaptureEvents);\n document.removeEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.removeEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.removeEventListener('blur', onClose);\n };\n }, [onClick, onClose, onTouchEnd, onTouchStart]);\n};\n\ninterface UseDropdownAlignmentOptions {\n direction: DropdownDirection;\n shouldUseTopAlignment: boolean;\n contentWidth: number;\n anchorElement: Element;\n}\n\nexport const useDropdownAlignment = ({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n}: UseDropdownAlignmentOptions) => {\n const [translateX, setTranslateX] = useState<string>('0px');\n const [translateY, setTranslateY] = useState<string>('0px');\n\n useEffect(() => {\n if (\n [\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.LEFT,\n ].includes(direction)\n ) {\n const difference = anchorElement.clientWidth - contentWidth;\n\n setTranslateX(`${difference}px`);\n } else {\n setTranslateX('0px');\n }\n }, [anchorElement.clientWidth, contentWidth, direction]);\n\n useEffect(() => {\n const useTopAlignment =\n shouldUseTopAlignment ||\n [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n if (useTopAlignment) {\n setTranslateY('-100%');\n } else {\n setTranslateY('0px');\n }\n }, [direction, shouldUseTopAlignment]);\n\n return useMemo(() => ({ x: translateX, y: translateY }), [translateX, translateY]);\n};\n\ninterface UseDropdownPositionOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\n/**\n * The space (in pixels) that should be kept between the dropdown content and the edge of the\n * container when calculating the available maximum height.\n */\nconst AVAILABLE_HEIGHT_SPACING = 16;\n\nexport const useDropdownPosition = ({\n anchorElement,\n container,\n contentHeight = 0,\n direction,\n shouldShowDropdown,\n}: UseDropdownPositionOptions) => {\n const [coordinates, setCoordinates] = useState<DropdownCoordinates>({ x: 0, y: 0 });\n const [shouldUseTopAlignment, setShouldUseTopAlignment] = useState(false);\n const [availableMaxHeight, setAvailableMaxHeight] = useState<number>(0);\n\n const calculateCoordinates = useCallback(() => {\n if (container) {\n const {\n left: anchorLeft,\n top: anchorTop,\n height: anchorHeight,\n } = anchorElement.getBoundingClientRect();\n\n const { left, top, height } = container.getBoundingClientRect();\n\n const x = anchorLeft - left + container.scrollLeft;\n const y = anchorTop - top + container.scrollTop;\n\n let useTopAlignment = [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n const hasBottomAlignment = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) {\n useTopAlignment = true;\n\n setShouldUseTopAlignment(true);\n } else {\n setShouldUseTopAlignment(false);\n }\n\n // Calculate the space that is available for the dropdown content. When the dropdown is\n // opened to the top, the available space reaches from the anchor to the top edge of the\n // container. When it is opened to the bottom, it reaches from the bottom of the anchor\n // to the bottom edge of the container. A small spacing is subtracted so the content does\n // not touch the container edge (e.g. to leave room for shadows).\n const spaceToTop = y;\n const spaceToBottom = height - (y + anchorHeight);\n\n const nextAvailableMaxHeight = Math.max(\n Math.round(\n (useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING,\n ),\n 0,\n );\n\n // Only update the available max height when it changes by more than one pixel. This\n // prevents subpixel fluctuations of getBoundingClientRect from repeatedly triggering\n // re-renders of the consumer, which could otherwise cause a layout shift loop.\n setAvailableMaxHeight((currentAvailableMaxHeight) =>\n Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1\n ? currentAvailableMaxHeight\n : nextAvailableMaxHeight,\n );\n\n setCoordinates({ x, y: useTopAlignment ? y : y + anchorHeight });\n }\n }, [anchorElement, container, contentHeight, direction]);\n\n useIsomorphicLayoutEffect(() => {\n const handleResize = () => {\n calculateCoordinates();\n\n setTimeout(calculateCoordinates, 300);\n };\n\n handleResize();\n\n if (shouldShowDropdown) {\n window.addEventListener('resize', handleResize);\n }\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [calculateCoordinates, shouldShowDropdown]);\n\n return useMemo(\n () => ({ shouldUseTopAlignment, coordinates, availableMaxHeight }),\n [availableMaxHeight, coordinates, shouldUseTopAlignment],\n );\n};\n\ninterface UseDropdownOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n contentWidth: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\nexport const useDropdown = ({\n anchorElement,\n container,\n contentHeight,\n contentWidth,\n direction,\n shouldShowDropdown,\n}: UseDropdownOptions) => {\n const { shouldUseTopAlignment, coordinates, availableMaxHeight } = useDropdownPosition({\n anchorElement,\n container,\n contentHeight,\n direction,\n shouldShowDropdown,\n });\n\n const transform = useDropdownAlignment({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n });\n\n const width = anchorElement.clientWidth;\n\n return useMemo(\n () => ({ coordinates, transform, width, availableMaxHeight }),\n [availableMaxHeight, coordinates, transform, width],\n );\n};\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"dropdown.js","names":["_react","require","_dropdown","useIsomorphicLayoutEffect","window","useLayoutEffect","useEffect","useDropdownListener","onClick","onClose","onTouchEnd","onTouchStart","shouldCaptureEvents","document","addEventListener","removeEventListener","exports","useDropdownAlignment","anchorElement","contentWidth","direction","shouldUseTopAlignment","translateX","setTranslateX","useState","translateY","setTranslateY","DropdownDirection","BOTTOM_LEFT","TOP_LEFT","LEFT","includes","difference","clientWidth","useTopAlignment","TOP","TOP_RIGHT","useMemo","x","y","AVAILABLE_HEIGHT_SPACING","useDropdownPosition","container","contentHeight","shouldShowDropdown","coordinates","setCoordinates","setShouldUseTopAlignment","availableMaxHeight","setAvailableMaxHeight","lockedTopAlignmentRef","useRef","calculateCoordinates","useCallback","left","anchorLeft","top","anchorTop","height","anchorHeight","getBoundingClientRect","scrollLeft","scrollTop","hasBottomAlignment","BOTTOM","BOTTOM_RIGHT","spaceToTop","spaceToBottom","current","nextAvailableMaxHeight","Math","max","round","currentAvailableMaxHeight","abs","handleResize","setTimeout","useDropdown","transform","width"],"sources":["../../../src/hooks/dropdown.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { DropdownCoordinates, DropdownDirection } from '../types/dropdown';\n\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\ninterface UseDropdownListenerOptions {\n onClick: (event: MouseEvent) => void;\n onClose: () => void;\n onTouchEnd: (event: TouchEvent) => void;\n onTouchStart: (event: TouchEvent) => void;\n shouldCaptureEvents?: boolean;\n}\n\nexport const useDropdownListener = ({\n onClick,\n onClose,\n onTouchEnd,\n onTouchStart,\n shouldCaptureEvents,\n}: UseDropdownListenerOptions) => {\n useEffect(() => {\n document.addEventListener('click', onClick, shouldCaptureEvents);\n document.addEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.addEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.addEventListener('blur', onClose);\n\n return () => {\n document.removeEventListener('click', onClick, shouldCaptureEvents);\n document.removeEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.removeEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.removeEventListener('blur', onClose);\n };\n }, [onClick, onClose, onTouchEnd, onTouchStart]);\n};\n\ninterface UseDropdownAlignmentOptions {\n direction: DropdownDirection;\n shouldUseTopAlignment: boolean;\n contentWidth: number;\n anchorElement: Element;\n}\n\nexport const useDropdownAlignment = ({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n}: UseDropdownAlignmentOptions) => {\n const [translateX, setTranslateX] = useState<string>('0px');\n const [translateY, setTranslateY] = useState<string>('0px');\n\n useEffect(() => {\n if (\n [\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.LEFT,\n ].includes(direction)\n ) {\n const difference = anchorElement.clientWidth - contentWidth;\n\n setTranslateX(`${difference}px`);\n } else {\n setTranslateX('0px');\n }\n }, [anchorElement.clientWidth, contentWidth, direction]);\n\n useEffect(() => {\n const useTopAlignment =\n shouldUseTopAlignment ||\n [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n if (useTopAlignment) {\n setTranslateY('-100%');\n } else {\n setTranslateY('0px');\n }\n }, [direction, shouldUseTopAlignment]);\n\n return useMemo(() => ({ x: translateX, y: translateY }), [translateX, translateY]);\n};\n\ninterface UseDropdownPositionOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\n/**\n * The space (in pixels) that should be kept between the dropdown content and the edge of the\n * container when calculating the available maximum height.\n */\nconst AVAILABLE_HEIGHT_SPACING = 16;\n\nexport const useDropdownPosition = ({\n anchorElement,\n container,\n contentHeight = 0,\n direction,\n shouldShowDropdown,\n}: UseDropdownPositionOptions) => {\n const [coordinates, setCoordinates] = useState<DropdownCoordinates>({ x: 0, y: 0 });\n const [shouldUseTopAlignment, setShouldUseTopAlignment] = useState(false);\n const [availableMaxHeight, setAvailableMaxHeight] = useState<number>(0);\n\n // Stores the alignment decision (top/bottom) that was made when the dropdown opened. The\n // decision depends on the content height, but the content height in turn is limited by the\n // available height (which depends on the alignment). Recalculating the alignment on every\n // change would create a feedback loop that makes the alignment flip and the height oscillate.\n // We therefore lock the alignment while the dropdown stays open and reset it on close.\n const lockedTopAlignmentRef = useRef<boolean | null>(null);\n\n const calculateCoordinates = useCallback(() => {\n // While the dropdown is closing (or closed) we must not recalculate the position and the\n // available height. Otherwise layout changes underneath the dropdown (e.g. content that\n // appears after a selection) would move or resize the dropdown body while it is still fading\n // out, causing it to visibly jump. Freezing the last calculated values keeps the closing\n // animation stable.\n if (!shouldShowDropdown) {\n return;\n }\n\n if (container) {\n const {\n left: anchorLeft,\n top: anchorTop,\n height: anchorHeight,\n } = anchorElement.getBoundingClientRect();\n\n const { left, top, height } = container.getBoundingClientRect();\n\n const x = anchorLeft - left + container.scrollLeft;\n const y = anchorTop - top + container.scrollTop;\n\n let useTopAlignment = [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n const hasBottomAlignment = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n // The available space above and below the anchor within the container.\n const spaceToTop = y;\n const spaceToBottom = height - (y + anchorHeight);\n\n if (lockedTopAlignmentRef.current !== null) {\n // Keep the alignment that was decided when the dropdown opened.\n useTopAlignment = lockedTopAlignmentRef.current;\n } else if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) {\n // The content does not fit below the anchor. Only flip to the top when there is\n // actually more space above than below. Otherwise the content would be cut off less\n // when opened downwards (the content is limited to the available height anyway), so\n // we keep the downward alignment. This avoids opening upwards for a content that,\n // once limited to the available space, would fit below just fine.\n useTopAlignment = spaceToTop > spaceToBottom;\n }\n\n lockedTopAlignmentRef.current = useTopAlignment;\n\n setShouldUseTopAlignment(useTopAlignment);\n\n // Calculate the space that is available for the dropdown content. When the dropdown is\n // opened to the top, the available space reaches from the anchor to the top edge of the\n // container. When it is opened to the bottom, it reaches from the bottom of the anchor\n // to the bottom edge of the container. A small spacing is subtracted so the content does\n // not touch the container edge (e.g. to leave room for shadows).\n\n const nextAvailableMaxHeight = Math.max(\n Math.round(\n (useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING,\n ),\n 0,\n );\n\n // Ignore sub-pixel fluctuations so tiny changes from getBoundingClientRect do not\n // repeatedly trigger re-renders that could keep the height oscillating.\n setAvailableMaxHeight((currentAvailableMaxHeight) =>\n Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1\n ? currentAvailableMaxHeight\n : nextAvailableMaxHeight,\n );\n\n setCoordinates({ x, y: useTopAlignment ? y : y + anchorHeight });\n }\n }, [anchorElement, container, contentHeight, direction, shouldShowDropdown]);\n\n useIsomorphicLayoutEffect(() => {\n // Reset the locked alignment whenever the dropdown is closed, so the next time it opens the\n // alignment (top/bottom) is decided freshly based on the then-available space.\n if (!shouldShowDropdown) {\n lockedTopAlignmentRef.current = null;\n }\n\n const handleResize = () => {\n calculateCoordinates();\n\n setTimeout(calculateCoordinates, 300);\n };\n\n handleResize();\n\n if (shouldShowDropdown) {\n window.addEventListener('resize', handleResize);\n }\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [calculateCoordinates, shouldShowDropdown]);\n\n return useMemo(\n () => ({ shouldUseTopAlignment, coordinates, availableMaxHeight }),\n [availableMaxHeight, coordinates, shouldUseTopAlignment],\n );\n};\n\ninterface UseDropdownOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n contentWidth: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\nexport const useDropdown = ({\n anchorElement,\n container,\n contentHeight,\n contentWidth,\n direction,\n shouldShowDropdown,\n}: UseDropdownOptions) => {\n const { shouldUseTopAlignment, coordinates, availableMaxHeight } = useDropdownPosition({\n anchorElement,\n container,\n contentHeight,\n direction,\n shouldShowDropdown,\n });\n\n const transform = useDropdownAlignment({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n });\n\n const width = anchorElement.clientWidth;\n\n return useMemo(\n () => ({ coordinates, transform, width, availableMaxHeight }),\n [availableMaxHeight, coordinates, transform, width],\n );\n};\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -46,13 +46,10 @@ const DropdownBodyWrapper = /*#__PURE__*/forwardRef(({
|
|
|
46
46
|
shouldShowDropdown
|
|
47
47
|
});
|
|
48
48
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
// the dropdown is closed would trigger unnecessary re-renders of the consumer (e.g. the
|
|
51
|
-
// ComboBox) during the initial layout, which can cause visible layout shifts.
|
|
52
|
-
if (shouldShowDropdown && typeof onAvailableMaxHeightChange === 'function') {
|
|
49
|
+
if (typeof onAvailableMaxHeightChange === 'function') {
|
|
53
50
|
onAvailableMaxHeightChange(availableMaxHeight);
|
|
54
51
|
}
|
|
55
|
-
}, [availableMaxHeight, onAvailableMaxHeightChange
|
|
52
|
+
}, [availableMaxHeight, onAvailableMaxHeightChange]);
|
|
56
53
|
const handleClose = useCallback(() => {
|
|
57
54
|
if (typeof onClose === 'function') {
|
|
58
55
|
onClose();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownBodyWrapper.js","names":["React","forwardRef","useCallback","useEffect","useImperativeHandle","useRef","useState","StyledDropdownBodyWrapper","StyledDropdownBodyWrapperContent","createPortal","DropdownDirection","DelayedDropdownContent","useDropdown","useDropdownListener","ContainerAnchor","useContainer","DropdownBodyWrapper","anchorElement","bodyWidth","children","container","containerProp","contentHeight","direction","BOTTOM_RIGHT","maxHeight","minBodyWidth","onAvailableMaxHeightChange","onClose","onOutsideClick","onMeasure","shouldCaptureEvents","shouldShowDropdown","ref","isInChaynsWalletRef","measuredContentHeight","setMeasuredContentHeight","measuredContentWidth","setMeasuredContentWidth","portal","setPortal","contentRef","shouldPreventClickRef","touchTimeoutRef","undefined","transform","width","coordinates","availableMaxHeight","contentWidth","handleClose","handleClick","event","current","contains","target","preventDefault","stopPropagation","shouldPreventCloseOnClick","handleContentMeasure","measurements","height","handleTouchEnd","clearTimeout","handleTouchStart","window","setTimeout","onClick","onTouchEnd","onTouchStart","isBottomDirection","BOTTOM","BOTTOM_LEFT","includes","reservationWrapperElement","closest","RESERVATION_WRAPPER","availableHeight","innerHeight","getBoundingClientRect","bottom","additionalNeededSpace","style","marginBottom","createElement","shouldShowContent","$width","$minWidth","$maxHeight","$direction","className","tabIndex","displayName"],"sources":["../../../../src/components/dropdown-body-wrapper/DropdownBodyWrapper.tsx"],"sourcesContent":["import React, {\n forwardRef,\n ReactNode,\n ReactPortal,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport {\n StyledDropdownBodyWrapper,\n StyledDropdownBodyWrapperContent,\n} from './DropdownBodyWrapper.styles';\nimport { createPortal } from 'react-dom';\nimport { DropdownDirection, DropdownMeasurements } from '../../types/dropdown';\nimport DelayedDropdownContent, {\n DelayedDropdownContentProps,\n} from './delayed-dropdown-content/DelayedDropdownContent';\nimport { useDropdown, useDropdownListener } from '../../hooks/dropdown';\nimport { ContainerAnchor, useContainer } from '../../hooks/container';\n\ninterface DropdownBodyWrapperProps {\n /**\n * The anchor element of the dropdown.\n */\n anchorElement: Element;\n /**\n * The width of the Body.\n */\n bodyWidth?: number;\n /**\n * The content of the dropdown body.\n */\n children: ReactNode;\n /**\n * The element where the content should be rendered via React Portal.\n */\n container?: Element;\n /**\n * The height of the content\n */\n contentHeight?: number;\n /**\n * The direction of the dropdown.\n */\n direction?: DropdownDirection;\n /**\n * The max height of the dropdown.\n */\n maxHeight?: number;\n /**\n * The minimum width of the body.\n */\n minBodyWidth?: number;\n /**\n * Function to be executed when the available maximum height of the dropdown changes. The\n * available maximum height is the space that is available for the dropdown content until the\n * edge of the container is reached (minus a small spacing). This can be used to limit the\n * content height, so the dropdown is not cut off when there is not enough space.\n */\n onAvailableMaxHeightChange?: (availableMaxHeight: number) => void;\n /**\n * Function to be executed when the body is closed.\n */\n onClose?: VoidFunction;\n /**\n * Function to be executed when the user clicks outside the dropdown.\n * If the function returns `true`, the dropdown will not be closed.\n */\n onOutsideClick?: () => boolean | void;\n /**\n * Function to be executed when the content is measured.\n */\n onMeasure?: DelayedDropdownContentProps['onMeasure'];\n /**\n * Whether the dropdown should be visible.\n */\n shouldShowDropdown: boolean;\n /**\n * Whether the outside events should be captured.\n */\n shouldCaptureEvents?: boolean;\n}\n\nconst DropdownBodyWrapper = forwardRef<HTMLDivElement, DropdownBodyWrapperProps>(\n (\n {\n anchorElement,\n bodyWidth,\n children,\n container: containerProp,\n contentHeight = 0,\n direction = DropdownDirection.BOTTOM_RIGHT,\n maxHeight,\n minBodyWidth = 0,\n onAvailableMaxHeightChange,\n onClose,\n onOutsideClick,\n onMeasure,\n shouldCaptureEvents = true,\n shouldShowDropdown,\n },\n ref,\n ) => {\n const isInChaynsWalletRef = useRef(false);\n\n const [measuredContentHeight, setMeasuredContentHeight] = useState<number>(0);\n const [measuredContentWidth, setMeasuredContentWidth] = useState<number>(0);\n const [portal, setPortal] = useState<ReactPortal>();\n\n const contentRef = useRef<HTMLDivElement>(null);\n const shouldPreventClickRef = useRef<boolean>(false);\n const touchTimeoutRef = useRef<number | undefined>(undefined);\n\n const container = useContainer({ anchorElement, container: containerProp });\n\n const { transform, width, coordinates, availableMaxHeight } = useDropdown({\n anchorElement,\n container,\n contentHeight,\n contentWidth: bodyWidth ?? measuredContentWidth,\n direction,\n shouldShowDropdown,\n });\n\n useEffect(() => {\n // Only report the available max height while the dropdown is shown. Reporting it while\n // the dropdown is closed would trigger unnecessary re-renders of the consumer (e.g. the\n // ComboBox) during the initial layout, which can cause visible layout shifts.\n if (shouldShowDropdown && typeof onAvailableMaxHeightChange === 'function') {\n onAvailableMaxHeightChange(availableMaxHeight);\n }\n }, [availableMaxHeight, onAvailableMaxHeightChange, shouldShowDropdown]);\n\n const handleClose = useCallback(() => {\n if (typeof onClose === 'function') {\n onClose();\n }\n }, [onClose]);\n\n /**\n * This function closes the body\n */\n const handleClick = useCallback(\n (event: MouseEvent) => {\n if (\n contentRef.current &&\n shouldShowDropdown &&\n !anchorElement.contains(event.target as Node) &&\n !contentRef.current.contains(event.target as Node)\n ) {\n event.preventDefault();\n event.stopPropagation();\n\n const shouldPreventCloseOnClick = onOutsideClick?.() ?? false;\n\n if (!shouldPreventClickRef.current && !shouldPreventCloseOnClick) {\n handleClose();\n }\n }\n\n shouldPreventClickRef.current = false;\n },\n [anchorElement, handleClose, onOutsideClick, shouldShowDropdown],\n );\n\n const handleContentMeasure = useCallback(\n (measurements: DropdownMeasurements) => {\n // Measurements are only needed if the content is shown in the chayns wallet. To prevent\n // unnecessary renders, we only set the height if the content is shown in the wallet.\n if (isInChaynsWalletRef.current) {\n setMeasuredContentHeight(measurements.height);\n }\n\n setMeasuredContentWidth(measurements.width);\n\n if (typeof onMeasure === 'function') {\n onMeasure(measurements);\n }\n },\n [onMeasure],\n );\n\n const handleTouchEnd = useCallback(() => {\n clearTimeout(touchTimeoutRef.current);\n }, []);\n\n const handleTouchStart = useCallback(() => {\n touchTimeoutRef.current = window.setTimeout(() => {\n shouldPreventClickRef.current = true;\n }, 500);\n }, []);\n\n /**\n * This hook listens for clicks\n */\n useDropdownListener({\n onClick: handleClick,\n onClose: handleClose,\n onTouchEnd: handleTouchEnd,\n onTouchStart: handleTouchStart,\n shouldCaptureEvents,\n });\n\n useEffect(() => {\n const isBottomDirection = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n const reservationWrapperElement = anchorElement.closest<HTMLDivElement>(\n ContainerAnchor.RESERVATION_WRAPPER,\n );\n\n isInChaynsWalletRef.current =\n !!(\n reservationWrapperElement && reservationWrapperElement.contains(anchorElement)\n ) || true;\n\n // This effect checks if additional space is needed to show dropdown content in chayns cards.\n if (\n isBottomDirection &&\n isInChaynsWalletRef.current &&\n measuredContentHeight > 0 &&\n reservationWrapperElement &&\n shouldShowDropdown\n ) {\n const availableHeight =\n window.innerHeight - anchorElement.getBoundingClientRect().bottom;\n\n // If the content height is greater than the available height, we need to add additional space.\n // This is to ensure that the dropdown content is fully visible. The 16 pixels are a buffer for shadows.\n const additionalNeededSpace = measuredContentHeight + 16 - availableHeight;\n\n if (additionalNeededSpace > 0) {\n // Add margin bottom to the reservation wrapper to ensure the dropdown content is fully visible.\n reservationWrapperElement.style.marginBottom = `${additionalNeededSpace}px`;\n } else {\n // Reset the margin bottom if no additional space is needed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n }\n\n if (isInChaynsWalletRef.current && reservationWrapperElement && !shouldShowDropdown) {\n // Reset the margin bottom when the dropdown is closed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n\n return () => {\n if (reservationWrapperElement) {\n reservationWrapperElement.style.marginBottom = '0px';\n }\n };\n }, [anchorElement, direction, measuredContentHeight, shouldShowDropdown]);\n\n useEffect(() => {\n if (!container) return;\n\n setPortal(() =>\n createPortal(\n <DelayedDropdownContent\n coordinates={coordinates}\n onMeasure={handleContentMeasure}\n shouldShowContent={shouldShowDropdown}\n transform={transform}\n >\n <StyledDropdownBodyWrapperContent\n $width={width}\n $minWidth={minBodyWidth}\n $maxHeight={maxHeight}\n $direction={direction}\n ref={contentRef}\n className={\n typeof maxHeight === 'number' ? 'chayns-scrollbar' : undefined\n }\n tabIndex={0}\n >\n {children}\n </StyledDropdownBodyWrapperContent>\n </DelayedDropdownContent>,\n container,\n ),\n );\n }, [\n children,\n container,\n coordinates,\n direction,\n handleContentMeasure,\n maxHeight,\n minBodyWidth,\n shouldShowDropdown,\n transform,\n width,\n ]);\n\n useImperativeHandle(ref, () => contentRef.current!, []);\n\n return <StyledDropdownBodyWrapper>{portal}</StyledDropdownBodyWrapper>;\n },\n);\n\nDropdownBodyWrapper.displayName = 'DropdownBodyWrapper';\n\nexport default DropdownBodyWrapper;\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"DropdownBodyWrapper.js","names":["React","forwardRef","useCallback","useEffect","useImperativeHandle","useRef","useState","StyledDropdownBodyWrapper","StyledDropdownBodyWrapperContent","createPortal","DropdownDirection","DelayedDropdownContent","useDropdown","useDropdownListener","ContainerAnchor","useContainer","DropdownBodyWrapper","anchorElement","bodyWidth","children","container","containerProp","contentHeight","direction","BOTTOM_RIGHT","maxHeight","minBodyWidth","onAvailableMaxHeightChange","onClose","onOutsideClick","onMeasure","shouldCaptureEvents","shouldShowDropdown","ref","isInChaynsWalletRef","measuredContentHeight","setMeasuredContentHeight","measuredContentWidth","setMeasuredContentWidth","portal","setPortal","contentRef","shouldPreventClickRef","touchTimeoutRef","undefined","transform","width","coordinates","availableMaxHeight","contentWidth","handleClose","handleClick","event","current","contains","target","preventDefault","stopPropagation","shouldPreventCloseOnClick","handleContentMeasure","measurements","height","handleTouchEnd","clearTimeout","handleTouchStart","window","setTimeout","onClick","onTouchEnd","onTouchStart","isBottomDirection","BOTTOM","BOTTOM_LEFT","includes","reservationWrapperElement","closest","RESERVATION_WRAPPER","availableHeight","innerHeight","getBoundingClientRect","bottom","additionalNeededSpace","style","marginBottom","createElement","shouldShowContent","$width","$minWidth","$maxHeight","$direction","className","tabIndex","displayName"],"sources":["../../../../src/components/dropdown-body-wrapper/DropdownBodyWrapper.tsx"],"sourcesContent":["import React, {\n forwardRef,\n ReactNode,\n ReactPortal,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport {\n StyledDropdownBodyWrapper,\n StyledDropdownBodyWrapperContent,\n} from './DropdownBodyWrapper.styles';\nimport { createPortal } from 'react-dom';\nimport { DropdownDirection, DropdownMeasurements } from '../../types/dropdown';\nimport DelayedDropdownContent, {\n DelayedDropdownContentProps,\n} from './delayed-dropdown-content/DelayedDropdownContent';\nimport { useDropdown, useDropdownListener } from '../../hooks/dropdown';\nimport { ContainerAnchor, useContainer } from '../../hooks/container';\n\ninterface DropdownBodyWrapperProps {\n /**\n * The anchor element of the dropdown.\n */\n anchorElement: Element;\n /**\n * The width of the Body.\n */\n bodyWidth?: number;\n /**\n * The content of the dropdown body.\n */\n children: ReactNode;\n /**\n * The element where the content should be rendered via React Portal.\n */\n container?: Element;\n /**\n * The height of the content\n */\n contentHeight?: number;\n /**\n * The direction of the dropdown.\n */\n direction?: DropdownDirection;\n /**\n * The max height of the dropdown.\n */\n maxHeight?: number;\n /**\n * The minimum width of the body.\n */\n minBodyWidth?: number;\n /**\n * Function to be executed when the available maximum height of the dropdown changes. The\n * available maximum height is the space that is available for the dropdown content until the\n * edge of the container is reached (minus a small spacing). This can be used to limit the\n * content height, so the dropdown is not cut off when there is not enough space.\n */\n onAvailableMaxHeightChange?: (availableMaxHeight: number) => void;\n /**\n * Function to be executed when the body is closed.\n */\n onClose?: VoidFunction;\n /**\n * Function to be executed when the user clicks outside the dropdown.\n * If the function returns `true`, the dropdown will not be closed.\n */\n onOutsideClick?: () => boolean | void;\n /**\n * Function to be executed when the content is measured.\n */\n onMeasure?: DelayedDropdownContentProps['onMeasure'];\n /**\n * Whether the dropdown should be visible.\n */\n shouldShowDropdown: boolean;\n /**\n * Whether the outside events should be captured.\n */\n shouldCaptureEvents?: boolean;\n}\n\nconst DropdownBodyWrapper = forwardRef<HTMLDivElement, DropdownBodyWrapperProps>(\n (\n {\n anchorElement,\n bodyWidth,\n children,\n container: containerProp,\n contentHeight = 0,\n direction = DropdownDirection.BOTTOM_RIGHT,\n maxHeight,\n minBodyWidth = 0,\n onAvailableMaxHeightChange,\n onClose,\n onOutsideClick,\n onMeasure,\n shouldCaptureEvents = true,\n shouldShowDropdown,\n },\n ref,\n ) => {\n const isInChaynsWalletRef = useRef(false);\n\n const [measuredContentHeight, setMeasuredContentHeight] = useState<number>(0);\n const [measuredContentWidth, setMeasuredContentWidth] = useState<number>(0);\n const [portal, setPortal] = useState<ReactPortal>();\n\n const contentRef = useRef<HTMLDivElement>(null);\n const shouldPreventClickRef = useRef<boolean>(false);\n const touchTimeoutRef = useRef<number | undefined>(undefined);\n\n const container = useContainer({ anchorElement, container: containerProp });\n\n const { transform, width, coordinates, availableMaxHeight } = useDropdown({\n anchorElement,\n container,\n contentHeight,\n contentWidth: bodyWidth ?? measuredContentWidth,\n direction,\n shouldShowDropdown,\n });\n\n useEffect(() => {\n if (typeof onAvailableMaxHeightChange === 'function') {\n onAvailableMaxHeightChange(availableMaxHeight);\n }\n }, [availableMaxHeight, onAvailableMaxHeightChange]);\n\n const handleClose = useCallback(() => {\n if (typeof onClose === 'function') {\n onClose();\n }\n }, [onClose]);\n\n /**\n * This function closes the body\n */\n const handleClick = useCallback(\n (event: MouseEvent) => {\n if (\n contentRef.current &&\n shouldShowDropdown &&\n !anchorElement.contains(event.target as Node) &&\n !contentRef.current.contains(event.target as Node)\n ) {\n event.preventDefault();\n event.stopPropagation();\n\n const shouldPreventCloseOnClick = onOutsideClick?.() ?? false;\n\n if (!shouldPreventClickRef.current && !shouldPreventCloseOnClick) {\n handleClose();\n }\n }\n\n shouldPreventClickRef.current = false;\n },\n [anchorElement, handleClose, onOutsideClick, shouldShowDropdown],\n );\n\n const handleContentMeasure = useCallback(\n (measurements: DropdownMeasurements) => {\n // Measurements are only needed if the content is shown in the chayns wallet. To prevent\n // unnecessary renders, we only set the height if the content is shown in the wallet.\n if (isInChaynsWalletRef.current) {\n setMeasuredContentHeight(measurements.height);\n }\n\n setMeasuredContentWidth(measurements.width);\n\n if (typeof onMeasure === 'function') {\n onMeasure(measurements);\n }\n },\n [onMeasure],\n );\n\n const handleTouchEnd = useCallback(() => {\n clearTimeout(touchTimeoutRef.current);\n }, []);\n\n const handleTouchStart = useCallback(() => {\n touchTimeoutRef.current = window.setTimeout(() => {\n shouldPreventClickRef.current = true;\n }, 500);\n }, []);\n\n /**\n * This hook listens for clicks\n */\n useDropdownListener({\n onClick: handleClick,\n onClose: handleClose,\n onTouchEnd: handleTouchEnd,\n onTouchStart: handleTouchStart,\n shouldCaptureEvents,\n });\n\n useEffect(() => {\n const isBottomDirection = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n const reservationWrapperElement = anchorElement.closest<HTMLDivElement>(\n ContainerAnchor.RESERVATION_WRAPPER,\n );\n\n isInChaynsWalletRef.current =\n !!(\n reservationWrapperElement && reservationWrapperElement.contains(anchorElement)\n ) || true;\n\n // This effect checks if additional space is needed to show dropdown content in chayns cards.\n if (\n isBottomDirection &&\n isInChaynsWalletRef.current &&\n measuredContentHeight > 0 &&\n reservationWrapperElement &&\n shouldShowDropdown\n ) {\n const availableHeight =\n window.innerHeight - anchorElement.getBoundingClientRect().bottom;\n\n // If the content height is greater than the available height, we need to add additional space.\n // This is to ensure that the dropdown content is fully visible. The 16 pixels are a buffer for shadows.\n const additionalNeededSpace = measuredContentHeight + 16 - availableHeight;\n\n if (additionalNeededSpace > 0) {\n // Add margin bottom to the reservation wrapper to ensure the dropdown content is fully visible.\n reservationWrapperElement.style.marginBottom = `${additionalNeededSpace}px`;\n } else {\n // Reset the margin bottom if no additional space is needed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n }\n\n if (isInChaynsWalletRef.current && reservationWrapperElement && !shouldShowDropdown) {\n // Reset the margin bottom when the dropdown is closed.\n reservationWrapperElement.style.marginBottom = '0px';\n }\n\n return () => {\n if (reservationWrapperElement) {\n reservationWrapperElement.style.marginBottom = '0px';\n }\n };\n }, [anchorElement, direction, measuredContentHeight, shouldShowDropdown]);\n\n useEffect(() => {\n if (!container) return;\n\n setPortal(() =>\n createPortal(\n <DelayedDropdownContent\n coordinates={coordinates}\n onMeasure={handleContentMeasure}\n shouldShowContent={shouldShowDropdown}\n transform={transform}\n >\n <StyledDropdownBodyWrapperContent\n $width={width}\n $minWidth={minBodyWidth}\n $maxHeight={maxHeight}\n $direction={direction}\n ref={contentRef}\n className={\n typeof maxHeight === 'number' ? 'chayns-scrollbar' : undefined\n }\n tabIndex={0}\n >\n {children}\n </StyledDropdownBodyWrapperContent>\n </DelayedDropdownContent>,\n container,\n ),\n );\n }, [\n children,\n container,\n coordinates,\n direction,\n handleContentMeasure,\n maxHeight,\n minBodyWidth,\n shouldShowDropdown,\n transform,\n width,\n ]);\n\n useImperativeHandle(ref, () => contentRef.current!, []);\n\n return <StyledDropdownBodyWrapper>{portal}</StyledDropdownBodyWrapper>;\n },\n);\n\nDropdownBodyWrapper.displayName = 'DropdownBodyWrapper';\n\nexport default DropdownBodyWrapper;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { DropdownDirection } from '../types/dropdown';
|
|
3
3
|
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
4
4
|
export const useDropdownListener = ({
|
|
@@ -68,7 +68,22 @@ export const useDropdownPosition = ({
|
|
|
68
68
|
});
|
|
69
69
|
const [shouldUseTopAlignment, setShouldUseTopAlignment] = useState(false);
|
|
70
70
|
const [availableMaxHeight, setAvailableMaxHeight] = useState(0);
|
|
71
|
+
|
|
72
|
+
// Stores the alignment decision (top/bottom) that was made when the dropdown opened. The
|
|
73
|
+
// decision depends on the content height, but the content height in turn is limited by the
|
|
74
|
+
// available height (which depends on the alignment). Recalculating the alignment on every
|
|
75
|
+
// change would create a feedback loop that makes the alignment flip and the height oscillate.
|
|
76
|
+
// We therefore lock the alignment while the dropdown stays open and reset it on close.
|
|
77
|
+
const lockedTopAlignmentRef = useRef(null);
|
|
71
78
|
const calculateCoordinates = useCallback(() => {
|
|
79
|
+
// While the dropdown is closing (or closed) we must not recalculate the position and the
|
|
80
|
+
// available height. Otherwise layout changes underneath the dropdown (e.g. content that
|
|
81
|
+
// appears after a selection) would move or resize the dropdown body while it is still fading
|
|
82
|
+
// out, causing it to visibly jump. Freezing the last calculated values keeps the closing
|
|
83
|
+
// animation stable.
|
|
84
|
+
if (!shouldShowDropdown) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
72
87
|
if (container) {
|
|
73
88
|
const {
|
|
74
89
|
left: anchorLeft,
|
|
@@ -84,33 +99,47 @@ export const useDropdownPosition = ({
|
|
|
84
99
|
const y = anchorTop - top + container.scrollTop;
|
|
85
100
|
let useTopAlignment = [DropdownDirection.TOP, DropdownDirection.TOP_LEFT, DropdownDirection.TOP_RIGHT].includes(direction);
|
|
86
101
|
const hasBottomAlignment = [DropdownDirection.BOTTOM, DropdownDirection.BOTTOM_LEFT, DropdownDirection.BOTTOM_RIGHT].includes(direction);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
|
|
103
|
+
// The available space above and below the anchor within the container.
|
|
104
|
+
const spaceToTop = y;
|
|
105
|
+
const spaceToBottom = height - (y + anchorHeight);
|
|
106
|
+
if (lockedTopAlignmentRef.current !== null) {
|
|
107
|
+
// Keep the alignment that was decided when the dropdown opened.
|
|
108
|
+
useTopAlignment = lockedTopAlignmentRef.current;
|
|
109
|
+
} else if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) {
|
|
110
|
+
// The content does not fit below the anchor. Only flip to the top when there is
|
|
111
|
+
// actually more space above than below. Otherwise the content would be cut off less
|
|
112
|
+
// when opened downwards (the content is limited to the available height anyway), so
|
|
113
|
+
// we keep the downward alignment. This avoids opening upwards for a content that,
|
|
114
|
+
// once limited to the available space, would fit below just fine.
|
|
115
|
+
useTopAlignment = spaceToTop > spaceToBottom;
|
|
92
116
|
}
|
|
117
|
+
lockedTopAlignmentRef.current = useTopAlignment;
|
|
118
|
+
setShouldUseTopAlignment(useTopAlignment);
|
|
93
119
|
|
|
94
120
|
// Calculate the space that is available for the dropdown content. When the dropdown is
|
|
95
121
|
// opened to the top, the available space reaches from the anchor to the top edge of the
|
|
96
122
|
// container. When it is opened to the bottom, it reaches from the bottom of the anchor
|
|
97
123
|
// to the bottom edge of the container. A small spacing is subtracted so the content does
|
|
98
124
|
// not touch the container edge (e.g. to leave room for shadows).
|
|
99
|
-
|
|
100
|
-
const spaceToBottom = height - (y + anchorHeight);
|
|
125
|
+
|
|
101
126
|
const nextAvailableMaxHeight = Math.max(Math.round((useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING), 0);
|
|
102
127
|
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// re-renders of the consumer, which could otherwise cause a layout shift loop.
|
|
128
|
+
// Ignore sub-pixel fluctuations so tiny changes from getBoundingClientRect do not
|
|
129
|
+
// repeatedly trigger re-renders that could keep the height oscillating.
|
|
106
130
|
setAvailableMaxHeight(currentAvailableMaxHeight => Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1 ? currentAvailableMaxHeight : nextAvailableMaxHeight);
|
|
107
131
|
setCoordinates({
|
|
108
132
|
x,
|
|
109
133
|
y: useTopAlignment ? y : y + anchorHeight
|
|
110
134
|
});
|
|
111
135
|
}
|
|
112
|
-
}, [anchorElement, container, contentHeight, direction]);
|
|
136
|
+
}, [anchorElement, container, contentHeight, direction, shouldShowDropdown]);
|
|
113
137
|
useIsomorphicLayoutEffect(() => {
|
|
138
|
+
// Reset the locked alignment whenever the dropdown is closed, so the next time it opens the
|
|
139
|
+
// alignment (top/bottom) is decided freshly based on the then-available space.
|
|
140
|
+
if (!shouldShowDropdown) {
|
|
141
|
+
lockedTopAlignmentRef.current = null;
|
|
142
|
+
}
|
|
114
143
|
const handleResize = () => {
|
|
115
144
|
calculateCoordinates();
|
|
116
145
|
setTimeout(calculateCoordinates, 300);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown.js","names":["useCallback","useEffect","useLayoutEffect","useMemo","useState","DropdownDirection","useIsomorphicLayoutEffect","window","useDropdownListener","onClick","onClose","onTouchEnd","onTouchStart","shouldCaptureEvents","document","addEventListener","removeEventListener","useDropdownAlignment","anchorElement","contentWidth","direction","shouldUseTopAlignment","translateX","setTranslateX","translateY","setTranslateY","BOTTOM_LEFT","TOP_LEFT","LEFT","includes","difference","clientWidth","useTopAlignment","TOP","TOP_RIGHT","x","y","AVAILABLE_HEIGHT_SPACING","useDropdownPosition","container","contentHeight","shouldShowDropdown","coordinates","setCoordinates","setShouldUseTopAlignment","availableMaxHeight","setAvailableMaxHeight","calculateCoordinates","left","anchorLeft","top","anchorTop","height","anchorHeight","getBoundingClientRect","scrollLeft","scrollTop","hasBottomAlignment","BOTTOM","BOTTOM_RIGHT","spaceToTop","spaceToBottom","nextAvailableMaxHeight","Math","max","round","currentAvailableMaxHeight","abs","handleResize","setTimeout","useDropdown","transform","width"],"sources":["../../../src/hooks/dropdown.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';\nimport { DropdownCoordinates, DropdownDirection } from '../types/dropdown';\n\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\ninterface UseDropdownListenerOptions {\n onClick: (event: MouseEvent) => void;\n onClose: () => void;\n onTouchEnd: (event: TouchEvent) => void;\n onTouchStart: (event: TouchEvent) => void;\n shouldCaptureEvents?: boolean;\n}\n\nexport const useDropdownListener = ({\n onClick,\n onClose,\n onTouchEnd,\n onTouchStart,\n shouldCaptureEvents,\n}: UseDropdownListenerOptions) => {\n useEffect(() => {\n document.addEventListener('click', onClick, shouldCaptureEvents);\n document.addEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.addEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.addEventListener('blur', onClose);\n\n return () => {\n document.removeEventListener('click', onClick, shouldCaptureEvents);\n document.removeEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.removeEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.removeEventListener('blur', onClose);\n };\n }, [onClick, onClose, onTouchEnd, onTouchStart]);\n};\n\ninterface UseDropdownAlignmentOptions {\n direction: DropdownDirection;\n shouldUseTopAlignment: boolean;\n contentWidth: number;\n anchorElement: Element;\n}\n\nexport const useDropdownAlignment = ({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n}: UseDropdownAlignmentOptions) => {\n const [translateX, setTranslateX] = useState<string>('0px');\n const [translateY, setTranslateY] = useState<string>('0px');\n\n useEffect(() => {\n if (\n [\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.LEFT,\n ].includes(direction)\n ) {\n const difference = anchorElement.clientWidth - contentWidth;\n\n setTranslateX(`${difference}px`);\n } else {\n setTranslateX('0px');\n }\n }, [anchorElement.clientWidth, contentWidth, direction]);\n\n useEffect(() => {\n const useTopAlignment =\n shouldUseTopAlignment ||\n [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n if (useTopAlignment) {\n setTranslateY('-100%');\n } else {\n setTranslateY('0px');\n }\n }, [direction, shouldUseTopAlignment]);\n\n return useMemo(() => ({ x: translateX, y: translateY }), [translateX, translateY]);\n};\n\ninterface UseDropdownPositionOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\n/**\n * The space (in pixels) that should be kept between the dropdown content and the edge of the\n * container when calculating the available maximum height.\n */\nconst AVAILABLE_HEIGHT_SPACING = 16;\n\nexport const useDropdownPosition = ({\n anchorElement,\n container,\n contentHeight = 0,\n direction,\n shouldShowDropdown,\n}: UseDropdownPositionOptions) => {\n const [coordinates, setCoordinates] = useState<DropdownCoordinates>({ x: 0, y: 0 });\n const [shouldUseTopAlignment, setShouldUseTopAlignment] = useState(false);\n const [availableMaxHeight, setAvailableMaxHeight] = useState<number>(0);\n\n const calculateCoordinates = useCallback(() => {\n if (container) {\n const {\n left: anchorLeft,\n top: anchorTop,\n height: anchorHeight,\n } = anchorElement.getBoundingClientRect();\n\n const { left, top, height } = container.getBoundingClientRect();\n\n const x = anchorLeft - left + container.scrollLeft;\n const y = anchorTop - top + container.scrollTop;\n\n let useTopAlignment = [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n const hasBottomAlignment = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) {\n useTopAlignment = true;\n\n setShouldUseTopAlignment(true);\n } else {\n setShouldUseTopAlignment(false);\n }\n\n // Calculate the space that is available for the dropdown content. When the dropdown is\n // opened to the top, the available space reaches from the anchor to the top edge of the\n // container. When it is opened to the bottom, it reaches from the bottom of the anchor\n // to the bottom edge of the container. A small spacing is subtracted so the content does\n // not touch the container edge (e.g. to leave room for shadows).\n const spaceToTop = y;\n const spaceToBottom = height - (y + anchorHeight);\n\n const nextAvailableMaxHeight = Math.max(\n Math.round(\n (useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING,\n ),\n 0,\n );\n\n // Only update the available max height when it changes by more than one pixel. This\n // prevents subpixel fluctuations of getBoundingClientRect from repeatedly triggering\n // re-renders of the consumer, which could otherwise cause a layout shift loop.\n setAvailableMaxHeight((currentAvailableMaxHeight) =>\n Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1\n ? currentAvailableMaxHeight\n : nextAvailableMaxHeight,\n );\n\n setCoordinates({ x, y: useTopAlignment ? y : y + anchorHeight });\n }\n }, [anchorElement, container, contentHeight, direction]);\n\n useIsomorphicLayoutEffect(() => {\n const handleResize = () => {\n calculateCoordinates();\n\n setTimeout(calculateCoordinates, 300);\n };\n\n handleResize();\n\n if (shouldShowDropdown) {\n window.addEventListener('resize', handleResize);\n }\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [calculateCoordinates, shouldShowDropdown]);\n\n return useMemo(\n () => ({ shouldUseTopAlignment, coordinates, availableMaxHeight }),\n [availableMaxHeight, coordinates, shouldUseTopAlignment],\n );\n};\n\ninterface UseDropdownOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n contentWidth: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\nexport const useDropdown = ({\n anchorElement,\n container,\n contentHeight,\n contentWidth,\n direction,\n shouldShowDropdown,\n}: UseDropdownOptions) => {\n const { shouldUseTopAlignment, coordinates, availableMaxHeight } = useDropdownPosition({\n anchorElement,\n container,\n contentHeight,\n direction,\n shouldShowDropdown,\n });\n\n const transform = useDropdownAlignment({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n });\n\n const width = anchorElement.clientWidth;\n\n return useMemo(\n () => ({ coordinates, transform, width, availableMaxHeight }),\n [availableMaxHeight, coordinates, transform, width],\n );\n};\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"dropdown.js","names":["useCallback","useEffect","useLayoutEffect","useMemo","useRef","useState","DropdownDirection","useIsomorphicLayoutEffect","window","useDropdownListener","onClick","onClose","onTouchEnd","onTouchStart","shouldCaptureEvents","document","addEventListener","removeEventListener","useDropdownAlignment","anchorElement","contentWidth","direction","shouldUseTopAlignment","translateX","setTranslateX","translateY","setTranslateY","BOTTOM_LEFT","TOP_LEFT","LEFT","includes","difference","clientWidth","useTopAlignment","TOP","TOP_RIGHT","x","y","AVAILABLE_HEIGHT_SPACING","useDropdownPosition","container","contentHeight","shouldShowDropdown","coordinates","setCoordinates","setShouldUseTopAlignment","availableMaxHeight","setAvailableMaxHeight","lockedTopAlignmentRef","calculateCoordinates","left","anchorLeft","top","anchorTop","height","anchorHeight","getBoundingClientRect","scrollLeft","scrollTop","hasBottomAlignment","BOTTOM","BOTTOM_RIGHT","spaceToTop","spaceToBottom","current","nextAvailableMaxHeight","Math","max","round","currentAvailableMaxHeight","abs","handleResize","setTimeout","useDropdown","transform","width"],"sources":["../../../src/hooks/dropdown.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { DropdownCoordinates, DropdownDirection } from '../types/dropdown';\n\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\ninterface UseDropdownListenerOptions {\n onClick: (event: MouseEvent) => void;\n onClose: () => void;\n onTouchEnd: (event: TouchEvent) => void;\n onTouchStart: (event: TouchEvent) => void;\n shouldCaptureEvents?: boolean;\n}\n\nexport const useDropdownListener = ({\n onClick,\n onClose,\n onTouchEnd,\n onTouchStart,\n shouldCaptureEvents,\n}: UseDropdownListenerOptions) => {\n useEffect(() => {\n document.addEventListener('click', onClick, shouldCaptureEvents);\n document.addEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.addEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.addEventListener('blur', onClose);\n\n return () => {\n document.removeEventListener('click', onClick, shouldCaptureEvents);\n document.removeEventListener('touchend', onTouchEnd, shouldCaptureEvents);\n document.removeEventListener('touchstart', onTouchStart, shouldCaptureEvents);\n\n window.removeEventListener('blur', onClose);\n };\n }, [onClick, onClose, onTouchEnd, onTouchStart]);\n};\n\ninterface UseDropdownAlignmentOptions {\n direction: DropdownDirection;\n shouldUseTopAlignment: boolean;\n contentWidth: number;\n anchorElement: Element;\n}\n\nexport const useDropdownAlignment = ({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n}: UseDropdownAlignmentOptions) => {\n const [translateX, setTranslateX] = useState<string>('0px');\n const [translateY, setTranslateY] = useState<string>('0px');\n\n useEffect(() => {\n if (\n [\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.LEFT,\n ].includes(direction)\n ) {\n const difference = anchorElement.clientWidth - contentWidth;\n\n setTranslateX(`${difference}px`);\n } else {\n setTranslateX('0px');\n }\n }, [anchorElement.clientWidth, contentWidth, direction]);\n\n useEffect(() => {\n const useTopAlignment =\n shouldUseTopAlignment ||\n [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n if (useTopAlignment) {\n setTranslateY('-100%');\n } else {\n setTranslateY('0px');\n }\n }, [direction, shouldUseTopAlignment]);\n\n return useMemo(() => ({ x: translateX, y: translateY }), [translateX, translateY]);\n};\n\ninterface UseDropdownPositionOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\n/**\n * The space (in pixels) that should be kept between the dropdown content and the edge of the\n * container when calculating the available maximum height.\n */\nconst AVAILABLE_HEIGHT_SPACING = 16;\n\nexport const useDropdownPosition = ({\n anchorElement,\n container,\n contentHeight = 0,\n direction,\n shouldShowDropdown,\n}: UseDropdownPositionOptions) => {\n const [coordinates, setCoordinates] = useState<DropdownCoordinates>({ x: 0, y: 0 });\n const [shouldUseTopAlignment, setShouldUseTopAlignment] = useState(false);\n const [availableMaxHeight, setAvailableMaxHeight] = useState<number>(0);\n\n // Stores the alignment decision (top/bottom) that was made when the dropdown opened. The\n // decision depends on the content height, but the content height in turn is limited by the\n // available height (which depends on the alignment). Recalculating the alignment on every\n // change would create a feedback loop that makes the alignment flip and the height oscillate.\n // We therefore lock the alignment while the dropdown stays open and reset it on close.\n const lockedTopAlignmentRef = useRef<boolean | null>(null);\n\n const calculateCoordinates = useCallback(() => {\n // While the dropdown is closing (or closed) we must not recalculate the position and the\n // available height. Otherwise layout changes underneath the dropdown (e.g. content that\n // appears after a selection) would move or resize the dropdown body while it is still fading\n // out, causing it to visibly jump. Freezing the last calculated values keeps the closing\n // animation stable.\n if (!shouldShowDropdown) {\n return;\n }\n\n if (container) {\n const {\n left: anchorLeft,\n top: anchorTop,\n height: anchorHeight,\n } = anchorElement.getBoundingClientRect();\n\n const { left, top, height } = container.getBoundingClientRect();\n\n const x = anchorLeft - left + container.scrollLeft;\n const y = anchorTop - top + container.scrollTop;\n\n let useTopAlignment = [\n DropdownDirection.TOP,\n DropdownDirection.TOP_LEFT,\n DropdownDirection.TOP_RIGHT,\n ].includes(direction);\n\n const hasBottomAlignment = [\n DropdownDirection.BOTTOM,\n DropdownDirection.BOTTOM_LEFT,\n DropdownDirection.BOTTOM_RIGHT,\n ].includes(direction);\n\n // The available space above and below the anchor within the container.\n const spaceToTop = y;\n const spaceToBottom = height - (y + anchorHeight);\n\n if (lockedTopAlignmentRef.current !== null) {\n // Keep the alignment that was decided when the dropdown opened.\n useTopAlignment = lockedTopAlignmentRef.current;\n } else if (!hasBottomAlignment && y + anchorHeight + contentHeight > height) {\n // The content does not fit below the anchor. Only flip to the top when there is\n // actually more space above than below. Otherwise the content would be cut off less\n // when opened downwards (the content is limited to the available height anyway), so\n // we keep the downward alignment. This avoids opening upwards for a content that,\n // once limited to the available space, would fit below just fine.\n useTopAlignment = spaceToTop > spaceToBottom;\n }\n\n lockedTopAlignmentRef.current = useTopAlignment;\n\n setShouldUseTopAlignment(useTopAlignment);\n\n // Calculate the space that is available for the dropdown content. When the dropdown is\n // opened to the top, the available space reaches from the anchor to the top edge of the\n // container. When it is opened to the bottom, it reaches from the bottom of the anchor\n // to the bottom edge of the container. A small spacing is subtracted so the content does\n // not touch the container edge (e.g. to leave room for shadows).\n\n const nextAvailableMaxHeight = Math.max(\n Math.round(\n (useTopAlignment ? spaceToTop : spaceToBottom) - AVAILABLE_HEIGHT_SPACING,\n ),\n 0,\n );\n\n // Ignore sub-pixel fluctuations so tiny changes from getBoundingClientRect do not\n // repeatedly trigger re-renders that could keep the height oscillating.\n setAvailableMaxHeight((currentAvailableMaxHeight) =>\n Math.abs(currentAvailableMaxHeight - nextAvailableMaxHeight) <= 1\n ? currentAvailableMaxHeight\n : nextAvailableMaxHeight,\n );\n\n setCoordinates({ x, y: useTopAlignment ? y : y + anchorHeight });\n }\n }, [anchorElement, container, contentHeight, direction, shouldShowDropdown]);\n\n useIsomorphicLayoutEffect(() => {\n // Reset the locked alignment whenever the dropdown is closed, so the next time it opens the\n // alignment (top/bottom) is decided freshly based on the then-available space.\n if (!shouldShowDropdown) {\n lockedTopAlignmentRef.current = null;\n }\n\n const handleResize = () => {\n calculateCoordinates();\n\n setTimeout(calculateCoordinates, 300);\n };\n\n handleResize();\n\n if (shouldShowDropdown) {\n window.addEventListener('resize', handleResize);\n }\n\n return () => {\n window.removeEventListener('resize', handleResize);\n };\n }, [calculateCoordinates, shouldShowDropdown]);\n\n return useMemo(\n () => ({ shouldUseTopAlignment, coordinates, availableMaxHeight }),\n [availableMaxHeight, coordinates, shouldUseTopAlignment],\n );\n};\n\ninterface UseDropdownOptions {\n anchorElement: Element;\n container?: Element;\n contentHeight?: number;\n contentWidth: number;\n direction: DropdownDirection;\n shouldShowDropdown: boolean;\n}\n\nexport const useDropdown = ({\n anchorElement,\n container,\n contentHeight,\n contentWidth,\n direction,\n shouldShowDropdown,\n}: UseDropdownOptions) => {\n const { shouldUseTopAlignment, coordinates, availableMaxHeight } = useDropdownPosition({\n anchorElement,\n container,\n contentHeight,\n direction,\n shouldShowDropdown,\n });\n\n const transform = useDropdownAlignment({\n anchorElement,\n contentWidth,\n direction,\n shouldUseTopAlignment,\n });\n\n const width = anchorElement.clientWidth;\n\n return useMemo(\n () => ({ coordinates, transform, width, availableMaxHeight }),\n [availableMaxHeight, coordinates, transform, width],\n );\n};\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chayns-components/core",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.7",
|
|
4
4
|
"description": "A set of beautiful React components for developing your own applications with chayns.",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"browserslist": [
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
},
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "4b2895fc123ff2178d65fe4714c9630b2741ca64"
|
|
92
92
|
}
|