@12-apps/ui 6.45.0 → 6.45.2
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/dist/_internal/components/data-display/HoverCard/HoverCard.hooks.js +1 -1
- package/dist/_internal/components/data-display/HoverCard/HoverCard.js +2 -2
- package/dist/_internal/components/data-display/Lightbox/Lightbox.js +1 -1
- package/dist/_internal/components/data-display/index.js +3 -3
- package/dist/_internal/components/index.js +3 -3
- package/dist/{chunk-Q534UJ34.js → chunk-KN3WYEH2.js} +54 -35
- package/dist/chunk-KN3WYEH2.js.map +1 -0
- package/dist/{chunk-IIXMTC65.js → chunk-SPZAEMGW.js} +8 -2
- package/dist/chunk-SPZAEMGW.js.map +1 -0
- package/dist/{chunk-VNWZWQF3.js → chunk-Z3USLQQT.js} +69 -24
- package/dist/chunk-Z3USLQQT.js.map +1 -0
- package/dist/data-display/HoverCard.js +2 -2
- package/dist/data-display/Lightbox.js +1 -1
- package/dist/types/components/data-display/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/types/components/data-display/HoverCard/HoverCard.hooks.d.ts +2 -8
- package/dist/types/components/data-display/HoverCard/HoverCard.hooks.d.ts.map +1 -1
- package/dist/types/components/data-display/Lightbox/Lightbox.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/HoverCard/HoverCard.hooks.ts +173 -33
- package/src/components/data-display/HoverCard/HoverCard.md +1 -0
- package/src/components/data-display/HoverCard/HoverCard.tsx +38 -16
- package/src/components/data-display/Lightbox/Lightbox.tsx +7 -1
- package/dist/chunk-IIXMTC65.js.map +0 -1
- package/dist/chunk-Q534UJ34.js.map +0 -1
- package/dist/chunk-VNWZWQF3.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HoverCard
|
|
3
|
-
} from "../../../../chunk-
|
|
3
|
+
} from "../../../../chunk-KN3WYEH2.js";
|
|
4
4
|
import "../../../../chunk-NRIV2CQ2.js";
|
|
5
5
|
import "../../../../chunk-BSYPPO2I.js";
|
|
6
6
|
import "../../../../chunk-ZHV7TCAC.js";
|
|
7
|
-
import "../../../../chunk-
|
|
7
|
+
import "../../../../chunk-Z3USLQQT.js";
|
|
8
8
|
import "../../../../chunk-HPK33ACY.js";
|
|
9
9
|
import "../../../../chunk-FQ6UW7IY.js";
|
|
10
10
|
import "../../../../chunk-6JAIFIPW.js";
|
|
@@ -77,11 +77,11 @@ import {
|
|
|
77
77
|
import "../../../chunk-6OS4AKWT.js";
|
|
78
78
|
import {
|
|
79
79
|
HoverCard
|
|
80
|
-
} from "../../../chunk-
|
|
80
|
+
} from "../../../chunk-KN3WYEH2.js";
|
|
81
81
|
import "../../../chunk-NRIV2CQ2.js";
|
|
82
82
|
import "../../../chunk-BSYPPO2I.js";
|
|
83
83
|
import "../../../chunk-ZHV7TCAC.js";
|
|
84
|
-
import "../../../chunk-
|
|
84
|
+
import "../../../chunk-Z3USLQQT.js";
|
|
85
85
|
import "../../../chunk-TAF3YYMK.js";
|
|
86
86
|
import {
|
|
87
87
|
InteractiveTooltip
|
|
@@ -97,7 +97,7 @@ import "../../../chunk-724ZONOL.js";
|
|
|
97
97
|
import "../../../chunk-UM35GGCS.js";
|
|
98
98
|
import {
|
|
99
99
|
Lightbox_default
|
|
100
|
-
} from "../../../chunk-
|
|
100
|
+
} from "../../../chunk-SPZAEMGW.js";
|
|
101
101
|
import "../../../chunk-UOS7DEQZ.js";
|
|
102
102
|
import "../../../chunk-HJPPNVCF.js";
|
|
103
103
|
import "../../../chunk-M6PWPEAV.js";
|
|
@@ -505,11 +505,11 @@ import {
|
|
|
505
505
|
import "../../chunk-6OS4AKWT.js";
|
|
506
506
|
import {
|
|
507
507
|
HoverCard
|
|
508
|
-
} from "../../chunk-
|
|
508
|
+
} from "../../chunk-KN3WYEH2.js";
|
|
509
509
|
import "../../chunk-NRIV2CQ2.js";
|
|
510
510
|
import "../../chunk-BSYPPO2I.js";
|
|
511
511
|
import "../../chunk-ZHV7TCAC.js";
|
|
512
|
-
import "../../chunk-
|
|
512
|
+
import "../../chunk-Z3USLQQT.js";
|
|
513
513
|
import "../../chunk-TAF3YYMK.js";
|
|
514
514
|
import {
|
|
515
515
|
InteractiveTooltip
|
|
@@ -525,7 +525,7 @@ import "../../chunk-724ZONOL.js";
|
|
|
525
525
|
import "../../chunk-UM35GGCS.js";
|
|
526
526
|
import {
|
|
527
527
|
Lightbox_default
|
|
528
|
-
} from "../../chunk-
|
|
528
|
+
} from "../../chunk-SPZAEMGW.js";
|
|
529
529
|
import "../../chunk-UOS7DEQZ.js";
|
|
530
530
|
import "../../chunk-HJPPNVCF.js";
|
|
531
531
|
import "../../chunk-M6PWPEAV.js";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./chunk-BSYPPO2I.js";
|
|
11
11
|
import {
|
|
12
12
|
useHoverCard
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-Z3USLQQT.js";
|
|
14
14
|
import {
|
|
15
15
|
withDefaults
|
|
16
16
|
} from "./chunk-HPK33ACY.js";
|
|
@@ -75,6 +75,14 @@ var capOf = (theme, maxWidth) => {
|
|
|
75
75
|
const cap = maxWidth ?? 400;
|
|
76
76
|
return cap <= 1 && cap !== 0 ? `${cap * 100}%` : rem(theme, cap);
|
|
77
77
|
};
|
|
78
|
+
var buildTriggerElement = (trigger, triggerHandlers, disabled, dataTestId, children) => {
|
|
79
|
+
const triggerProps = {
|
|
80
|
+
...triggerHandlers,
|
|
81
|
+
style: { cursor: disabled ? "default" : "pointer" },
|
|
82
|
+
"data-testid": dataTestId ? `${dataTestId}-trigger` : "hover-card-trigger"
|
|
83
|
+
};
|
|
84
|
+
return trigger ? React.cloneElement(trigger, triggerProps) : /* @__PURE__ */ jsx("span", { ...triggerProps, style: { display: "inline-block", ...triggerProps.style }, children });
|
|
85
|
+
};
|
|
78
86
|
var HoverCard = React.forwardRef((props, ref) => {
|
|
79
87
|
const {
|
|
80
88
|
variant,
|
|
@@ -102,7 +110,15 @@ var HoverCard = React.forwardRef((props, ref) => {
|
|
|
102
110
|
dataTestId,
|
|
103
111
|
...rest
|
|
104
112
|
} = withDefaults(props, DEFAULTS);
|
|
105
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
anchorEl,
|
|
115
|
+
isOpen,
|
|
116
|
+
handleClose,
|
|
117
|
+
triggerHandlers,
|
|
118
|
+
cardHandlers,
|
|
119
|
+
contentRef,
|
|
120
|
+
onContentPointerDownCapture
|
|
121
|
+
} = useHoverCard({
|
|
106
122
|
disabled,
|
|
107
123
|
touchEnabled,
|
|
108
124
|
enterDelay,
|
|
@@ -110,12 +126,7 @@ var HoverCard = React.forwardRef((props, ref) => {
|
|
|
110
126
|
onOpen,
|
|
111
127
|
onClose
|
|
112
128
|
});
|
|
113
|
-
const
|
|
114
|
-
...triggerHandlers,
|
|
115
|
-
style: { cursor: disabled ? "default" : "pointer" },
|
|
116
|
-
"data-testid": dataTestId ? `${dataTestId}-trigger` : "hover-card-trigger"
|
|
117
|
-
};
|
|
118
|
-
const triggerElement = trigger ? React.cloneElement(trigger, triggerProps) : /* @__PURE__ */ jsx("span", { ...triggerProps, style: { display: "inline-block", ...triggerProps.style }, children });
|
|
129
|
+
const triggerElement = buildTriggerElement(trigger, triggerHandlers, disabled, dataTestId, children);
|
|
119
130
|
const padding = CONTENT_PADDING[variant] ?? 2;
|
|
120
131
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
121
132
|
triggerElement,
|
|
@@ -135,34 +146,42 @@ var HoverCard = React.forwardRef((props, ref) => {
|
|
|
135
146
|
customAnimation: animation,
|
|
136
147
|
slotProps: { paper: { style: { pointerEvents: "auto" } } },
|
|
137
148
|
...rest,
|
|
138
|
-
children: /* @__PURE__ */ jsxs(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
sx: (theme) => ({ maxWidth: capOf(theme, maxWidth), ...offsetMargin(theme, placement, offset) }),
|
|
149
|
-
children: /* @__PURE__ */ jsx(CardContent, { sx: { p: padding, "&:last-child": { pb: padding } }, children: /* @__PURE__ */ jsx(
|
|
150
|
-
HoverCardContent,
|
|
149
|
+
children: /* @__PURE__ */ jsxs(
|
|
150
|
+
Box,
|
|
151
|
+
{
|
|
152
|
+
ref: contentRef,
|
|
153
|
+
onPointerDownCapture: onContentPointerDownCapture,
|
|
154
|
+
sx: { position: "relative" },
|
|
155
|
+
children: [
|
|
156
|
+
showArrow && /* @__PURE__ */ jsx(ArrowContainer, { placement, offset }),
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
StyledCard,
|
|
151
159
|
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
customVariant: variant,
|
|
161
|
+
glow,
|
|
162
|
+
pulse,
|
|
163
|
+
animation,
|
|
164
|
+
"data-testid": dataTestId || "hover-card-content",
|
|
165
|
+
sx: (theme) => ({ maxWidth: capOf(theme, maxWidth), ...offsetMargin(theme, placement, offset) }),
|
|
166
|
+
children: /* @__PURE__ */ jsx(CardContent, { sx: { p: padding, "&:last-child": { pb: padding } }, children: /* @__PURE__ */ jsx(
|
|
167
|
+
HoverCardContent,
|
|
168
|
+
{
|
|
169
|
+
loadingText,
|
|
170
|
+
variant,
|
|
171
|
+
title,
|
|
172
|
+
description,
|
|
173
|
+
avatar,
|
|
174
|
+
loading,
|
|
175
|
+
loadingComponent,
|
|
176
|
+
dataTestId,
|
|
177
|
+
children
|
|
178
|
+
}
|
|
179
|
+
) })
|
|
161
180
|
}
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
)
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
)
|
|
166
185
|
}
|
|
167
186
|
)
|
|
168
187
|
] });
|
|
@@ -172,4 +191,4 @@ HoverCard.displayName = "HoverCard";
|
|
|
172
191
|
export {
|
|
173
192
|
HoverCard
|
|
174
193
|
};
|
|
175
|
-
//# sourceMappingURL=chunk-
|
|
194
|
+
//# sourceMappingURL=chunk-KN3WYEH2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/data-display/HoverCard/HoverCard.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport Card from '@mui/material/Card/index.js';\nimport CardContent from '@mui/material/CardContent/index.js';\nimport Popover from '@mui/material/Popover/index.js';\nimport { styled, type Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { rem } from '../../../tokens/relative';\nimport { withDefaults } from '../../../utils/withDefaults';\n\nimport { HoverCardContent } from './HoverCard.content';\nimport { useHoverCard } from './HoverCard.hooks';\nimport { arrowSx, cardSx, getAnchorOrigin, getTransformOrigin, sideOf } from './HoverCard.styles';\nimport type { CardStyleFlags } from './HoverCard.styles';\nimport type { HoverCardAnimation, HoverCardPlacement, HoverCardProps } from './HoverCard.types';\n\nconst ArrowContainer = styled('div')<{ placement: HoverCardPlacement; offset?: number }>(\n ({ theme, placement }) => ({ ...arrowSx(theme, placement) }),\n);\n\nconst StyledCard = styled(Card, {\n shouldForwardProp: (prop) =>\n !['customVariant', 'glow', 'pulse', 'animation'].includes(prop as string),\n})<CardStyleFlags>(({ theme, ...flags }) => ({ ...cardSx(theme, flags) }));\n\nconst StyledPopover = styled(Popover, {\n shouldForwardProp: (prop) => prop !== 'customAnimation',\n})<{ customAnimation?: HoverCardAnimation }>(({ customAnimation }) => ({\n // Only the card takes the pointer (its paper sets `pointer-events: auto`).\n // MUI's popover root and its invisible backdrop cover the whole viewport, so\n // while a card was open the pointer was \"on the card\" wherever it went: the\n // backdrop's mouseover reached the card's onMouseEnter and cancelled the\n // close the trigger's mouseleave had just scheduled. A card left by moving\n // the mouse away stayed open, and the page's next click hit the backdrop\n // instead of its target (FUT-2619). The backdrop was also the only\n // click-away; `useClickAway` in the hooks replaces it.\n pointerEvents: 'none',\n // The popover only positions; StyledCard draws the surface.\n '& .MuiPopover-paper': {\n backgroundColor: 'transparent',\n boxShadow: 'none',\n overflow: 'visible',\n ...(customAnimation === 'scale' && { transformOrigin: 'center' }),\n },\n}));\n\nconst DEFAULTS = {\n variant: 'default',\n glow: false,\n pulse: false,\n placement: 'bottom',\n showArrow: false,\n animation: 'fade',\n enterDelay: 700,\n exitDelay: 0,\n loading: false,\n touchEnabled: true,\n disabled: false,\n} satisfies Partial<HoverCardProps>;\n\ntype ResolvedProps = HoverCardProps & Required<Pick<HoverCardProps, keyof typeof DEFAULTS>>;\n\n/** Padding is the variant's own: detailed roomier, minimal tighter. */\nconst CONTENT_PADDING: Record<string, number> = { detailed: 3, minimal: 1.5 };\n\n/**\n * The card is nudged off the anchor on whichever side it sits — `offset` design\n * px (8 unless the caller says otherwise), through the theme's type scale.\n */\nconst offsetMargin = (theme: Theme, placement: HoverCardPlacement, offset: number | undefined) => {\n const side = sideOf(placement);\n const key = ({ top: 'mt', bottom: 'mb', left: 'ml', right: 'mr' } as const)[side];\n return { [key]: rem(theme, offset ?? 8) };\n};\n\n/**\n * The card's cap: 400 design px unless the caller says otherwise. `sx` has always\n * read a number of 1 or less as a fraction of the parent, and that stays so.\n */\nconst capOf = (theme: Theme, maxWidth: number | undefined): string => {\n const cap = maxWidth ?? 400;\n return cap <= 1 && cap !== 0 ? `${cap * 100}%` : rem(theme, cap);\n};\n\n/** The clone-onto-`trigger` vs. wrap-in-a-`span` choice, pulled out of the component so its body stays readable. */\nconst buildTriggerElement = (\n trigger: HoverCardProps['trigger'],\n triggerHandlers: ReturnType<typeof useHoverCard>['triggerHandlers'],\n disabled: boolean,\n dataTestId: string | undefined,\n children: React.ReactNode,\n) => {\n const triggerProps = {\n ...triggerHandlers,\n style: { cursor: disabled ? 'default' : 'pointer' },\n 'data-testid': dataTestId ? `${dataTestId}-trigger` : 'hover-card-trigger',\n };\n\n return trigger ? (\n React.cloneElement(trigger, triggerProps)\n ) : (\n <span {...triggerProps} style={{ display: 'inline-block', ...triggerProps.style }}>\n {children}\n </span>\n );\n};\n\nexport const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props, ref) => {\n const {\n variant, glow, pulse, title, description, avatar, trigger, placement, showArrow,\n animation, enterDelay, exitDelay, maxWidth, loading, loadingComponent, loadingText, touchEnabled,\n offset, disabled, onOpen, onClose, children, dataTestId,\n ...rest\n } = withDefaults(props, DEFAULTS) as ResolvedProps;\n\n const {\n anchorEl,\n isOpen,\n handleClose,\n triggerHandlers,\n cardHandlers,\n contentRef,\n onContentPointerDownCapture,\n } = useHoverCard({\n disabled,\n touchEnabled,\n enterDelay,\n exitDelay,\n onOpen,\n onClose,\n });\n\n const triggerElement = buildTriggerElement(trigger, triggerHandlers, disabled, dataTestId, children);\n const padding = CONTENT_PADDING[variant] ?? 2;\n\n return (\n <>\n {triggerElement}\n <StyledPopover\n ref={ref}\n open={isOpen && !disabled}\n anchorEl={anchorEl}\n onClose={handleClose}\n anchorOrigin={getAnchorOrigin(placement)}\n transformOrigin={getTransformOrigin(placement)}\n {...cardHandlers}\n disableAutoFocus\n disableEnforceFocus\n disableRestoreFocus\n customAnimation={animation}\n slotProps={{ paper: { style: { pointerEvents: 'auto' } } }}\n {...rest}\n >\n <Box\n ref={contentRef}\n onPointerDownCapture={onContentPointerDownCapture}\n sx={{ position: 'relative' }}\n >\n {showArrow && <ArrowContainer placement={placement} offset={offset} />}\n <StyledCard\n customVariant={variant}\n glow={glow}\n pulse={pulse}\n animation={animation}\n data-testid={dataTestId || 'hover-card-content'}\n sx={(theme) => ({ maxWidth: capOf(theme, maxWidth), ...offsetMargin(theme, placement, offset) })}\n >\n <CardContent sx={{ p: padding, '&:last-child': { pb: padding } }}>\n <HoverCardContent\n loadingText={loadingText} variant={variant} title={title}\n description={description}\n avatar={avatar}\n loading={loading}\n loadingComponent={loadingComponent}\n dataTestId={dataTestId}\n >\n {children}\n </HoverCardContent>\n </CardContent>\n </StyledCard>\n </Box>\n </StyledPopover>\n </>\n );\n});\n\nHoverCard.displayName = 'HoverCard';\n"],"mappings":"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","names":[]}
|
|
@@ -49,8 +49,14 @@ var DIALOG_STATIC_PROPS = {
|
|
|
49
49
|
// second, non-modal `role="dialog"` this component sets of its own accord
|
|
50
50
|
// — duplicating (and, as a static English literal, contradicting) the name
|
|
51
51
|
// the Paper already carries.
|
|
52
|
+
//
|
|
53
|
+
// For the same reason, `role` is deliberately NOT set here (FUT-2861):
|
|
54
|
+
// `Dialog` destructures `aria-labelledby` and `aria-modal` by name and
|
|
55
|
+
// applies them to the Paper alongside its own hard-coded `role="dialog"`,
|
|
56
|
+
// but `role` itself is not one of the props it destructures — a `role`
|
|
57
|
+
// passed here falls into `...other` and lands a second, unlabelled
|
|
58
|
+
// `role="dialog"` on the outer Modal root instead of the Paper.
|
|
52
59
|
"aria-labelledby": "lightbox-title",
|
|
53
|
-
role: "dialog",
|
|
54
60
|
"aria-modal": "true"
|
|
55
61
|
};
|
|
56
62
|
var Lightbox = React.forwardRef((componentProps, ref) => {
|
|
@@ -142,4 +148,4 @@ export {
|
|
|
142
148
|
Lightbox,
|
|
143
149
|
Lightbox_default
|
|
144
150
|
};
|
|
145
|
-
//# sourceMappingURL=chunk-
|
|
151
|
+
//# sourceMappingURL=chunk-SPZAEMGW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/data-display/Lightbox/Lightbox.tsx"],"sourcesContent":["import Dialog from '@mui/material/Dialog/index.js';\nimport Fade from '@mui/material/Fade/index.js';\nimport Typography from '@mui/material/Typography/index.js';\nimport type { Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { makeTestId } from './Lightbox.constants';\nimport { resolveLightboxProps } from './Lightbox.helpers';\nimport { LightboxCopyProvider } from './lightbox-copy-context';\nimport type { LightboxProps, LightboxRef } from './Lightbox.types';\nimport { LightboxCurrentMedia } from './LightboxCurrentMedia';\nimport { LightboxOverlay } from './LightboxOverlay';\nimport { LightboxStage } from './LightboxStage';\nimport { LightboxThumbnails } from './LightboxThumbnails';\nimport { useLightbox } from './useLightbox';\nimport { scrim } from '../../../tokens/ink';\nimport { rem, sxRem } from '../../../tokens/relative';\n\nconst DIALOG_STATIC_PROPS = {\n maxWidth: false,\n fullScreen: true,\n TransitionComponent: Fade,\n TransitionProps: { timeout: 300 },\n PaperProps: { sx: { background: (theme: Theme) => scrim(theme, 0.9), backdropFilter: (theme: Theme) => `blur(${rem(theme, 2)})` } },\n // `aria-labelledby` is the one accessible name for the modal itself: MUI\n // forwards it (with `aria-modal` and its own `role=\"dialog\"`) onto the\n // Paper, the element that is actually the WAI-ARIA dialog. A plain\n // `aria-label` alongside it would land on the OUTER root instead — a\n // second, non-modal `role=\"dialog\"` this component sets of its own accord\n // — duplicating (and, as a static English literal, contradicting) the name\n // the Paper already carries.\n //\n // For the same reason, `role` is deliberately NOT set here (FUT-2861):\n // `Dialog` destructures `aria-labelledby` and `aria-modal` by name and\n // applies them to the Paper alongside its own hard-coded `role=\"dialog\"`,\n // but `role` itself is not one of the props it destructures — a `role`\n // passed here falls into `...other` and lands a second, unlabelled\n // `role=\"dialog\"` on the outer Modal root instead of the Paper.\n 'aria-labelledby': 'lightbox-title',\n 'aria-modal': 'true',\n} as const;\n\nexport const Lightbox = React.forwardRef<LightboxRef, LightboxProps>((componentProps, ref) => {\n const props = resolveLightboxProps(componentProps);\n const { isOpen, items, loop, autoplay, showControls, showCaptions, thumbnails, zoomable } = props;\n const { className, style, dataTestId } = props;\n\n const testId = makeTestId(dataTestId);\n const lightbox = useLightbox(props, ref);\n const { currentIndex, currentItem, zoom } = lightbox;\n\n return (\n <LightboxCopyProvider copy={props.copy}>\n <Dialog\n open={isOpen}\n onClose={lightbox.close}\n {...DIALOG_STATIC_PROPS}\n className={className}\n style={style}\n onTouchStart={lightbox.handleTouchStart}\n onTouchEnd={lightbox.handleTouchEnd}\n data-testid={dataTestId || 'lightbox'}\n >\n {/* Visually hidden title for screen readers */}\n <Typography id=\"lightbox-title\" variant=\"h6\" sx={{ position: 'absolute', left: sxRem(-10000) }}>\n {props.copy.dialogLabel} - {currentItem?.alt || props.copy.itemPosition(currentIndex + 1, items.length)}\n </Typography>\n\n <LightboxOverlay\n testId={testId}\n itemCount={items.length}\n currentIndex={currentIndex}\n showControls={showControls}\n loop={loop}\n autoplay={autoplay}\n canZoom={lightbox.canZoom}\n isAutoPlaying={lightbox.isAutoPlaying}\n onClose={lightbox.close}\n onPrev={lightbox.prev}\n onNext={lightbox.next}\n onZoomIn={zoom.zoomIn}\n onZoomOut={zoom.zoomOut}\n onResetZoom={zoom.resetZoom}\n onToggleAutoplay={() => lightbox.setIsAutoPlaying((playing) => !playing)}\n />\n\n <LightboxStage\n items={items}\n currentItem={currentItem}\n currentIndex={currentIndex}\n isLoading={lightbox.isLoading}\n showCaptions={showCaptions}\n thumbnails={thumbnails}\n testId={testId}\n media={\n <LightboxCurrentMedia\n item={currentItem}\n index={currentIndex}\n total={items.length}\n zoomable={zoomable}\n zoom={zoom}\n testId={testId}\n onLoadingChange={lightbox.setIsLoading}\n onWheel={lightbox.handleWheel}\n />\n }\n thumbnailStrip={\n thumbnails &&\n items.length > 1 && (\n <LightboxThumbnails\n items={items}\n currentIndex={currentIndex}\n testId={testId}\n onSelect={lightbox.goToIndex}\n />\n )\n }\n />\n </Dialog>\n </LightboxCopyProvider>\n );\n});\n\nLightbox.displayName = 'Lightbox';\n\nexport default Lightbox;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB,OAAO,gBAAgB;AAEvB,OAAO,WAAW;AA4DZ,SAIA,KAJA;AA9CN,IAAM,sBAAsB;AAAA,EAC1B,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,qBAAqB;AAAA,EACrB,iBAAiB,EAAE,SAAS,IAAI;AAAA,EAChC,YAAY,EAAE,IAAI,EAAE,YAAY,CAAC,UAAiB,MAAM,OAAO,GAAG,GAAG,gBAAgB,CAAC,UAAiB,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAelI,mBAAmB;AAAA,EACnB,cAAc;AAChB;AAEO,IAAM,WAAW,MAAM,WAAuC,CAAC,gBAAgB,QAAQ;AAC5F,QAAM,QAAQ,qBAAqB,cAAc;AACjD,QAAM,EAAE,QAAQ,OAAO,MAAM,UAAU,cAAc,cAAc,YAAY,SAAS,IAAI;AAC5F,QAAM,EAAE,WAAW,OAAO,WAAW,IAAI;AAEzC,QAAM,SAAS,WAAW,UAAU;AACpC,QAAM,WAAW,YAAY,OAAO,GAAG;AACvC,QAAM,EAAE,cAAc,aAAa,KAAK,IAAI;AAE5C,SACE,oBAAC,wBAAqB,MAAM,MAAM,MAClC;AAAA,IAAC;AAAA;AAAA,MACC,MAAM;AAAA,MACN,SAAS,SAAS;AAAA,MACjB,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA,cAAc,SAAS;AAAA,MACvB,YAAY,SAAS;AAAA,MACrB,eAAa,cAAc;AAAA,MAG3B;AAAA,6BAAC,cAAW,IAAG,kBAAiB,SAAQ,MAAK,IAAI,EAAE,UAAU,YAAY,MAAM,MAAM,IAAM,EAAE,GAC1F;AAAA,gBAAM,KAAK;AAAA,UAAY;AAAA,UAAI,aAAa,OAAO,MAAM,KAAK,aAAa,eAAe,GAAG,MAAM,MAAM;AAAA,WACxG;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,WAAW,MAAM;AAAA,YACjB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,SAAS,SAAS;AAAA,YAClB,eAAe,SAAS;AAAA,YACxB,SAAS,SAAS;AAAA,YAClB,QAAQ,SAAS;AAAA,YACjB,QAAQ,SAAS;AAAA,YACjB,UAAU,KAAK;AAAA,YACf,WAAW,KAAK;AAAA,YAChB,aAAa,KAAK;AAAA,YAClB,kBAAkB,MAAM,SAAS,iBAAiB,CAAC,YAAY,CAAC,OAAO;AAAA;AAAA,QACzE;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,WAAW,SAAS;AAAA,YACpB;AAAA,YACA;AAAA,YACA;AAAA,YACA,OACE;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,gBACP,OAAO,MAAM;AAAA,gBACb;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,iBAAiB,SAAS;AAAA,gBAC1B,SAAS,SAAS;AAAA;AAAA,YACpB;AAAA,YAEF,gBACE,cACA,MAAM,SAAS,KACb;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,UAAU,SAAS;AAAA;AAAA,YACrB;AAAA;AAAA,QAGN;AAAA;AAAA;AAAA,EACF,GACA;AAEJ,CAAC;AAED,SAAS,cAAc;AAEvB,IAAO,mBAAQ;","names":[]}
|
|
@@ -61,18 +61,49 @@ var useEscapeKey = (active, onEscape) => {
|
|
|
61
61
|
return () => document.removeEventListener("keydown", handleEscape);
|
|
62
62
|
}, [active, onEscape]);
|
|
63
63
|
};
|
|
64
|
-
var useClickAway = (active, inside, onAway) => {
|
|
64
|
+
var useClickAway = (active, inside, ownsEvent, onAway) => {
|
|
65
|
+
const sessionRef = React.useRef(null);
|
|
66
|
+
const pendingRef = React.useRef(/* @__PURE__ */ new Set());
|
|
65
67
|
React.useEffect(() => {
|
|
66
|
-
if (!active) return;
|
|
68
|
+
if (!active) return void 0;
|
|
69
|
+
const session = {};
|
|
70
|
+
sessionRef.current = session;
|
|
67
71
|
const handlePointerDown = (event) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
const timeoutId = window.setTimeout(() => {
|
|
73
|
+
pendingRef.current.delete(timeoutId);
|
|
74
|
+
if (sessionRef.current !== session) return;
|
|
75
|
+
const target = event.target;
|
|
76
|
+
const ownedByReactTree = ownsEvent(event);
|
|
77
|
+
const ownedByDom = Boolean(target) && inside().some((el) => el?.contains(target));
|
|
78
|
+
if (ownedByReactTree || ownedByDom) return;
|
|
79
|
+
onAway();
|
|
80
|
+
}, 0);
|
|
81
|
+
pendingRef.current.add(timeoutId);
|
|
71
82
|
};
|
|
72
83
|
document.addEventListener("pointerdown", handlePointerDown, true);
|
|
73
|
-
return () =>
|
|
74
|
-
|
|
84
|
+
return () => {
|
|
85
|
+
sessionRef.current = null;
|
|
86
|
+
pendingRef.current.forEach((id) => window.clearTimeout(id));
|
|
87
|
+
pendingRef.current.clear();
|
|
88
|
+
document.removeEventListener("pointerdown", handlePointerDown, true);
|
|
89
|
+
};
|
|
90
|
+
}, [active, inside, ownsEvent, onAway]);
|
|
75
91
|
};
|
|
92
|
+
var useCardOwnership = () => {
|
|
93
|
+
const ownedEventsRef = React.useRef(/* @__PURE__ */ new WeakSet());
|
|
94
|
+
const markInside = React.useCallback((event) => {
|
|
95
|
+
ownedEventsRef.current.add(event.nativeEvent);
|
|
96
|
+
}, []);
|
|
97
|
+
const ownsEvent = React.useCallback(
|
|
98
|
+
(event) => ownedEventsRef.current.has(event),
|
|
99
|
+
[]
|
|
100
|
+
);
|
|
101
|
+
return { markInside, ownsEvent };
|
|
102
|
+
};
|
|
103
|
+
var useInsideCard = (anchorEl, contentRef) => React.useCallback(
|
|
104
|
+
() => [anchorEl, contentRef.current],
|
|
105
|
+
[anchorEl, contentRef]
|
|
106
|
+
);
|
|
76
107
|
var useClearOnUnmount = (...refs) => {
|
|
77
108
|
React.useEffect(
|
|
78
109
|
() => () => refs.forEach(clearPending),
|
|
@@ -80,23 +111,11 @@ var useClearOnUnmount = (...refs) => {
|
|
|
80
111
|
[]
|
|
81
112
|
);
|
|
82
113
|
};
|
|
83
|
-
var
|
|
84
|
-
disabled,
|
|
85
|
-
touchEnabled,
|
|
86
|
-
enterDelay,
|
|
87
|
-
exitDelay,
|
|
88
|
-
onOpen,
|
|
89
|
-
onClose
|
|
90
|
-
}) => {
|
|
114
|
+
var useCardVisibility = ({ disabled, enterDelay, exitDelay, onOpen, onClose }) => {
|
|
91
115
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
|
92
116
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
93
|
-
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
|
|
94
117
|
const enterTimeoutRef = React.useRef(void 0);
|
|
95
118
|
const exitTimeoutRef = React.useRef(void 0);
|
|
96
|
-
const contentRef = React.useRef(null);
|
|
97
|
-
React.useEffect(() => {
|
|
98
|
-
setIsTouchDevice("ontouchstart" in window);
|
|
99
|
-
}, []);
|
|
100
119
|
const openCard = React.useCallback(
|
|
101
120
|
(trigger) => {
|
|
102
121
|
if (disabled) return;
|
|
@@ -126,6 +145,22 @@ var useHoverCard = ({
|
|
|
126
145
|
setAnchorEl(null);
|
|
127
146
|
onClose?.();
|
|
128
147
|
}, [onClose]);
|
|
148
|
+
return { anchorEl, isOpen, openCard, closeCard, handleClose, enterTimeoutRef, exitTimeoutRef };
|
|
149
|
+
};
|
|
150
|
+
var useHoverCard = ({
|
|
151
|
+
disabled,
|
|
152
|
+
touchEnabled,
|
|
153
|
+
enterDelay,
|
|
154
|
+
exitDelay,
|
|
155
|
+
onOpen,
|
|
156
|
+
onClose
|
|
157
|
+
}) => {
|
|
158
|
+
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
|
|
159
|
+
const contentRef = React.useRef(null);
|
|
160
|
+
const { anchorEl, isOpen, openCard, closeCard, handleClose, enterTimeoutRef, exitTimeoutRef } = useCardVisibility({ disabled, enterDelay, exitDelay, onOpen, onClose });
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
setIsTouchDevice("ontouchstart" in window);
|
|
163
|
+
}, []);
|
|
129
164
|
const { triggerHandlers, cardHandlers } = useHoverHandlers({
|
|
130
165
|
isTouchDevice,
|
|
131
166
|
touchEnabled,
|
|
@@ -134,14 +169,24 @@ var useHoverCard = ({
|
|
|
134
169
|
closeCard,
|
|
135
170
|
exitTimeoutRef
|
|
136
171
|
});
|
|
137
|
-
const insideCard =
|
|
172
|
+
const insideCard = useInsideCard(anchorEl, contentRef);
|
|
173
|
+
const { markInside, ownsEvent } = useCardOwnership();
|
|
138
174
|
useEscapeKey(isOpen, handleClose);
|
|
139
|
-
useClickAway(isOpen, insideCard, handleClose);
|
|
175
|
+
useClickAway(isOpen, insideCard, ownsEvent, handleClose);
|
|
140
176
|
useClearOnUnmount(enterTimeoutRef, exitTimeoutRef);
|
|
141
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
anchorEl,
|
|
179
|
+
isOpen,
|
|
180
|
+
handleClose,
|
|
181
|
+
triggerHandlers,
|
|
182
|
+
cardHandlers,
|
|
183
|
+
contentRef,
|
|
184
|
+
/** Wire onto the card's content root as `onPointerDownCapture` (FUT-2776). */
|
|
185
|
+
onContentPointerDownCapture: markInside
|
|
186
|
+
};
|
|
142
187
|
};
|
|
143
188
|
|
|
144
189
|
export {
|
|
145
190
|
useHoverCard
|
|
146
191
|
};
|
|
147
|
-
//# sourceMappingURL=chunk-
|
|
192
|
+
//# sourceMappingURL=chunk-Z3USLQQT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/data-display/HoverCard/HoverCard.hooks.ts"],"sourcesContent":["import React from 'react';\n\ntype TimerRef = React.MutableRefObject<number | undefined>;\n\n/**\n * Cancel the timer a ref holds, and forget it. Every place that schedules\n * or dismisses goes through this, so no timer outlives the ref that could\n * cancel it (FUT-2619: an overwritten enter timer reopened a dismissed card).\n */\nconst clearPending = (ref: TimerRef) => {\n if (ref.current !== undefined) window.clearTimeout(ref.current);\n ref.current = undefined;\n};\n\ninterface HandlerInput {\n isTouchDevice: boolean;\n touchEnabled: boolean;\n enterDelay: number;\n openCard: (trigger: HTMLElement) => void;\n closeCard: () => void;\n exitTimeoutRef: TimerRef;\n}\n\n/**\n * The pointer and touch handlers, over the open/close pair. Touch gets a\n * long-press because there is no hover to respond to; the card's own handlers\n * keep it open while the pointer is travelling onto it.\n */\nconst useHoverHandlers = ({\n isTouchDevice,\n touchEnabled,\n enterDelay,\n openCard,\n closeCard,\n exitTimeoutRef,\n}: HandlerInput) => {\n const [touchTimeout, setTouchTimeout] = React.useState<number>();\n\n /** A touch device with touch disabled has no hover to respond to. */\n const hoverIgnored = isTouchDevice && !touchEnabled;\n\n const cancelTouch = React.useCallback(() => {\n if (touchTimeout) {\n window.clearTimeout(touchTimeout);\n setTouchTimeout(undefined);\n }\n }, [touchTimeout]);\n\n React.useEffect(\n () => () => {\n if (touchTimeout) window.clearTimeout(touchTimeout);\n },\n [touchTimeout],\n );\n\n const triggerHandlers = {\n onMouseEnter: (event: React.MouseEvent<HTMLElement>) => {\n if (hoverIgnored) return;\n openCard(event.currentTarget);\n },\n onMouseLeave: () => {\n if (hoverIgnored) return;\n closeCard();\n },\n onTouchStart: (event: React.TouchEvent<HTMLElement>) => {\n if (!touchEnabled) return;\n if (touchTimeout) window.clearTimeout(touchTimeout);\n // Read NOW: React nulls `currentTarget` once the handler returns, so\n // the long-press timer used to open the card with no anchor at all.\n const trigger = event.currentTarget;\n setTouchTimeout(window.setTimeout(() => openCard(trigger), enterDelay));\n },\n // End and cancel do the same thing: the press did not become a long press.\n onTouchEnd: cancelTouch,\n onTouchCancel: cancelTouch,\n };\n\n /** Moving onto the card keeps it open; leaving it starts the exit delay. */\n const cardHandlers = {\n onMouseEnter: () => clearPending(exitTimeoutRef),\n onMouseLeave: closeCard,\n };\n\n return { triggerHandlers, cardHandlers };\n};\n\n/** Escape dismisses the card while it is open. */\nconst useEscapeKey = (active: boolean, onEscape: () => void) => {\n React.useEffect(() => {\n if (!active) return;\n\n const handleEscape = (event: globalThis.KeyboardEvent) => {\n if (event.key === 'Escape') onEscape();\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [active, onEscape]);\n};\n\n/**\n * A press anywhere outside the trigger and the card dismisses it.\n *\n * Touch has no mouse-leave, and MUI's own click-away was the popover's\n * backdrop, which `pointer-events: none` on the popover root (so the page\n * under an open card stays usable) takes out of play. So this listens on the\n * document itself, in the CAPTURE phase, where a `stopPropagation` further in\n * — including one a nested control (or something entirely unrelated to the\n * card) calls on its OWN bubble — cannot swallow it (FUT-2776 adversarial\n * review: a bubble-phase listener here was tried and reverted; see below).\n * It only observes: no `preventDefault`, so the tap or click still reaches\n * whatever it landed on. Attached while open, removed on close and on\n * unmount.\n *\n * That capture placement is exactly what makes the decision impossible to\n * make SYNCHRONOUSLY: `document` is the outermost node on the capture path,\n * so this handler fires before React's own capture dispatch even starts —\n * before `useCardOwnership`'s marker on the card's content root (also\n * capture, but on a node further down the same tree) has had a chance to run\n * for this same event. So the decision is deferred — but NOT to a microtask:\n * a microtask here reads the marker before it is set for TRUSTED input (a\n * real press, or Playwright's `page.mouse`), which is the very bug this hook\n * exists to fix (FUT-2776, third-review adversarial probe, measured with\n * real Chromium and `page.mouse.down()`):\n *\n * TRUSTED: doc-capture-sync → doc-capture-microtask (marker unset) → inner-capture (sets marker)\n * SCRIPT: doc-capture-sync → inner-capture (sets marker) → doc-capture-microtask (marker set)\n *\n * Chromium runs any microtasks queued by a capture-phase listener BEFORE\n * dispatching to the next listener on the path when the event is trusted —\n * so a microtask queued here runs before `useCardOwnership`'s capture\n * listener further down the tree ever gets a turn, and the ownership check\n * always reads empty. A script-dispatched event (`fireEvent`, `userEvent` in\n * jsdom, or in a Storybook `play` function) instead completes its WHOLE\n * capture-then-bubble dispatch before any microtask runs — which is why\n * every test under the microtask version passed and the bug still shipped.\n * A macrotask (`setTimeout(0)`) is not part of the event dispatch under\n * EITHER kind of input, so it always runs after the whole dispatch — marker\n * included — regardless of trusted vs. script. That ordering is what this\n * hook now relies on.\n *\n * `sessionRef` replaces a plain boolean: it is a fresh object assigned each\n * time the card opens (this effect's mount), so a `setTimeout` callback\n * queued for a press during one open session can tell whether it is still\n * that SAME session by comparing against the ref, not just whether the card\n * happens to be open again — the card closing and reopening while a stale\n * timeout is still pending must not let that timeout act on the new session.\n * Every scheduled timeout is also tracked so a close or unmount can cancel\n * whatever is still pending, rather than letting it fire against a card that\n * is no longer there to close.\n */\nconst useClickAway = (\n active: boolean,\n inside: () => ReadonlyArray<Element | null>,\n ownsEvent: (event: Event) => boolean,\n onAway: () => void,\n) => {\n const sessionRef = React.useRef<object | null>(null);\n const pendingRef = React.useRef<Set<number>>(new Set());\n\n React.useEffect(() => {\n if (!active) return undefined;\n\n const session = {};\n sessionRef.current = session;\n\n const handlePointerDown = (event: globalThis.PointerEvent) => {\n const timeoutId = window.setTimeout(() => {\n pendingRef.current.delete(timeoutId);\n // Stale: this session closed (or a newer one opened) before this\n // press's decision could run.\n if (sessionRef.current !== session) return;\n\n const target = event.target as Node | null;\n const ownedByReactTree = ownsEvent(event);\n const ownedByDom = Boolean(target) && inside().some((el) => el?.contains(target));\n if (ownedByReactTree || ownedByDom) return;\n onAway();\n }, 0);\n pendingRef.current.add(timeoutId);\n };\n\n document.addEventListener('pointerdown', handlePointerDown, true);\n return () => {\n sessionRef.current = null;\n pendingRef.current.forEach((id) => window.clearTimeout(id));\n pendingRef.current.clear();\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [active, inside, ownsEvent, onAway]);\n};\n\n/**\n * Whether the pointerdown `useClickAway` is about to look at started inside\n * the card's OWN React tree — a MUI `Select`'s menu, a nested `Popover`, a\n * `DropdownMenu` the card's `content` renders — even when that control's DOM\n * node is not a descendant of the card's box at all, because it portalled\n * itself out near `document.body` (FUT-2776).\n *\n * React re-parents a portal's events to bubble AND capture through its OWNING\n * React tree, not the DOM tree it renders into (see the React docs on\n * `createPortal`), so an `onPointerDownCapture` on the card's own content root\n * still fires for a press inside a portal the card's content renders, and\n * NEVER fires for a press inside an unrelated one mounted by something that\n * is not a descendant of the card in the REACT tree — ownership, not timing.\n * That is the whole fix: the ORIGINAL approach (a `MutationObserver` on\n * `document.body`) treated ANY element appended to the body while the card\n * was open as inside, so an unrelated Snackbar, dev overlay or Popover that\n * happened to mount at the same time stopped closing the card too.\n *\n * Tracked per NATIVE EVENT OBJECT in a `WeakSet`, not a boolean flag: a\n * boolean stayed `true` forever once a press set it whose NATIVE propagation\n * then got stopped before reaching `useClickAway`'s own (bubble-phase, in\n * that earlier revision) listener — common for anything that treats the\n * press as its own gesture — so the marker never got consumed, and the\n * NEXT, genuinely outside press read a STALE `true` and stayed open\n * (FUT-2776, caught on adversarial review of the first fix here). Keying on\n * the event itself instead needs no consuming step and cannot go stale: a\n * `WeakSet` entry only ever answers for the ONE dispatch that created it,\n * and nothing outside this closure keeps a reference to a past pointerdown\n * once `useClickAway`'s deferred (`setTimeout(0)`, not a microtask — see that\n * hook for why) check for it has run, so the entry is simply garbage from\n * then on.\n */\nconst useCardOwnership = () => {\n const ownedEventsRef = React.useRef<WeakSet<Event>>(new WeakSet());\n\n const markInside = React.useCallback((event: React.PointerEvent<HTMLElement>) => {\n ownedEventsRef.current.add(event.nativeEvent);\n }, []);\n\n const ownsEvent = React.useCallback(\n (event: Event) => ownedEventsRef.current.has(event),\n [],\n );\n\n return { markInside, ownsEvent };\n};\n\n/** The trigger and the card's own box — a plain DOM containment check. */\nconst useInsideCard = (\n anchorEl: HTMLElement | null,\n contentRef: React.MutableRefObject<HTMLDivElement | null>,\n) =>\n React.useCallback(\n () => [anchorEl, contentRef.current],\n [anchorEl, contentRef],\n );\n\n/** Whatever is still pending when the card unmounts is cancelled with it. */\nconst useClearOnUnmount = (...refs: TimerRef[]) => {\n React.useEffect(\n () => () => refs.forEach(clearPending),\n // The refs are stable objects: a mount/unmount pair, as before.\n [],\n );\n};\n\ninterface HoverCardTimingInput {\n disabled: boolean;\n touchEnabled: boolean;\n enterDelay: number;\n exitDelay: number;\n onOpen?: () => void;\n onClose?: () => void;\n}\n\ntype VisibilityInput = Omit<HoverCardTimingInput, 'touchEnabled'>;\n\n/**\n * The open/close state and its two timers, split out of `useHoverCard` so\n * that composing it with click-away, escape and ownership reads as one flat\n * list rather than one long function.\n *\n * The enter delay before opening and the exit delay before closing (so the\n * pointer can travel from the trigger onto the card without dismissing it)\n * each cancel the other's pending work, which is why they live together.\n */\nconst useCardVisibility = ({ disabled, enterDelay, exitDelay, onOpen, onClose }: VisibilityInput) => {\n const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);\n const [isOpen, setIsOpen] = React.useState(false);\n const enterTimeoutRef = React.useRef<number | undefined>(undefined);\n const exitTimeoutRef = React.useRef<number | undefined>(undefined);\n\n const openCard = React.useCallback(\n (trigger: HTMLElement) => {\n if (disabled) return;\n\n clearPending(exitTimeoutRef);\n // A second enter before the delay ran out (a click and then a hover, or\n // the pointer jittering across the edge) must REPLACE the pending open,\n // not add one: an overwritten timer can no longer be cancelled, and it\n // reopened the card after Escape had closed it.\n clearPending(enterTimeoutRef);\n\n enterTimeoutRef.current = window.setTimeout(() => {\n setAnchorEl(trigger);\n setIsOpen(true);\n onOpen?.();\n }, enterDelay);\n },\n [disabled, enterDelay, onOpen],\n );\n\n const closeCard = React.useCallback(() => {\n clearPending(enterTimeoutRef);\n clearPending(exitTimeoutRef);\n\n exitTimeoutRef.current = window.setTimeout(() => {\n setIsOpen(false);\n setAnchorEl(null);\n onClose?.();\n }, exitDelay);\n }, [exitDelay, onClose]);\n\n // Escape and the popover's own close are FINAL: nothing still pending may\n // reopen the card behind them. It opens again on the next enter.\n const handleClose = React.useCallback(() => {\n clearPending(enterTimeoutRef);\n clearPending(exitTimeoutRef);\n setIsOpen(false);\n setAnchorEl(null);\n onClose?.();\n }, [onClose]);\n\n return { anchorEl, isOpen, openCard, closeCard, handleClose, enterTimeoutRef, exitTimeoutRef };\n};\n\nexport const useHoverCard = ({\n disabled,\n touchEnabled,\n enterDelay,\n exitDelay,\n onOpen,\n onClose,\n}: HoverCardTimingInput) => {\n const [isTouchDevice, setIsTouchDevice] = React.useState(false);\n /** The card's own box, inside the popover paper: a press here is not \"away\". */\n const contentRef = React.useRef<HTMLDivElement | null>(null);\n\n const { anchorEl, isOpen, openCard, closeCard, handleClose, enterTimeoutRef, exitTimeoutRef } =\n useCardVisibility({ disabled, enterDelay, exitDelay, onOpen, onClose });\n\n // Probed in an effect, not during render, so SSR and hydration stay intact.\n React.useEffect(() => {\n setIsTouchDevice('ontouchstart' in window);\n }, []);\n\n const { triggerHandlers, cardHandlers } = useHoverHandlers({\n isTouchDevice,\n touchEnabled,\n enterDelay,\n openCard,\n closeCard,\n exitTimeoutRef,\n });\n\n const insideCard = useInsideCard(anchorEl, contentRef);\n const { markInside, ownsEvent } = useCardOwnership();\n\n useEscapeKey(isOpen, handleClose);\n useClickAway(isOpen, insideCard, ownsEvent, handleClose);\n useClearOnUnmount(enterTimeoutRef, exitTimeoutRef);\n\n return {\n anchorEl,\n isOpen,\n handleClose,\n triggerHandlers,\n cardHandlers,\n contentRef,\n /** Wire onto the card's content root as `onPointerDownCapture` (FUT-2776). */\n onContentPointerDownCapture: markInside,\n };\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import "../chunk-6OS4AKWT.js";
|
|
2
2
|
import {
|
|
3
3
|
HoverCard
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-KN3WYEH2.js";
|
|
5
5
|
import "../chunk-NRIV2CQ2.js";
|
|
6
6
|
import "../chunk-BSYPPO2I.js";
|
|
7
7
|
import "../chunk-ZHV7TCAC.js";
|
|
8
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-Z3USLQQT.js";
|
|
9
9
|
import "../chunk-HPK33ACY.js";
|
|
10
10
|
import "../chunk-FQ6UW7IY.js";
|
|
11
11
|
import "../chunk-6JAIFIPW.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HoverCard.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/HoverCard/HoverCard.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAS1B,OAAO,KAAK,EAA0C,cAAc,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"HoverCard.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/HoverCard/HoverCard.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAS1B,OAAO,KAAK,EAA0C,cAAc,EAAE,MAAM,mBAAmB,CAAC;AA6FhG,eAAO,MAAM,SAAS,oGA6EpB,CAAC"}
|
|
@@ -7,14 +7,6 @@ interface HoverCardTimingInput {
|
|
|
7
7
|
onOpen?: () => void;
|
|
8
8
|
onClose?: () => void;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* When the card is open, and the delays that decide it.
|
|
12
|
-
*
|
|
13
|
-
* Three timers are in play: the enter delay before opening, the exit delay
|
|
14
|
-
* before closing (so the pointer can travel from the trigger onto the card
|
|
15
|
-
* without dismissing it), and the long-press timer on touch. Each cancels the
|
|
16
|
-
* others' pending work, which is why they live together.
|
|
17
|
-
*/
|
|
18
10
|
export declare const useHoverCard: ({ disabled, touchEnabled, enterDelay, exitDelay, onOpen, onClose, }: HoverCardTimingInput) => {
|
|
19
11
|
anchorEl: HTMLElement | null;
|
|
20
12
|
isOpen: boolean;
|
|
@@ -31,6 +23,8 @@ export declare const useHoverCard: ({ disabled, touchEnabled, enterDelay, exitDe
|
|
|
31
23
|
onMouseLeave: () => void;
|
|
32
24
|
};
|
|
33
25
|
contentRef: React.RefObject<HTMLDivElement | null>;
|
|
26
|
+
/** Wire onto the card's content root as `onPointerDownCapture` (FUT-2776). */
|
|
27
|
+
onContentPointerDownCapture: (event: React.PointerEvent<HTMLElement>) => void;
|
|
34
28
|
};
|
|
35
29
|
export {};
|
|
36
30
|
//# sourceMappingURL=HoverCard.hooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HoverCard.hooks.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/HoverCard/HoverCard.hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"HoverCard.hooks.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/HoverCard/HoverCard.hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAkQ1B,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AA+DD,eAAO,MAAM,YAAY,GAAI,qEAO1B,oBAAoB;;;;;8BAvRG,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC;;8BAQ7B,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC;;;;;;4BA7CpC,IAAI;;;IAgWnB,8EAA8E;yCAhJnC,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC;CAmJ7E,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Lightbox.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/Lightbox/Lightbox.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Lightbox.d.ts","sourceRoot":"","sources":["../../../../../src/components/data-display/Lightbox/Lightbox.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAiCnE,eAAO,MAAM,QAAQ,mFA+EnB,CAAC;AAIH,eAAe,QAAQ,CAAC"}
|
package/package.json
CHANGED
|
@@ -105,29 +105,148 @@ const useEscapeKey = (active: boolean, onEscape: () => void) => {
|
|
|
105
105
|
* backdrop, which `pointer-events: none` on the popover root (so the page
|
|
106
106
|
* under an open card stays usable) takes out of play. So this listens on the
|
|
107
107
|
* document itself, in the CAPTURE phase, where a `stopPropagation` further in
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* — including one a nested control (or something entirely unrelated to the
|
|
109
|
+
* card) calls on its OWN bubble — cannot swallow it (FUT-2776 adversarial
|
|
110
|
+
* review: a bubble-phase listener here was tried and reverted; see below).
|
|
111
|
+
* It only observes: no `preventDefault`, so the tap or click still reaches
|
|
112
|
+
* whatever it landed on. Attached while open, removed on close and on
|
|
113
|
+
* unmount.
|
|
114
|
+
*
|
|
115
|
+
* That capture placement is exactly what makes the decision impossible to
|
|
116
|
+
* make SYNCHRONOUSLY: `document` is the outermost node on the capture path,
|
|
117
|
+
* so this handler fires before React's own capture dispatch even starts —
|
|
118
|
+
* before `useCardOwnership`'s marker on the card's content root (also
|
|
119
|
+
* capture, but on a node further down the same tree) has had a chance to run
|
|
120
|
+
* for this same event. So the decision is deferred — but NOT to a microtask:
|
|
121
|
+
* a microtask here reads the marker before it is set for TRUSTED input (a
|
|
122
|
+
* real press, or Playwright's `page.mouse`), which is the very bug this hook
|
|
123
|
+
* exists to fix (FUT-2776, third-review adversarial probe, measured with
|
|
124
|
+
* real Chromium and `page.mouse.down()`):
|
|
125
|
+
*
|
|
126
|
+
* TRUSTED: doc-capture-sync → doc-capture-microtask (marker unset) → inner-capture (sets marker)
|
|
127
|
+
* SCRIPT: doc-capture-sync → inner-capture (sets marker) → doc-capture-microtask (marker set)
|
|
128
|
+
*
|
|
129
|
+
* Chromium runs any microtasks queued by a capture-phase listener BEFORE
|
|
130
|
+
* dispatching to the next listener on the path when the event is trusted —
|
|
131
|
+
* so a microtask queued here runs before `useCardOwnership`'s capture
|
|
132
|
+
* listener further down the tree ever gets a turn, and the ownership check
|
|
133
|
+
* always reads empty. A script-dispatched event (`fireEvent`, `userEvent` in
|
|
134
|
+
* jsdom, or in a Storybook `play` function) instead completes its WHOLE
|
|
135
|
+
* capture-then-bubble dispatch before any microtask runs — which is why
|
|
136
|
+
* every test under the microtask version passed and the bug still shipped.
|
|
137
|
+
* A macrotask (`setTimeout(0)`) is not part of the event dispatch under
|
|
138
|
+
* EITHER kind of input, so it always runs after the whole dispatch — marker
|
|
139
|
+
* included — regardless of trusted vs. script. That ordering is what this
|
|
140
|
+
* hook now relies on.
|
|
141
|
+
*
|
|
142
|
+
* `sessionRef` replaces a plain boolean: it is a fresh object assigned each
|
|
143
|
+
* time the card opens (this effect's mount), so a `setTimeout` callback
|
|
144
|
+
* queued for a press during one open session can tell whether it is still
|
|
145
|
+
* that SAME session by comparing against the ref, not just whether the card
|
|
146
|
+
* happens to be open again — the card closing and reopening while a stale
|
|
147
|
+
* timeout is still pending must not let that timeout act on the new session.
|
|
148
|
+
* Every scheduled timeout is also tracked so a close or unmount can cancel
|
|
149
|
+
* whatever is still pending, rather than letting it fire against a card that
|
|
150
|
+
* is no longer there to close.
|
|
111
151
|
*/
|
|
112
152
|
const useClickAway = (
|
|
113
153
|
active: boolean,
|
|
114
154
|
inside: () => ReadonlyArray<Element | null>,
|
|
155
|
+
ownsEvent: (event: Event) => boolean,
|
|
115
156
|
onAway: () => void,
|
|
116
157
|
) => {
|
|
158
|
+
const sessionRef = React.useRef<object | null>(null);
|
|
159
|
+
const pendingRef = React.useRef<Set<number>>(new Set());
|
|
160
|
+
|
|
117
161
|
React.useEffect(() => {
|
|
118
|
-
if (!active) return;
|
|
162
|
+
if (!active) return undefined;
|
|
163
|
+
|
|
164
|
+
const session = {};
|
|
165
|
+
sessionRef.current = session;
|
|
119
166
|
|
|
120
167
|
const handlePointerDown = (event: globalThis.PointerEvent) => {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
const timeoutId = window.setTimeout(() => {
|
|
169
|
+
pendingRef.current.delete(timeoutId);
|
|
170
|
+
// Stale: this session closed (or a newer one opened) before this
|
|
171
|
+
// press's decision could run.
|
|
172
|
+
if (sessionRef.current !== session) return;
|
|
173
|
+
|
|
174
|
+
const target = event.target as Node | null;
|
|
175
|
+
const ownedByReactTree = ownsEvent(event);
|
|
176
|
+
const ownedByDom = Boolean(target) && inside().some((el) => el?.contains(target));
|
|
177
|
+
if (ownedByReactTree || ownedByDom) return;
|
|
178
|
+
onAway();
|
|
179
|
+
}, 0);
|
|
180
|
+
pendingRef.current.add(timeoutId);
|
|
124
181
|
};
|
|
125
182
|
|
|
126
183
|
document.addEventListener('pointerdown', handlePointerDown, true);
|
|
127
|
-
return () =>
|
|
128
|
-
|
|
184
|
+
return () => {
|
|
185
|
+
sessionRef.current = null;
|
|
186
|
+
pendingRef.current.forEach((id) => window.clearTimeout(id));
|
|
187
|
+
pendingRef.current.clear();
|
|
188
|
+
document.removeEventListener('pointerdown', handlePointerDown, true);
|
|
189
|
+
};
|
|
190
|
+
}, [active, inside, ownsEvent, onAway]);
|
|
129
191
|
};
|
|
130
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Whether the pointerdown `useClickAway` is about to look at started inside
|
|
195
|
+
* the card's OWN React tree — a MUI `Select`'s menu, a nested `Popover`, a
|
|
196
|
+
* `DropdownMenu` the card's `content` renders — even when that control's DOM
|
|
197
|
+
* node is not a descendant of the card's box at all, because it portalled
|
|
198
|
+
* itself out near `document.body` (FUT-2776).
|
|
199
|
+
*
|
|
200
|
+
* React re-parents a portal's events to bubble AND capture through its OWNING
|
|
201
|
+
* React tree, not the DOM tree it renders into (see the React docs on
|
|
202
|
+
* `createPortal`), so an `onPointerDownCapture` on the card's own content root
|
|
203
|
+
* still fires for a press inside a portal the card's content renders, and
|
|
204
|
+
* NEVER fires for a press inside an unrelated one mounted by something that
|
|
205
|
+
* is not a descendant of the card in the REACT tree — ownership, not timing.
|
|
206
|
+
* That is the whole fix: the ORIGINAL approach (a `MutationObserver` on
|
|
207
|
+
* `document.body`) treated ANY element appended to the body while the card
|
|
208
|
+
* was open as inside, so an unrelated Snackbar, dev overlay or Popover that
|
|
209
|
+
* happened to mount at the same time stopped closing the card too.
|
|
210
|
+
*
|
|
211
|
+
* Tracked per NATIVE EVENT OBJECT in a `WeakSet`, not a boolean flag: a
|
|
212
|
+
* boolean stayed `true` forever once a press set it whose NATIVE propagation
|
|
213
|
+
* then got stopped before reaching `useClickAway`'s own (bubble-phase, in
|
|
214
|
+
* that earlier revision) listener — common for anything that treats the
|
|
215
|
+
* press as its own gesture — so the marker never got consumed, and the
|
|
216
|
+
* NEXT, genuinely outside press read a STALE `true` and stayed open
|
|
217
|
+
* (FUT-2776, caught on adversarial review of the first fix here). Keying on
|
|
218
|
+
* the event itself instead needs no consuming step and cannot go stale: a
|
|
219
|
+
* `WeakSet` entry only ever answers for the ONE dispatch that created it,
|
|
220
|
+
* and nothing outside this closure keeps a reference to a past pointerdown
|
|
221
|
+
* once `useClickAway`'s deferred (`setTimeout(0)`, not a microtask — see that
|
|
222
|
+
* hook for why) check for it has run, so the entry is simply garbage from
|
|
223
|
+
* then on.
|
|
224
|
+
*/
|
|
225
|
+
const useCardOwnership = () => {
|
|
226
|
+
const ownedEventsRef = React.useRef<WeakSet<Event>>(new WeakSet());
|
|
227
|
+
|
|
228
|
+
const markInside = React.useCallback((event: React.PointerEvent<HTMLElement>) => {
|
|
229
|
+
ownedEventsRef.current.add(event.nativeEvent);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
const ownsEvent = React.useCallback(
|
|
233
|
+
(event: Event) => ownedEventsRef.current.has(event),
|
|
234
|
+
[],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return { markInside, ownsEvent };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** The trigger and the card's own box — a plain DOM containment check. */
|
|
241
|
+
const useInsideCard = (
|
|
242
|
+
anchorEl: HTMLElement | null,
|
|
243
|
+
contentRef: React.MutableRefObject<HTMLDivElement | null>,
|
|
244
|
+
) =>
|
|
245
|
+
React.useCallback(
|
|
246
|
+
() => [anchorEl, contentRef.current],
|
|
247
|
+
[anchorEl, contentRef],
|
|
248
|
+
);
|
|
249
|
+
|
|
131
250
|
/** Whatever is still pending when the card unmounts is cancelled with it. */
|
|
132
251
|
const useClearOnUnmount = (...refs: TimerRef[]) => {
|
|
133
252
|
React.useEffect(
|
|
@@ -146,34 +265,22 @@ interface HoverCardTimingInput {
|
|
|
146
265
|
onClose?: () => void;
|
|
147
266
|
}
|
|
148
267
|
|
|
268
|
+
type VisibilityInput = Omit<HoverCardTimingInput, 'touchEnabled'>;
|
|
269
|
+
|
|
149
270
|
/**
|
|
150
|
-
*
|
|
271
|
+
* The open/close state and its two timers, split out of `useHoverCard` so
|
|
272
|
+
* that composing it with click-away, escape and ownership reads as one flat
|
|
273
|
+
* list rather than one long function.
|
|
151
274
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* others' pending work, which is why they live together.
|
|
275
|
+
* The enter delay before opening and the exit delay before closing (so the
|
|
276
|
+
* pointer can travel from the trigger onto the card without dismissing it)
|
|
277
|
+
* each cancel the other's pending work, which is why they live together.
|
|
156
278
|
*/
|
|
157
|
-
|
|
158
|
-
disabled,
|
|
159
|
-
touchEnabled,
|
|
160
|
-
enterDelay,
|
|
161
|
-
exitDelay,
|
|
162
|
-
onOpen,
|
|
163
|
-
onClose,
|
|
164
|
-
}: HoverCardTimingInput) => {
|
|
279
|
+
const useCardVisibility = ({ disabled, enterDelay, exitDelay, onOpen, onClose }: VisibilityInput) => {
|
|
165
280
|
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
|
|
166
281
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
167
|
-
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
|
|
168
282
|
const enterTimeoutRef = React.useRef<number | undefined>(undefined);
|
|
169
283
|
const exitTimeoutRef = React.useRef<number | undefined>(undefined);
|
|
170
|
-
/** The card's own box, inside the popover paper: a press here is not "away". */
|
|
171
|
-
const contentRef = React.useRef<HTMLDivElement | null>(null);
|
|
172
|
-
|
|
173
|
-
// Probed in an effect, not during render, so SSR and hydration stay intact.
|
|
174
|
-
React.useEffect(() => {
|
|
175
|
-
setIsTouchDevice('ontouchstart' in window);
|
|
176
|
-
}, []);
|
|
177
284
|
|
|
178
285
|
const openCard = React.useCallback(
|
|
179
286
|
(trigger: HTMLElement) => {
|
|
@@ -216,6 +323,29 @@ export const useHoverCard = ({
|
|
|
216
323
|
onClose?.();
|
|
217
324
|
}, [onClose]);
|
|
218
325
|
|
|
326
|
+
return { anchorEl, isOpen, openCard, closeCard, handleClose, enterTimeoutRef, exitTimeoutRef };
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const useHoverCard = ({
|
|
330
|
+
disabled,
|
|
331
|
+
touchEnabled,
|
|
332
|
+
enterDelay,
|
|
333
|
+
exitDelay,
|
|
334
|
+
onOpen,
|
|
335
|
+
onClose,
|
|
336
|
+
}: HoverCardTimingInput) => {
|
|
337
|
+
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
|
|
338
|
+
/** The card's own box, inside the popover paper: a press here is not "away". */
|
|
339
|
+
const contentRef = React.useRef<HTMLDivElement | null>(null);
|
|
340
|
+
|
|
341
|
+
const { anchorEl, isOpen, openCard, closeCard, handleClose, enterTimeoutRef, exitTimeoutRef } =
|
|
342
|
+
useCardVisibility({ disabled, enterDelay, exitDelay, onOpen, onClose });
|
|
343
|
+
|
|
344
|
+
// Probed in an effect, not during render, so SSR and hydration stay intact.
|
|
345
|
+
React.useEffect(() => {
|
|
346
|
+
setIsTouchDevice('ontouchstart' in window);
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
219
349
|
const { triggerHandlers, cardHandlers } = useHoverHandlers({
|
|
220
350
|
isTouchDevice,
|
|
221
351
|
touchEnabled,
|
|
@@ -225,11 +355,21 @@ export const useHoverCard = ({
|
|
|
225
355
|
exitTimeoutRef,
|
|
226
356
|
});
|
|
227
357
|
|
|
228
|
-
const insideCard =
|
|
358
|
+
const insideCard = useInsideCard(anchorEl, contentRef);
|
|
359
|
+
const { markInside, ownsEvent } = useCardOwnership();
|
|
229
360
|
|
|
230
361
|
useEscapeKey(isOpen, handleClose);
|
|
231
|
-
useClickAway(isOpen, insideCard, handleClose);
|
|
362
|
+
useClickAway(isOpen, insideCard, ownsEvent, handleClose);
|
|
232
363
|
useClearOnUnmount(enterTimeoutRef, exitTimeoutRef);
|
|
233
364
|
|
|
234
|
-
return {
|
|
365
|
+
return {
|
|
366
|
+
anchorEl,
|
|
367
|
+
isOpen,
|
|
368
|
+
handleClose,
|
|
369
|
+
triggerHandlers,
|
|
370
|
+
cardHandlers,
|
|
371
|
+
contentRef,
|
|
372
|
+
/** Wire onto the card's content root as `onPointerDownCapture` (FUT-2776). */
|
|
373
|
+
onContentPointerDownCapture: markInside,
|
|
374
|
+
};
|
|
235
375
|
};
|
|
@@ -108,6 +108,7 @@ The HoverCard component displays additional information in a card-style popup wh
|
|
|
108
108
|
|
|
109
109
|
- The component supports keyboard navigation (Escape key to close)
|
|
110
110
|
- The card is dismissed by moving the pointer off the trigger and the card (after `exitDelay`), by Escape, or by a tap or click anywhere outside them. The outside press still reaches what it landed on: the page under an open card stays usable
|
|
111
|
+
- Nested portal content is supported: if `children` opens something of its own in a portal — a MUI `Select`'s menu, a nested `Popover`, a `DropdownMenu` — a press inside it counts as inside the card too, so picking an option there does not dismiss the card. No extra wiring is needed for this; it works for anything the card's OWN content renders, portalled or not — ownership is by React tree membership, not by when something happened to mount to `document.body`, so an unrelated portal (a Snackbar, a dev overlay, another component's Popover) that mounts while the card is open is not treated as inside it, and a press there still closes the card like any other outside press
|
|
111
112
|
- Proper ARIA attributes are applied to the popover
|
|
112
113
|
- Focus management is handled automatically
|
|
113
114
|
- Touch support for mobile devices can be enabled/disabled
|
|
@@ -82,6 +82,29 @@ const capOf = (theme: Theme, maxWidth: number | undefined): string => {
|
|
|
82
82
|
return cap <= 1 && cap !== 0 ? `${cap * 100}%` : rem(theme, cap);
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
/** The clone-onto-`trigger` vs. wrap-in-a-`span` choice, pulled out of the component so its body stays readable. */
|
|
86
|
+
const buildTriggerElement = (
|
|
87
|
+
trigger: HoverCardProps['trigger'],
|
|
88
|
+
triggerHandlers: ReturnType<typeof useHoverCard>['triggerHandlers'],
|
|
89
|
+
disabled: boolean,
|
|
90
|
+
dataTestId: string | undefined,
|
|
91
|
+
children: React.ReactNode,
|
|
92
|
+
) => {
|
|
93
|
+
const triggerProps = {
|
|
94
|
+
...triggerHandlers,
|
|
95
|
+
style: { cursor: disabled ? 'default' : 'pointer' },
|
|
96
|
+
'data-testid': dataTestId ? `${dataTestId}-trigger` : 'hover-card-trigger',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return trigger ? (
|
|
100
|
+
React.cloneElement(trigger, triggerProps)
|
|
101
|
+
) : (
|
|
102
|
+
<span {...triggerProps} style={{ display: 'inline-block', ...triggerProps.style }}>
|
|
103
|
+
{children}
|
|
104
|
+
</span>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
85
108
|
export const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props, ref) => {
|
|
86
109
|
const {
|
|
87
110
|
variant, glow, pulse, title, description, avatar, trigger, placement, showArrow,
|
|
@@ -90,7 +113,15 @@ export const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props
|
|
|
90
113
|
...rest
|
|
91
114
|
} = withDefaults(props, DEFAULTS) as ResolvedProps;
|
|
92
115
|
|
|
93
|
-
const {
|
|
116
|
+
const {
|
|
117
|
+
anchorEl,
|
|
118
|
+
isOpen,
|
|
119
|
+
handleClose,
|
|
120
|
+
triggerHandlers,
|
|
121
|
+
cardHandlers,
|
|
122
|
+
contentRef,
|
|
123
|
+
onContentPointerDownCapture,
|
|
124
|
+
} = useHoverCard({
|
|
94
125
|
disabled,
|
|
95
126
|
touchEnabled,
|
|
96
127
|
enterDelay,
|
|
@@ -99,20 +130,7 @@ export const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props
|
|
|
99
130
|
onClose,
|
|
100
131
|
});
|
|
101
132
|
|
|
102
|
-
const
|
|
103
|
-
...triggerHandlers,
|
|
104
|
-
style: { cursor: disabled ? 'default' : 'pointer' },
|
|
105
|
-
'data-testid': dataTestId ? `${dataTestId}-trigger` : 'hover-card-trigger',
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const triggerElement = trigger ? (
|
|
109
|
-
React.cloneElement(trigger, triggerProps)
|
|
110
|
-
) : (
|
|
111
|
-
<span {...triggerProps} style={{ display: 'inline-block', ...triggerProps.style }}>
|
|
112
|
-
{children}
|
|
113
|
-
</span>
|
|
114
|
-
);
|
|
115
|
-
|
|
133
|
+
const triggerElement = buildTriggerElement(trigger, triggerHandlers, disabled, dataTestId, children);
|
|
116
134
|
const padding = CONTENT_PADDING[variant] ?? 2;
|
|
117
135
|
|
|
118
136
|
return (
|
|
@@ -133,7 +151,11 @@ export const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props
|
|
|
133
151
|
slotProps={{ paper: { style: { pointerEvents: 'auto' } } }}
|
|
134
152
|
{...rest}
|
|
135
153
|
>
|
|
136
|
-
<Box
|
|
154
|
+
<Box
|
|
155
|
+
ref={contentRef}
|
|
156
|
+
onPointerDownCapture={onContentPointerDownCapture}
|
|
157
|
+
sx={{ position: 'relative' }}
|
|
158
|
+
>
|
|
137
159
|
{showArrow && <ArrowContainer placement={placement} offset={offset} />}
|
|
138
160
|
<StyledCard
|
|
139
161
|
customVariant={variant}
|
|
@@ -29,8 +29,14 @@ const DIALOG_STATIC_PROPS = {
|
|
|
29
29
|
// second, non-modal `role="dialog"` this component sets of its own accord
|
|
30
30
|
// — duplicating (and, as a static English literal, contradicting) the name
|
|
31
31
|
// the Paper already carries.
|
|
32
|
+
//
|
|
33
|
+
// For the same reason, `role` is deliberately NOT set here (FUT-2861):
|
|
34
|
+
// `Dialog` destructures `aria-labelledby` and `aria-modal` by name and
|
|
35
|
+
// applies them to the Paper alongside its own hard-coded `role="dialog"`,
|
|
36
|
+
// but `role` itself is not one of the props it destructures — a `role`
|
|
37
|
+
// passed here falls into `...other` and lands a second, unlabelled
|
|
38
|
+
// `role="dialog"` on the outer Modal root instead of the Paper.
|
|
32
39
|
'aria-labelledby': 'lightbox-title',
|
|
33
|
-
role: 'dialog',
|
|
34
40
|
'aria-modal': 'true',
|
|
35
41
|
} as const;
|
|
36
42
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/Lightbox/Lightbox.tsx"],"sourcesContent":["import Dialog from '@mui/material/Dialog/index.js';\nimport Fade from '@mui/material/Fade/index.js';\nimport Typography from '@mui/material/Typography/index.js';\nimport type { Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { makeTestId } from './Lightbox.constants';\nimport { resolveLightboxProps } from './Lightbox.helpers';\nimport { LightboxCopyProvider } from './lightbox-copy-context';\nimport type { LightboxProps, LightboxRef } from './Lightbox.types';\nimport { LightboxCurrentMedia } from './LightboxCurrentMedia';\nimport { LightboxOverlay } from './LightboxOverlay';\nimport { LightboxStage } from './LightboxStage';\nimport { LightboxThumbnails } from './LightboxThumbnails';\nimport { useLightbox } from './useLightbox';\nimport { scrim } from '../../../tokens/ink';\nimport { rem, sxRem } from '../../../tokens/relative';\n\nconst DIALOG_STATIC_PROPS = {\n maxWidth: false,\n fullScreen: true,\n TransitionComponent: Fade,\n TransitionProps: { timeout: 300 },\n PaperProps: { sx: { background: (theme: Theme) => scrim(theme, 0.9), backdropFilter: (theme: Theme) => `blur(${rem(theme, 2)})` } },\n // `aria-labelledby` is the one accessible name for the modal itself: MUI\n // forwards it (with `aria-modal` and its own `role=\"dialog\"`) onto the\n // Paper, the element that is actually the WAI-ARIA dialog. A plain\n // `aria-label` alongside it would land on the OUTER root instead — a\n // second, non-modal `role=\"dialog\"` this component sets of its own accord\n // — duplicating (and, as a static English literal, contradicting) the name\n // the Paper already carries.\n 'aria-labelledby': 'lightbox-title',\n role: 'dialog',\n 'aria-modal': 'true',\n} as const;\n\nexport const Lightbox = React.forwardRef<LightboxRef, LightboxProps>((componentProps, ref) => {\n const props = resolveLightboxProps(componentProps);\n const { isOpen, items, loop, autoplay, showControls, showCaptions, thumbnails, zoomable } = props;\n const { className, style, dataTestId } = props;\n\n const testId = makeTestId(dataTestId);\n const lightbox = useLightbox(props, ref);\n const { currentIndex, currentItem, zoom } = lightbox;\n\n return (\n <LightboxCopyProvider copy={props.copy}>\n <Dialog\n open={isOpen}\n onClose={lightbox.close}\n {...DIALOG_STATIC_PROPS}\n className={className}\n style={style}\n onTouchStart={lightbox.handleTouchStart}\n onTouchEnd={lightbox.handleTouchEnd}\n data-testid={dataTestId || 'lightbox'}\n >\n {/* Visually hidden title for screen readers */}\n <Typography id=\"lightbox-title\" variant=\"h6\" sx={{ position: 'absolute', left: sxRem(-10000) }}>\n {props.copy.dialogLabel} - {currentItem?.alt || props.copy.itemPosition(currentIndex + 1, items.length)}\n </Typography>\n\n <LightboxOverlay\n testId={testId}\n itemCount={items.length}\n currentIndex={currentIndex}\n showControls={showControls}\n loop={loop}\n autoplay={autoplay}\n canZoom={lightbox.canZoom}\n isAutoPlaying={lightbox.isAutoPlaying}\n onClose={lightbox.close}\n onPrev={lightbox.prev}\n onNext={lightbox.next}\n onZoomIn={zoom.zoomIn}\n onZoomOut={zoom.zoomOut}\n onResetZoom={zoom.resetZoom}\n onToggleAutoplay={() => lightbox.setIsAutoPlaying((playing) => !playing)}\n />\n\n <LightboxStage\n items={items}\n currentItem={currentItem}\n currentIndex={currentIndex}\n isLoading={lightbox.isLoading}\n showCaptions={showCaptions}\n thumbnails={thumbnails}\n testId={testId}\n media={\n <LightboxCurrentMedia\n item={currentItem}\n index={currentIndex}\n total={items.length}\n zoomable={zoomable}\n zoom={zoom}\n testId={testId}\n onLoadingChange={lightbox.setIsLoading}\n onWheel={lightbox.handleWheel}\n />\n }\n thumbnailStrip={\n thumbnails &&\n items.length > 1 && (\n <LightboxThumbnails\n items={items}\n currentIndex={currentIndex}\n testId={testId}\n onSelect={lightbox.goToIndex}\n />\n )\n }\n />\n </Dialog>\n </LightboxCopyProvider>\n );\n});\n\nLightbox.displayName = 'Lightbox';\n\nexport default Lightbox;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB,OAAO,gBAAgB;AAEvB,OAAO,WAAW;AAsDZ,SAIA,KAJA;AAxCN,IAAM,sBAAsB;AAAA,EAC1B,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,qBAAqB;AAAA,EACrB,iBAAiB,EAAE,SAAS,IAAI;AAAA,EAChC,YAAY,EAAE,IAAI,EAAE,YAAY,CAAC,UAAiB,MAAM,OAAO,GAAG,GAAG,gBAAgB,CAAC,UAAiB,QAAQ,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQlI,mBAAmB;AAAA,EACnB,MAAM;AAAA,EACN,cAAc;AAChB;AAEO,IAAM,WAAW,MAAM,WAAuC,CAAC,gBAAgB,QAAQ;AAC5F,QAAM,QAAQ,qBAAqB,cAAc;AACjD,QAAM,EAAE,QAAQ,OAAO,MAAM,UAAU,cAAc,cAAc,YAAY,SAAS,IAAI;AAC5F,QAAM,EAAE,WAAW,OAAO,WAAW,IAAI;AAEzC,QAAM,SAAS,WAAW,UAAU;AACpC,QAAM,WAAW,YAAY,OAAO,GAAG;AACvC,QAAM,EAAE,cAAc,aAAa,KAAK,IAAI;AAE5C,SACE,oBAAC,wBAAqB,MAAM,MAAM,MAClC;AAAA,IAAC;AAAA;AAAA,MACC,MAAM;AAAA,MACN,SAAS,SAAS;AAAA,MACjB,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA,cAAc,SAAS;AAAA,MACvB,YAAY,SAAS;AAAA,MACrB,eAAa,cAAc;AAAA,MAG3B;AAAA,6BAAC,cAAW,IAAG,kBAAiB,SAAQ,MAAK,IAAI,EAAE,UAAU,YAAY,MAAM,MAAM,IAAM,EAAE,GAC1F;AAAA,gBAAM,KAAK;AAAA,UAAY;AAAA,UAAI,aAAa,OAAO,MAAM,KAAK,aAAa,eAAe,GAAG,MAAM,MAAM;AAAA,WACxG;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,WAAW,MAAM;AAAA,YACjB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,SAAS,SAAS;AAAA,YAClB,eAAe,SAAS;AAAA,YACxB,SAAS,SAAS;AAAA,YAClB,QAAQ,SAAS;AAAA,YACjB,QAAQ,SAAS;AAAA,YACjB,UAAU,KAAK;AAAA,YACf,WAAW,KAAK;AAAA,YAChB,aAAa,KAAK;AAAA,YAClB,kBAAkB,MAAM,SAAS,iBAAiB,CAAC,YAAY,CAAC,OAAO;AAAA;AAAA,QACzE;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,WAAW,SAAS;AAAA,YACpB;AAAA,YACA;AAAA,YACA;AAAA,YACA,OACE;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN,OAAO;AAAA,gBACP,OAAO,MAAM;AAAA,gBACb;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,iBAAiB,SAAS;AAAA,gBAC1B,SAAS,SAAS;AAAA;AAAA,YACpB;AAAA,YAEF,gBACE,cACA,MAAM,SAAS,KACb;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA,UAAU,SAAS;AAAA;AAAA,YACrB;AAAA;AAAA,QAGN;AAAA;AAAA;AAAA,EACF,GACA;AAEJ,CAAC;AAED,SAAS,cAAc;AAEvB,IAAO,mBAAQ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/HoverCard/HoverCard.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport Card from '@mui/material/Card/index.js';\nimport CardContent from '@mui/material/CardContent/index.js';\nimport Popover from '@mui/material/Popover/index.js';\nimport { styled, type Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { rem } from '../../../tokens/relative';\nimport { withDefaults } from '../../../utils/withDefaults';\n\nimport { HoverCardContent } from './HoverCard.content';\nimport { useHoverCard } from './HoverCard.hooks';\nimport { arrowSx, cardSx, getAnchorOrigin, getTransformOrigin, sideOf } from './HoverCard.styles';\nimport type { CardStyleFlags } from './HoverCard.styles';\nimport type { HoverCardAnimation, HoverCardPlacement, HoverCardProps } from './HoverCard.types';\n\nconst ArrowContainer = styled('div')<{ placement: HoverCardPlacement; offset?: number }>(\n ({ theme, placement }) => ({ ...arrowSx(theme, placement) }),\n);\n\nconst StyledCard = styled(Card, {\n shouldForwardProp: (prop) =>\n !['customVariant', 'glow', 'pulse', 'animation'].includes(prop as string),\n})<CardStyleFlags>(({ theme, ...flags }) => ({ ...cardSx(theme, flags) }));\n\nconst StyledPopover = styled(Popover, {\n shouldForwardProp: (prop) => prop !== 'customAnimation',\n})<{ customAnimation?: HoverCardAnimation }>(({ customAnimation }) => ({\n // Only the card takes the pointer (its paper sets `pointer-events: auto`).\n // MUI's popover root and its invisible backdrop cover the whole viewport, so\n // while a card was open the pointer was \"on the card\" wherever it went: the\n // backdrop's mouseover reached the card's onMouseEnter and cancelled the\n // close the trigger's mouseleave had just scheduled. A card left by moving\n // the mouse away stayed open, and the page's next click hit the backdrop\n // instead of its target (FUT-2619). The backdrop was also the only\n // click-away; `useClickAway` in the hooks replaces it.\n pointerEvents: 'none',\n // The popover only positions; StyledCard draws the surface.\n '& .MuiPopover-paper': {\n backgroundColor: 'transparent',\n boxShadow: 'none',\n overflow: 'visible',\n ...(customAnimation === 'scale' && { transformOrigin: 'center' }),\n },\n}));\n\nconst DEFAULTS = {\n variant: 'default',\n glow: false,\n pulse: false,\n placement: 'bottom',\n showArrow: false,\n animation: 'fade',\n enterDelay: 700,\n exitDelay: 0,\n loading: false,\n touchEnabled: true,\n disabled: false,\n} satisfies Partial<HoverCardProps>;\n\ntype ResolvedProps = HoverCardProps & Required<Pick<HoverCardProps, keyof typeof DEFAULTS>>;\n\n/** Padding is the variant's own: detailed roomier, minimal tighter. */\nconst CONTENT_PADDING: Record<string, number> = { detailed: 3, minimal: 1.5 };\n\n/**\n * The card is nudged off the anchor on whichever side it sits — `offset` design\n * px (8 unless the caller says otherwise), through the theme's type scale.\n */\nconst offsetMargin = (theme: Theme, placement: HoverCardPlacement, offset: number | undefined) => {\n const side = sideOf(placement);\n const key = ({ top: 'mt', bottom: 'mb', left: 'ml', right: 'mr' } as const)[side];\n return { [key]: rem(theme, offset ?? 8) };\n};\n\n/**\n * The card's cap: 400 design px unless the caller says otherwise. `sx` has always\n * read a number of 1 or less as a fraction of the parent, and that stays so.\n */\nconst capOf = (theme: Theme, maxWidth: number | undefined): string => {\n const cap = maxWidth ?? 400;\n return cap <= 1 && cap !== 0 ? `${cap * 100}%` : rem(theme, cap);\n};\n\nexport const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props, ref) => {\n const {\n variant, glow, pulse, title, description, avatar, trigger, placement, showArrow,\n animation, enterDelay, exitDelay, maxWidth, loading, loadingComponent, loadingText, touchEnabled,\n offset, disabled, onOpen, onClose, children, dataTestId,\n ...rest\n } = withDefaults(props, DEFAULTS) as ResolvedProps;\n\n const { anchorEl, isOpen, handleClose, triggerHandlers, cardHandlers, contentRef } = useHoverCard({\n disabled,\n touchEnabled,\n enterDelay,\n exitDelay,\n onOpen,\n onClose,\n });\n\n const triggerProps = {\n ...triggerHandlers,\n style: { cursor: disabled ? 'default' : 'pointer' },\n 'data-testid': dataTestId ? `${dataTestId}-trigger` : 'hover-card-trigger',\n };\n\n const triggerElement = trigger ? (\n React.cloneElement(trigger, triggerProps)\n ) : (\n <span {...triggerProps} style={{ display: 'inline-block', ...triggerProps.style }}>\n {children}\n </span>\n );\n\n const padding = CONTENT_PADDING[variant] ?? 2;\n\n return (\n <>\n {triggerElement}\n <StyledPopover\n ref={ref}\n open={isOpen && !disabled}\n anchorEl={anchorEl}\n onClose={handleClose}\n anchorOrigin={getAnchorOrigin(placement)}\n transformOrigin={getTransformOrigin(placement)}\n {...cardHandlers}\n disableAutoFocus\n disableEnforceFocus\n disableRestoreFocus\n customAnimation={animation}\n slotProps={{ paper: { style: { pointerEvents: 'auto' } } }}\n {...rest}\n >\n <Box ref={contentRef} sx={{ position: 'relative' }}>\n {showArrow && <ArrowContainer placement={placement} offset={offset} />}\n <StyledCard\n customVariant={variant}\n glow={glow}\n pulse={pulse}\n animation={animation}\n data-testid={dataTestId || 'hover-card-content'}\n sx={(theme) => ({ maxWidth: capOf(theme, maxWidth), ...offsetMargin(theme, placement, offset) })}\n >\n <CardContent sx={{ p: padding, '&:last-child': { pb: padding } }}>\n <HoverCardContent\n loadingText={loadingText} variant={variant} title={title}\n description={description}\n avatar={avatar}\n loading={loading}\n loadingComponent={loadingComponent}\n dataTestId={dataTestId}\n >\n {children}\n </HoverCardContent>\n </CardContent>\n </StyledCard>\n </Box>\n </StyledPopover>\n </>\n );\n});\n\nHoverCard.displayName = 'HoverCard';\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/data-display/HoverCard/HoverCard.hooks.ts"],"sourcesContent":["import React from 'react';\n\ntype TimerRef = React.MutableRefObject<number | undefined>;\n\n/**\n * Cancel the timer a ref holds, and forget it. Every place that schedules\n * or dismisses goes through this, so no timer outlives the ref that could\n * cancel it (FUT-2619: an overwritten enter timer reopened a dismissed card).\n */\nconst clearPending = (ref: TimerRef) => {\n if (ref.current !== undefined) window.clearTimeout(ref.current);\n ref.current = undefined;\n};\n\ninterface HandlerInput {\n isTouchDevice: boolean;\n touchEnabled: boolean;\n enterDelay: number;\n openCard: (trigger: HTMLElement) => void;\n closeCard: () => void;\n exitTimeoutRef: TimerRef;\n}\n\n/**\n * The pointer and touch handlers, over the open/close pair. Touch gets a\n * long-press because there is no hover to respond to; the card's own handlers\n * keep it open while the pointer is travelling onto it.\n */\nconst useHoverHandlers = ({\n isTouchDevice,\n touchEnabled,\n enterDelay,\n openCard,\n closeCard,\n exitTimeoutRef,\n}: HandlerInput) => {\n const [touchTimeout, setTouchTimeout] = React.useState<number>();\n\n /** A touch device with touch disabled has no hover to respond to. */\n const hoverIgnored = isTouchDevice && !touchEnabled;\n\n const cancelTouch = React.useCallback(() => {\n if (touchTimeout) {\n window.clearTimeout(touchTimeout);\n setTouchTimeout(undefined);\n }\n }, [touchTimeout]);\n\n React.useEffect(\n () => () => {\n if (touchTimeout) window.clearTimeout(touchTimeout);\n },\n [touchTimeout],\n );\n\n const triggerHandlers = {\n onMouseEnter: (event: React.MouseEvent<HTMLElement>) => {\n if (hoverIgnored) return;\n openCard(event.currentTarget);\n },\n onMouseLeave: () => {\n if (hoverIgnored) return;\n closeCard();\n },\n onTouchStart: (event: React.TouchEvent<HTMLElement>) => {\n if (!touchEnabled) return;\n if (touchTimeout) window.clearTimeout(touchTimeout);\n // Read NOW: React nulls `currentTarget` once the handler returns, so\n // the long-press timer used to open the card with no anchor at all.\n const trigger = event.currentTarget;\n setTouchTimeout(window.setTimeout(() => openCard(trigger), enterDelay));\n },\n // End and cancel do the same thing: the press did not become a long press.\n onTouchEnd: cancelTouch,\n onTouchCancel: cancelTouch,\n };\n\n /** Moving onto the card keeps it open; leaving it starts the exit delay. */\n const cardHandlers = {\n onMouseEnter: () => clearPending(exitTimeoutRef),\n onMouseLeave: closeCard,\n };\n\n return { triggerHandlers, cardHandlers };\n};\n\n/** Escape dismisses the card while it is open. */\nconst useEscapeKey = (active: boolean, onEscape: () => void) => {\n React.useEffect(() => {\n if (!active) return;\n\n const handleEscape = (event: globalThis.KeyboardEvent) => {\n if (event.key === 'Escape') onEscape();\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [active, onEscape]);\n};\n\n/**\n * A press anywhere outside the trigger and the card dismisses it.\n *\n * Touch has no mouse-leave, and MUI's own click-away was the popover's\n * backdrop, which `pointer-events: none` on the popover root (so the page\n * under an open card stays usable) takes out of play. So this listens on the\n * document itself, in the CAPTURE phase, where a `stopPropagation` further in\n * cannot swallow it. It only observes: no `preventDefault`, so the tap or\n * click still reaches whatever it landed on. Attached while open, removed on\n * close and on unmount.\n */\nconst useClickAway = (\n active: boolean,\n inside: () => ReadonlyArray<Element | null>,\n onAway: () => void,\n) => {\n React.useEffect(() => {\n if (!active) return;\n\n const handlePointerDown = (event: globalThis.PointerEvent) => {\n const target = event.target as Node | null;\n if (target && inside().some((el) => el?.contains(target))) return;\n onAway();\n };\n\n document.addEventListener('pointerdown', handlePointerDown, true);\n return () => document.removeEventListener('pointerdown', handlePointerDown, true);\n }, [active, inside, onAway]);\n};\n\n/** Whatever is still pending when the card unmounts is cancelled with it. */\nconst useClearOnUnmount = (...refs: TimerRef[]) => {\n React.useEffect(\n () => () => refs.forEach(clearPending),\n // The refs are stable objects: a mount/unmount pair, as before.\n [],\n );\n};\n\ninterface HoverCardTimingInput {\n disabled: boolean;\n touchEnabled: boolean;\n enterDelay: number;\n exitDelay: number;\n onOpen?: () => void;\n onClose?: () => void;\n}\n\n/**\n * When the card is open, and the delays that decide it.\n *\n * Three timers are in play: the enter delay before opening, the exit delay\n * before closing (so the pointer can travel from the trigger onto the card\n * without dismissing it), and the long-press timer on touch. Each cancels the\n * others' pending work, which is why they live together.\n */\nexport const useHoverCard = ({\n disabled,\n touchEnabled,\n enterDelay,\n exitDelay,\n onOpen,\n onClose,\n}: HoverCardTimingInput) => {\n const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);\n const [isOpen, setIsOpen] = React.useState(false);\n const [isTouchDevice, setIsTouchDevice] = React.useState(false);\n const enterTimeoutRef = React.useRef<number | undefined>(undefined);\n const exitTimeoutRef = React.useRef<number | undefined>(undefined);\n /** The card's own box, inside the popover paper: a press here is not \"away\". */\n const contentRef = React.useRef<HTMLDivElement | null>(null);\n\n // Probed in an effect, not during render, so SSR and hydration stay intact.\n React.useEffect(() => {\n setIsTouchDevice('ontouchstart' in window);\n }, []);\n\n const openCard = React.useCallback(\n (trigger: HTMLElement) => {\n if (disabled) return;\n\n clearPending(exitTimeoutRef);\n // A second enter before the delay ran out (a click and then a hover, or\n // the pointer jittering across the edge) must REPLACE the pending open,\n // not add one: an overwritten timer can no longer be cancelled, and it\n // reopened the card after Escape had closed it.\n clearPending(enterTimeoutRef);\n\n enterTimeoutRef.current = window.setTimeout(() => {\n setAnchorEl(trigger);\n setIsOpen(true);\n onOpen?.();\n }, enterDelay);\n },\n [disabled, enterDelay, onOpen],\n );\n\n const closeCard = React.useCallback(() => {\n clearPending(enterTimeoutRef);\n clearPending(exitTimeoutRef);\n\n exitTimeoutRef.current = window.setTimeout(() => {\n setIsOpen(false);\n setAnchorEl(null);\n onClose?.();\n }, exitDelay);\n }, [exitDelay, onClose]);\n\n // Escape and the popover's own close are FINAL: nothing still pending may\n // reopen the card behind them. It opens again on the next enter.\n const handleClose = React.useCallback(() => {\n clearPending(enterTimeoutRef);\n clearPending(exitTimeoutRef);\n setIsOpen(false);\n setAnchorEl(null);\n onClose?.();\n }, [onClose]);\n\n const { triggerHandlers, cardHandlers } = useHoverHandlers({\n isTouchDevice,\n touchEnabled,\n enterDelay,\n openCard,\n closeCard,\n exitTimeoutRef,\n });\n\n const insideCard = React.useCallback(() => [anchorEl, contentRef.current], [anchorEl]);\n\n useEscapeKey(isOpen, handleClose);\n useClickAway(isOpen, insideCard, handleClose);\n useClearOnUnmount(enterTimeoutRef, exitTimeoutRef);\n\n return { anchorEl, isOpen, handleClose, triggerHandlers, cardHandlers, contentRef };\n};\n"],"mappings":"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","names":[]}
|