@proyecto-viviana/solidaria 0.2.2 → 0.2.3
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/autocomplete/createAutocomplete.d.ts +2 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/index.js +233 -234
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +233 -234
- package/dist/index.ssr.js.map +2 -2
- package/dist/interactions/PressEvent.d.ts +13 -10
- package/dist/interactions/PressEvent.d.ts.map +1 -1
- package/dist/interactions/createPress.d.ts.map +1 -1
- package/dist/interactions/index.d.ts +1 -1
- package/dist/interactions/index.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/package.json +9 -7
- package/src/autocomplete/createAutocomplete.ts +341 -0
- package/src/autocomplete/index.ts +9 -0
- package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
- package/src/breadcrumbs/index.ts +8 -0
- package/src/button/createButton.ts +142 -0
- package/src/button/createToggleButton.ts +101 -0
- package/src/button/index.ts +4 -0
- package/src/button/types.ts +78 -0
- package/src/calendar/createCalendar.ts +138 -0
- package/src/calendar/createCalendarCell.ts +187 -0
- package/src/calendar/createCalendarGrid.ts +140 -0
- package/src/calendar/createRangeCalendar.ts +136 -0
- package/src/calendar/createRangeCalendarCell.ts +186 -0
- package/src/calendar/index.ts +34 -0
- package/src/checkbox/createCheckbox.ts +135 -0
- package/src/checkbox/createCheckboxGroup.ts +137 -0
- package/src/checkbox/createCheckboxGroupItem.ts +117 -0
- package/src/checkbox/createCheckboxGroupState.ts +193 -0
- package/src/checkbox/index.ts +13 -0
- package/src/color/createColorArea.ts +314 -0
- package/src/color/createColorField.ts +137 -0
- package/src/color/createColorSlider.ts +197 -0
- package/src/color/createColorSwatch.ts +40 -0
- package/src/color/createColorWheel.ts +208 -0
- package/src/color/index.ts +24 -0
- package/src/color/types.ts +116 -0
- package/src/combobox/createComboBox.ts +647 -0
- package/src/combobox/index.ts +6 -0
- package/src/combobox/intl/en-US.json +7 -0
- package/src/combobox/intl/es-ES.json +7 -0
- package/src/combobox/intl/index.ts +23 -0
- package/src/datepicker/createDateField.ts +154 -0
- package/src/datepicker/createDatePicker.ts +206 -0
- package/src/datepicker/createDateSegment.ts +229 -0
- package/src/datepicker/createTimeField.ts +154 -0
- package/src/datepicker/index.ts +28 -0
- package/src/dialog/createDialog.ts +120 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/types.ts +19 -0
- package/src/disclosure/createDisclosure.ts +131 -0
- package/src/disclosure/createDisclosureGroup.ts +62 -0
- package/src/disclosure/index.ts +11 -0
- package/src/dnd/createDrag.ts +209 -0
- package/src/dnd/createDraggableCollection.ts +63 -0
- package/src/dnd/createDraggableItem.ts +243 -0
- package/src/dnd/createDrop.ts +321 -0
- package/src/dnd/createDroppableCollection.ts +293 -0
- package/src/dnd/createDroppableItem.ts +213 -0
- package/src/dnd/index.ts +47 -0
- package/src/dnd/types.ts +89 -0
- package/src/dnd/utils.ts +294 -0
- package/src/focus/FocusScope.tsx +408 -0
- package/src/focus/createAutoFocus.ts +321 -0
- package/src/focus/createFocusRestore.ts +313 -0
- package/src/focus/createVirtualFocus.ts +396 -0
- package/src/focus/index.ts +35 -0
- package/src/form/createFormReset.ts +51 -0
- package/src/form/createFormValidation.ts +224 -0
- package/src/form/index.ts +11 -0
- package/src/grid/GridKeyboardDelegate.ts +429 -0
- package/src/grid/createGrid.ts +261 -0
- package/src/grid/createGridCell.ts +182 -0
- package/src/grid/createGridRow.ts +153 -0
- package/src/grid/index.ts +18 -0
- package/src/grid/types.ts +133 -0
- package/src/gridlist/createGridList.ts +185 -0
- package/src/gridlist/createGridListItem.ts +180 -0
- package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
- package/src/gridlist/index.ts +16 -0
- package/src/gridlist/types.ts +81 -0
- package/src/i18n/NumberFormatter.ts +266 -0
- package/src/i18n/createCollator.ts +79 -0
- package/src/i18n/createDateFormatter.ts +83 -0
- package/src/i18n/createFilter.ts +131 -0
- package/src/i18n/createNumberFormatter.ts +52 -0
- package/src/i18n/createStringFormatter.ts +87 -0
- package/src/i18n/index.ts +40 -0
- package/src/i18n/locale.tsx +188 -0
- package/src/i18n/utils.ts +99 -0
- package/src/index.ts +670 -0
- package/src/interactions/FocusableProvider.tsx +44 -0
- package/src/interactions/PressEvent.ts +126 -0
- package/src/interactions/createFocus.ts +163 -0
- package/src/interactions/createFocusRing.ts +89 -0
- package/src/interactions/createFocusWithin.ts +206 -0
- package/src/interactions/createFocusable.ts +168 -0
- package/src/interactions/createHover.ts +254 -0
- package/src/interactions/createInteractionModality.ts +424 -0
- package/src/interactions/createKeyboard.ts +82 -0
- package/src/interactions/createLongPress.ts +174 -0
- package/src/interactions/createMove.ts +289 -0
- package/src/interactions/createPress.ts +834 -0
- package/src/interactions/index.ts +78 -0
- package/src/label/createField.ts +145 -0
- package/src/label/createLabel.ts +117 -0
- package/src/label/createLabels.ts +50 -0
- package/src/label/index.ts +19 -0
- package/src/landmark/createLandmark.ts +377 -0
- package/src/landmark/index.ts +8 -0
- package/src/link/createLink.ts +182 -0
- package/src/link/index.ts +1 -0
- package/src/listbox/createListBox.ts +269 -0
- package/src/listbox/createOption.ts +151 -0
- package/src/listbox/index.ts +12 -0
- package/src/live-announcer/announce.ts +322 -0
- package/src/live-announcer/index.ts +9 -0
- package/src/menu/createMenu.ts +396 -0
- package/src/menu/createMenuItem.ts +149 -0
- package/src/menu/createMenuTrigger.ts +88 -0
- package/src/menu/index.ts +18 -0
- package/src/meter/createMeter.ts +75 -0
- package/src/meter/index.ts +1 -0
- package/src/numberfield/createNumberField.ts +268 -0
- package/src/numberfield/index.ts +5 -0
- package/src/overlays/ariaHideOutside.ts +219 -0
- package/src/overlays/createInteractOutside.ts +149 -0
- package/src/overlays/createModal.tsx +202 -0
- package/src/overlays/createOverlay.ts +155 -0
- package/src/overlays/createOverlayTrigger.ts +85 -0
- package/src/overlays/createPreventScroll.ts +266 -0
- package/src/overlays/index.ts +44 -0
- package/src/popover/calculatePosition.ts +766 -0
- package/src/popover/createOverlayPosition.ts +356 -0
- package/src/popover/createPopover.ts +170 -0
- package/src/popover/index.ts +24 -0
- package/src/progress/createProgressBar.ts +128 -0
- package/src/progress/index.ts +5 -0
- package/src/radio/createRadio.ts +287 -0
- package/src/radio/createRadioGroup.ts +189 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.ts +23 -0
- package/src/searchfield/createSearchField.ts +186 -0
- package/src/searchfield/index.ts +2 -0
- package/src/select/createHiddenSelect.tsx +236 -0
- package/src/select/createSelect.ts +395 -0
- package/src/select/index.ts +14 -0
- package/src/selection/createTypeSelect.ts +201 -0
- package/src/selection/index.ts +6 -0
- package/src/separator/createSeparator.ts +82 -0
- package/src/separator/index.ts +6 -0
- package/src/slider/createSlider.ts +349 -0
- package/src/slider/index.ts +2 -0
- package/src/ssr/index.tsx +370 -0
- package/src/switch/createSwitch.ts +70 -0
- package/src/switch/index.ts +1 -0
- package/src/table/createTable.ts +526 -0
- package/src/table/createTableCell.ts +147 -0
- package/src/table/createTableColumnHeader.ts +115 -0
- package/src/table/createTableHeaderRow.ts +40 -0
- package/src/table/createTableRow.ts +155 -0
- package/src/table/createTableRowGroup.ts +32 -0
- package/src/table/createTableSelectAllCheckbox.ts +73 -0
- package/src/table/createTableSelectionCheckbox.ts +59 -0
- package/src/table/index.ts +30 -0
- package/src/table/types.ts +165 -0
- package/src/tabs/createTabs.ts +472 -0
- package/src/tabs/index.ts +14 -0
- package/src/tag/createTag.ts +194 -0
- package/src/tag/createTagGroup.ts +154 -0
- package/src/tag/index.ts +12 -0
- package/src/textfield/createTextField.ts +198 -0
- package/src/textfield/index.ts +5 -0
- package/src/toast/createToast.ts +118 -0
- package/src/toast/createToastRegion.ts +100 -0
- package/src/toast/index.ts +11 -0
- package/src/toggle/createToggle.ts +223 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.ts +7 -0
- package/src/toolbar/createToolbar.ts +369 -0
- package/src/toolbar/index.ts +6 -0
- package/src/tooltip/createTooltip.ts +79 -0
- package/src/tooltip/createTooltipTrigger.ts +222 -0
- package/src/tooltip/index.ts +6 -0
- package/src/tree/createTree.ts +246 -0
- package/src/tree/createTreeItem.ts +233 -0
- package/src/tree/createTreeSelectionCheckbox.ts +68 -0
- package/src/tree/index.ts +16 -0
- package/src/tree/types.ts +87 -0
- package/src/utils/createDescription.ts +137 -0
- package/src/utils/dom.ts +327 -0
- package/src/utils/env.ts +54 -0
- package/src/utils/events.ts +106 -0
- package/src/utils/filterDOMProps.ts +116 -0
- package/src/utils/focus.ts +151 -0
- package/src/utils/geometry.ts +115 -0
- package/src/utils/globalListeners.ts +142 -0
- package/src/utils/index.ts +80 -0
- package/src/utils/mergeProps.ts +52 -0
- package/src/utils/platform.ts +52 -0
- package/src/utils/reactivity.ts +36 -0
- package/src/utils/textSelection.ts +114 -0
- package/src/visually-hidden/createVisuallyHidden.ts +124 -0
- package/src/visually-hidden/index.ts +6 -0
- package/dist/index.jsx +0 -15845
- package/dist/index.jsx.map +0 -7
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPress - Handles press interactions across mouse, touch, keyboard, and virtual clicks.
|
|
3
|
+
*
|
|
4
|
+
* This is a 1-1 port of React-Aria's usePress hook adapted for SolidJS.
|
|
5
|
+
* All behaviors, edge cases, and platform-specific handling are preserved.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createSignal, JSX, Accessor, onCleanup } from 'solid-js';
|
|
9
|
+
import { PressEvent, PointerType, createPressEvent, type PressEventSource } from './PressEvent';
|
|
10
|
+
import {
|
|
11
|
+
nodeContains,
|
|
12
|
+
getEventTarget,
|
|
13
|
+
isValidKeyboardEvent,
|
|
14
|
+
isHTMLAnchorLink,
|
|
15
|
+
shouldPreventDefaultKeyboard,
|
|
16
|
+
isVirtualClick,
|
|
17
|
+
isVirtualPointerEvent,
|
|
18
|
+
isPointOverTarget,
|
|
19
|
+
getTouchFromEvent,
|
|
20
|
+
getTouchById,
|
|
21
|
+
disableTextSelection,
|
|
22
|
+
restoreTextSelection,
|
|
23
|
+
preventFocus,
|
|
24
|
+
openLink,
|
|
25
|
+
isMac,
|
|
26
|
+
createGlobalListeners,
|
|
27
|
+
setEventTarget,
|
|
28
|
+
} from '../utils';
|
|
29
|
+
|
|
30
|
+
// Re-export PressEvent types
|
|
31
|
+
export { PressEvent, type PointerType } from './PressEvent';
|
|
32
|
+
export type { IPressEvent, PressEventType } from './PressEvent';
|
|
33
|
+
|
|
34
|
+
export interface CreatePressProps {
|
|
35
|
+
/** Whether the target is currently disabled. */
|
|
36
|
+
isDisabled?: Accessor<boolean> | boolean;
|
|
37
|
+
/** Handler called when the press is released over the target. */
|
|
38
|
+
onPress?: (e: PressEvent) => void;
|
|
39
|
+
/** Handler called when a press interaction starts. */
|
|
40
|
+
onPressStart?: (e: PressEvent) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Handler called when a press interaction ends, either
|
|
43
|
+
* over the target or when the pointer leaves the target.
|
|
44
|
+
*/
|
|
45
|
+
onPressEnd?: (e: PressEvent) => void;
|
|
46
|
+
/** Handler called when a press is released over the target, regardless of whether it started on the target. */
|
|
47
|
+
onPressUp?: (e: PressEvent) => void;
|
|
48
|
+
/** Handler called when the press state changes. */
|
|
49
|
+
onPressChange?: (isPressed: boolean) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Handler called on native click event.
|
|
52
|
+
* Some third-party libraries pass onClick instead of onPress.
|
|
53
|
+
* This matches the browser's native activation behavior for certain elements.
|
|
54
|
+
*/
|
|
55
|
+
onClick?: (e: MouseEvent) => void;
|
|
56
|
+
/** Whether the press should be visual only, not triggering onPress. */
|
|
57
|
+
isPressed?: Accessor<boolean> | boolean;
|
|
58
|
+
/** Whether to prevent focus when pressing. */
|
|
59
|
+
preventFocusOnPress?: boolean;
|
|
60
|
+
/** Whether long press should cancel when pointer moves out of target. */
|
|
61
|
+
shouldCancelOnPointerExit?: boolean;
|
|
62
|
+
/** Whether text selection should be allowed during press. */
|
|
63
|
+
allowTextSelectionOnPress?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PressResult {
|
|
67
|
+
/** Whether the target is currently pressed. */
|
|
68
|
+
isPressed: Accessor<boolean>;
|
|
69
|
+
/** Props to spread on the target element. */
|
|
70
|
+
pressProps: JSX.HTMLAttributes<HTMLElement>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isDisabledValue(isDisabled: Accessor<boolean> | boolean | undefined): boolean {
|
|
74
|
+
if (typeof isDisabled === 'function') {
|
|
75
|
+
return isDisabled();
|
|
76
|
+
}
|
|
77
|
+
return isDisabled ?? false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPressedValue(isPressed: Accessor<boolean> | boolean | undefined): boolean {
|
|
81
|
+
if (typeof isPressed === 'function') {
|
|
82
|
+
return isPressed();
|
|
83
|
+
}
|
|
84
|
+
return isPressed ?? false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Symbol to track if a link click was handled by us
|
|
88
|
+
const LINK_CLICKED = Symbol('linkClicked');
|
|
89
|
+
|
|
90
|
+
// CSS for preventing double-tap zoom delay
|
|
91
|
+
let pressableCSSInjected = false;
|
|
92
|
+
function injectPressableCSS(): void {
|
|
93
|
+
if (pressableCSSInjected || typeof document === 'undefined') return;
|
|
94
|
+
|
|
95
|
+
const style = document.createElement('style');
|
|
96
|
+
style.id = 'solidaria-pressable-style';
|
|
97
|
+
style.textContent = `
|
|
98
|
+
[data-solidaria-pressable] {
|
|
99
|
+
touch-action: pan-x pan-y pinch-zoom;
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
document.head.appendChild(style);
|
|
103
|
+
pressableCSSInjected = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handles press interactions across mouse, touch, keyboard, and screen readers.
|
|
108
|
+
* Provides consistent press behavior regardless of input method.
|
|
109
|
+
*
|
|
110
|
+
* Based on react-aria's usePress but adapted for SolidJS.
|
|
111
|
+
*/
|
|
112
|
+
export function createPress(props: CreatePressProps = {}): PressResult {
|
|
113
|
+
// Internal pressed state (for visual feedback)
|
|
114
|
+
const [internalIsPressed, setInternalIsPressed] = createSignal(false);
|
|
115
|
+
|
|
116
|
+
// Use controlled isPressed if provided, otherwise internal state
|
|
117
|
+
const isPressed = (): boolean => {
|
|
118
|
+
const controlledPressed = isPressedValue(props.isPressed);
|
|
119
|
+
if (controlledPressed) {
|
|
120
|
+
return controlledPressed;
|
|
121
|
+
}
|
|
122
|
+
return internalIsPressed();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// State tracking (using plain variables - SolidJS doesn't need refs for mutable state)
|
|
126
|
+
let pressState = {
|
|
127
|
+
isPressed: false,
|
|
128
|
+
ignoreEmulatedMouseEvents: false,
|
|
129
|
+
ignoreClickAfterPress: false,
|
|
130
|
+
didFirePressStart: false,
|
|
131
|
+
isTriggeringEvent: false,
|
|
132
|
+
activePointerId: null as number | null,
|
|
133
|
+
target: null as Element | null,
|
|
134
|
+
isOverTarget: false,
|
|
135
|
+
pointerType: null as PointerType | null,
|
|
136
|
+
userSelect: undefined as string | undefined,
|
|
137
|
+
metaKeyEvents: null as Map<string, KeyboardEvent> | null,
|
|
138
|
+
clickCleanup: null as (() => void) | null,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Global listeners manager
|
|
142
|
+
const { addGlobalListener, removeAllGlobalListeners } = createGlobalListeners();
|
|
143
|
+
|
|
144
|
+
// Inject CSS on first use
|
|
145
|
+
injectPressableCSS();
|
|
146
|
+
|
|
147
|
+
// --- Event Triggers ---
|
|
148
|
+
|
|
149
|
+
const triggerPressStart = (originalEvent: PressEventSource, pointerType: PointerType): boolean => {
|
|
150
|
+
if (isDisabledValue(props.isDisabled) || pressState.didFirePressStart) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let shouldStopPropagation = true;
|
|
155
|
+
pressState.isTriggeringEvent = true;
|
|
156
|
+
|
|
157
|
+
if (props.onPressStart) {
|
|
158
|
+
const event = createPressEvent('pressstart', pointerType, originalEvent, pressState.target!);
|
|
159
|
+
props.onPressStart(event);
|
|
160
|
+
shouldStopPropagation = event.shouldStopPropagation;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (props.onPressChange) {
|
|
164
|
+
props.onPressChange(true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pressState.isTriggeringEvent = false;
|
|
168
|
+
pressState.didFirePressStart = true;
|
|
169
|
+
setInternalIsPressed(true);
|
|
170
|
+
|
|
171
|
+
return shouldStopPropagation;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const triggerPressEnd = (originalEvent: PressEventSource, pointerType: PointerType, wasPressed = true): boolean => {
|
|
175
|
+
if (!pressState.didFirePressStart) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
pressState.didFirePressStart = false;
|
|
180
|
+
pressState.isTriggeringEvent = true;
|
|
181
|
+
|
|
182
|
+
let shouldStopPropagation = true;
|
|
183
|
+
if (props.onPressEnd) {
|
|
184
|
+
const event = createPressEvent('pressend', pointerType, originalEvent, pressState.target!);
|
|
185
|
+
props.onPressEnd(event);
|
|
186
|
+
shouldStopPropagation = event.shouldStopPropagation;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (props.onPressChange) {
|
|
190
|
+
props.onPressChange(false);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setInternalIsPressed(false);
|
|
194
|
+
|
|
195
|
+
if (wasPressed && !isDisabledValue(props.isDisabled)) {
|
|
196
|
+
if (props.onPress) {
|
|
197
|
+
const event = createPressEvent('press', pointerType, originalEvent, pressState.target!);
|
|
198
|
+
props.onPress(event);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
pressState.isTriggeringEvent = false;
|
|
203
|
+
|
|
204
|
+
return shouldStopPropagation;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const triggerPressUp = (originalEvent: PressEventSource, pointerType: PointerType): boolean => {
|
|
208
|
+
if (isDisabledValue(props.isDisabled)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (props.onPressUp) {
|
|
213
|
+
pressState.isTriggeringEvent = true;
|
|
214
|
+
const event = createPressEvent('pressup', pointerType, originalEvent, pressState.target!);
|
|
215
|
+
props.onPressUp(event);
|
|
216
|
+
pressState.isTriggeringEvent = false;
|
|
217
|
+
return event.shouldStopPropagation;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return true;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const triggerSyntheticClick = (originalEvent: KeyboardEvent | TouchEvent, target: HTMLElement): void => {
|
|
224
|
+
if (isDisabledValue(props.isDisabled)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (props.onClick) {
|
|
229
|
+
const event = new MouseEvent('click', originalEvent as MouseEventInit);
|
|
230
|
+
setEventTarget(event, target);
|
|
231
|
+
props.onClick(event);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const cancel = (originalEvent: PressEventSource): void => {
|
|
236
|
+
if (!pressState.isPressed) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (pressState.target && pressState.didFirePressStart && pressState.pointerType != null) {
|
|
241
|
+
triggerPressEnd(originalEvent, pressState.pointerType, false);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pressState.isPressed = false;
|
|
245
|
+
pressState.isOverTarget = false;
|
|
246
|
+
pressState.activePointerId = null;
|
|
247
|
+
pressState.pointerType = null;
|
|
248
|
+
|
|
249
|
+
removeAllGlobalListeners();
|
|
250
|
+
|
|
251
|
+
// Clean up click timeout/listener if set
|
|
252
|
+
if (pressState.clickCleanup) {
|
|
253
|
+
pressState.clickCleanup();
|
|
254
|
+
pressState.clickCleanup = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!props.allowTextSelectionOnPress) {
|
|
258
|
+
restoreTextSelection(pressState.target as HTMLElement);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// --- Pointer Event Handlers (used when PointerEvent is available) ---
|
|
263
|
+
|
|
264
|
+
const onPointerDown: JSX.EventHandler<HTMLElement, PointerEvent> = (e) => {
|
|
265
|
+
// Only handle left clicks, and ignore events that bubbled through portals
|
|
266
|
+
const button = e.button ?? 0;
|
|
267
|
+
if (button !== 0 || !nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// iOS VoiceOver bug: fires pointer events with incorrect coordinates
|
|
272
|
+
// Let the click handler deal with it instead
|
|
273
|
+
if (isVirtualPointerEvent(e)) {
|
|
274
|
+
pressState.pointerType = 'virtual';
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
pressState.pointerType = e.pointerType as PointerType;
|
|
279
|
+
|
|
280
|
+
if (!pressState.isPressed) {
|
|
281
|
+
pressState.isPressed = true;
|
|
282
|
+
pressState.isOverTarget = true;
|
|
283
|
+
pressState.activePointerId = e.pointerId;
|
|
284
|
+
pressState.target = e.currentTarget;
|
|
285
|
+
|
|
286
|
+
if (!props.allowTextSelectionOnPress) {
|
|
287
|
+
disableTextSelection(pressState.target as HTMLElement);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const shouldStopPropagation = triggerPressStart(e, pressState.pointerType);
|
|
291
|
+
if (shouldStopPropagation) {
|
|
292
|
+
e.stopPropagation();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Set up global listeners for pointer events
|
|
296
|
+
addGlobalListener('pointerup', onPointerUp);
|
|
297
|
+
addGlobalListener('pointercancel', onPointerCancel);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Mouse down handler when using pointer events - only prevents focus, doesn't trigger press
|
|
302
|
+
const onMouseDownPointer: JSX.EventHandler<HTMLElement, MouseEvent> = (e) => {
|
|
303
|
+
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (e.button === 0) {
|
|
308
|
+
// Prevent focus if requested
|
|
309
|
+
if (props.preventFocusOnPress) {
|
|
310
|
+
preventFocus(e.currentTarget);
|
|
311
|
+
}
|
|
312
|
+
e.stopPropagation();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const onPointerUp = (e: PointerEvent): void => {
|
|
317
|
+
// Only handle events for our active pointer
|
|
318
|
+
const button = e.button ?? 0;
|
|
319
|
+
if (e.pointerId !== pressState.activePointerId || !pressState.isPressed || button !== 0 || !pressState.target) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const isOverTarget = nodeContains(pressState.target, getEventTarget(e) as Element);
|
|
324
|
+
if (isOverTarget && pressState.pointerType != null && pressState.pointerType !== 'virtual') {
|
|
325
|
+
// Pointer released over target - wait for onClick to complete the press sequence.
|
|
326
|
+
// This matches React-Aria's behavior for compatibility with DOM mutations and third-party libraries.
|
|
327
|
+
// https://github.com/adobe/react-spectrum/issues/1513
|
|
328
|
+
// https://issues.chromium.org/issues/40732224
|
|
329
|
+
//
|
|
330
|
+
// However, if stopPropagation is called on the click event (e.g., by a child input element),
|
|
331
|
+
// the onClick handler on this element won't fire. We work around this by triggering a click
|
|
332
|
+
// ourselves after a timeout. This timeout is canceled during the click event in case the
|
|
333
|
+
// real one fires first. The timeout must be at least 32ms, because Safari on iOS delays the
|
|
334
|
+
// click event on non-form elements without certain ARIA roles (for hover emulation).
|
|
335
|
+
// https://github.com/WebKit/WebKit/blob/dccfae42bb29bd4bdef052e469f604a9387241c0/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm#L875-L892
|
|
336
|
+
let clickFired = false;
|
|
337
|
+
const timeout = setTimeout(() => {
|
|
338
|
+
// Guard for SSR/test environments where the element may no longer exist
|
|
339
|
+
if (typeof HTMLElement === 'undefined') {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (pressState.isPressed && pressState.target instanceof HTMLElement) {
|
|
343
|
+
if (clickFired) {
|
|
344
|
+
// Click already happened, just cancel the press state
|
|
345
|
+
cancel(e);
|
|
346
|
+
} else {
|
|
347
|
+
// Click didn't happen (probably due to stopPropagation), trigger it manually
|
|
348
|
+
pressState.target.focus();
|
|
349
|
+
pressState.target.click();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}, 80);
|
|
353
|
+
|
|
354
|
+
// Use a capturing listener to track if a click occurred.
|
|
355
|
+
// If stopPropagation is called it may never reach our handler.
|
|
356
|
+
const doc = pressState.target.ownerDocument ?? document;
|
|
357
|
+
const clickListener = () => {
|
|
358
|
+
clickFired = true;
|
|
359
|
+
};
|
|
360
|
+
doc.addEventListener('click', clickListener, true);
|
|
361
|
+
|
|
362
|
+
// Store cleanup function
|
|
363
|
+
pressState.clickCleanup = () => {
|
|
364
|
+
clearTimeout(timeout);
|
|
365
|
+
doc.removeEventListener('click', clickListener, true);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
pressState.isOverTarget = false;
|
|
369
|
+
} else {
|
|
370
|
+
// Pointer released outside target, or virtual - cancel the press
|
|
371
|
+
cancel(e);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const onPointerCancel = (e: PointerEvent): void => {
|
|
376
|
+
if (e.pointerId === pressState.activePointerId) {
|
|
377
|
+
cancel(e);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const onPointerEnter: JSX.EventHandler<HTMLElement, PointerEvent> = (e) => {
|
|
382
|
+
if (e.pointerId === pressState.activePointerId && pressState.target && !pressState.isOverTarget && pressState.pointerType != null) {
|
|
383
|
+
pressState.isOverTarget = true;
|
|
384
|
+
triggerPressStart(e, pressState.pointerType);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const onPointerLeave: JSX.EventHandler<HTMLElement, PointerEvent> = (e) => {
|
|
389
|
+
if (e.pointerId === pressState.activePointerId && pressState.target && pressState.isOverTarget && pressState.pointerType != null) {
|
|
390
|
+
pressState.isOverTarget = false;
|
|
391
|
+
triggerPressEnd(e, pressState.pointerType, false);
|
|
392
|
+
|
|
393
|
+
if (props.shouldCancelOnPointerExit) {
|
|
394
|
+
cancel(e);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// --- Touch Event Helpers ---
|
|
400
|
+
|
|
401
|
+
const createTouchEvent = (target: Element, event: TouchEvent): PressEventSource => {
|
|
402
|
+
let clientX = 0;
|
|
403
|
+
let clientY = 0;
|
|
404
|
+
if (event.targetTouches && event.targetTouches.length === 1) {
|
|
405
|
+
clientX = event.targetTouches[0].clientX;
|
|
406
|
+
clientY = event.targetTouches[0].clientY;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
currentTarget: target,
|
|
410
|
+
shiftKey: event.shiftKey,
|
|
411
|
+
ctrlKey: event.ctrlKey,
|
|
412
|
+
metaKey: event.metaKey,
|
|
413
|
+
altKey: event.altKey,
|
|
414
|
+
clientX,
|
|
415
|
+
clientY,
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// --- Touch Event Handlers (fallback for testing/older browsers) ---
|
|
420
|
+
|
|
421
|
+
const onTouchStart: JSX.EventHandler<HTMLElement, TouchEvent> = (e) => {
|
|
422
|
+
if (isDisabledValue(props.isDisabled)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// If already pressed via pointer events, ignore touch events
|
|
427
|
+
if (pressState.isPressed) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const touch = getTouchFromEvent(e);
|
|
432
|
+
if (!touch) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
pressState.activePointerId = touch.identifier;
|
|
437
|
+
pressState.ignoreEmulatedMouseEvents = true;
|
|
438
|
+
pressState.isOverTarget = true;
|
|
439
|
+
pressState.isPressed = true;
|
|
440
|
+
pressState.target = e.currentTarget;
|
|
441
|
+
pressState.pointerType = 'touch';
|
|
442
|
+
|
|
443
|
+
if (!props.allowTextSelectionOnPress) {
|
|
444
|
+
disableTextSelection(pressState.target as HTMLElement);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const shouldStopPropagation = triggerPressStart(createTouchEvent(pressState.target, e), 'touch');
|
|
448
|
+
if (shouldStopPropagation) {
|
|
449
|
+
e.stopPropagation();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
addGlobalListener('scroll', onScroll, { capture: true, isWindow: true });
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const onTouchMove: JSX.EventHandler<HTMLElement, TouchEvent> = (e) => {
|
|
456
|
+
if (!pressState.isPressed) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const touch = getTouchById(e, pressState.activePointerId);
|
|
461
|
+
if (!touch) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const target = pressState.target!;
|
|
466
|
+
const isOverTarget = isPointOverTarget(touch, target);
|
|
467
|
+
|
|
468
|
+
if (isOverTarget !== pressState.isOverTarget) {
|
|
469
|
+
pressState.isOverTarget = isOverTarget;
|
|
470
|
+
if (isOverTarget) {
|
|
471
|
+
triggerPressStart(createTouchEvent(target, e), 'touch');
|
|
472
|
+
} else {
|
|
473
|
+
triggerPressEnd(createTouchEvent(target, e), 'touch', false);
|
|
474
|
+
|
|
475
|
+
if (props.shouldCancelOnPointerExit) {
|
|
476
|
+
cancel(createTouchEvent(target, e));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const onTouchEnd: JSX.EventHandler<HTMLElement, TouchEvent> = (e) => {
|
|
483
|
+
if (!pressState.isPressed) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const touch = getTouchById(e, pressState.activePointerId);
|
|
488
|
+
if (!touch) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const target = pressState.target!;
|
|
493
|
+
const isOverTarget = isPointOverTarget(touch, target);
|
|
494
|
+
|
|
495
|
+
if (isOverTarget) {
|
|
496
|
+
triggerPressUp(createTouchEvent(target, e), 'touch');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
triggerPressEnd(createTouchEvent(target, e), 'touch', isOverTarget && pressState.isOverTarget);
|
|
500
|
+
|
|
501
|
+
pressState.isPressed = false;
|
|
502
|
+
pressState.isOverTarget = false;
|
|
503
|
+
pressState.activePointerId = null;
|
|
504
|
+
pressState.pointerType = null;
|
|
505
|
+
|
|
506
|
+
removeAllGlobalListeners();
|
|
507
|
+
|
|
508
|
+
if (!props.allowTextSelectionOnPress) {
|
|
509
|
+
restoreTextSelection(target as HTMLElement);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const onTouchCancel: JSX.EventHandler<HTMLElement, TouchEvent> = (e) => {
|
|
514
|
+
if (pressState.target) {
|
|
515
|
+
cancel(createTouchEvent(pressState.target, e));
|
|
516
|
+
} else {
|
|
517
|
+
cancel(e);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const onScroll = (e: Event): void => {
|
|
522
|
+
if (pressState.isPressed && nodeContains(e.target as Element, pressState.target)) {
|
|
523
|
+
cancel(e);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// --- Mouse Event Handlers (fallback when PointerEvent is not available) ---
|
|
528
|
+
|
|
529
|
+
const onMouseDownFallback: JSX.EventHandler<HTMLElement, MouseEvent> = (e) => {
|
|
530
|
+
// Only handle left button
|
|
531
|
+
if (e.button !== 0) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Ignore emulated mouse events from touch
|
|
536
|
+
if (pressState.ignoreEmulatedMouseEvents) {
|
|
537
|
+
e.stopPropagation();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
pressState.isPressed = true;
|
|
542
|
+
pressState.isOverTarget = true;
|
|
543
|
+
pressState.target = e.currentTarget;
|
|
544
|
+
pressState.pointerType = isVirtualClick(e) ? 'virtual' : 'mouse';
|
|
545
|
+
|
|
546
|
+
const shouldStopPropagation = triggerPressStart(e, pressState.pointerType);
|
|
547
|
+
if (shouldStopPropagation) {
|
|
548
|
+
e.stopPropagation();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
addGlobalListener('mouseup', onMouseUpFallback);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const onMouseUpFallback = (e: MouseEvent): void => {
|
|
555
|
+
if (e.button !== 0) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!pressState.ignoreEmulatedMouseEvents && e.button === 0 && !pressState.isPressed) {
|
|
560
|
+
triggerPressUp(e, pressState.pointerType || 'mouse');
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const onMouseEnterFallback: JSX.EventHandler<HTMLElement, MouseEvent> = (e) => {
|
|
565
|
+
if (!pressState.isPressed || pressState.ignoreEmulatedMouseEvents) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (pressState.isPressed && !pressState.ignoreEmulatedMouseEvents && pressState.pointerType != null) {
|
|
570
|
+
pressState.isOverTarget = true;
|
|
571
|
+
triggerPressStart(e, pressState.pointerType);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const onMouseLeaveFallback: JSX.EventHandler<HTMLElement, MouseEvent> = (e) => {
|
|
576
|
+
if (!pressState.isPressed || pressState.ignoreEmulatedMouseEvents) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (pressState.isPressed && !pressState.ignoreEmulatedMouseEvents && pressState.pointerType != null) {
|
|
581
|
+
pressState.isOverTarget = false;
|
|
582
|
+
triggerPressEnd(e, pressState.pointerType, false);
|
|
583
|
+
|
|
584
|
+
if (props.shouldCancelOnPointerExit) {
|
|
585
|
+
cancel(e);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// --- Keyboard Event Handlers ---
|
|
591
|
+
|
|
592
|
+
const onKeyDown: JSX.EventHandler<HTMLElement, KeyboardEvent> = (e) => {
|
|
593
|
+
if (isDisabledValue(props.isDisabled)) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!isValidKeyboardEvent(e, e.currentTarget)) {
|
|
598
|
+
// Allow event to propagate for invalid keys
|
|
599
|
+
if (e.key === 'Enter') {
|
|
600
|
+
e.stopPropagation();
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Prevent key repeat
|
|
606
|
+
if (e.repeat) {
|
|
607
|
+
e.preventDefault();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
pressState.target = e.currentTarget;
|
|
612
|
+
pressState.isPressed = true;
|
|
613
|
+
pressState.isOverTarget = true;
|
|
614
|
+
pressState.pointerType = 'keyboard';
|
|
615
|
+
|
|
616
|
+
const shouldStopPropagation = triggerPressStart(e, 'keyboard');
|
|
617
|
+
if (shouldStopPropagation) {
|
|
618
|
+
e.stopPropagation();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Prevent default for non-native interactive elements
|
|
622
|
+
if (shouldPreventDefaultKeyboard(e.currentTarget, e.key)) {
|
|
623
|
+
e.preventDefault();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// macOS bug: keyup doesn't fire while Meta key is held
|
|
627
|
+
// Track keydown events while Meta is held so we can manually dispatch keyup
|
|
628
|
+
if (isMac() && e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
629
|
+
pressState.metaKeyEvents = pressState.metaKeyEvents || new Map();
|
|
630
|
+
pressState.metaKeyEvents.set(e.key, e);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// For Enter key on native buttons, the click fires on keydown
|
|
634
|
+
// Set flag to ignore it
|
|
635
|
+
if (e.key === 'Enter') {
|
|
636
|
+
pressState.ignoreClickAfterPress = true;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Set up global keyup listener
|
|
640
|
+
addGlobalListener('keyup', onKeyUp, { capture: true });
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const onKeyUp = (e: KeyboardEvent): void => {
|
|
644
|
+
if (!pressState.isPressed || pressState.pointerType !== 'keyboard') {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (!isValidKeyboardEvent(e, pressState.target!)) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Handle macOS Meta key bug
|
|
653
|
+
if (isMac() && e.key === 'Meta' && pressState.metaKeyEvents?.size) {
|
|
654
|
+
// When Meta releases, dispatch keyup for any keys that were pressed during
|
|
655
|
+
for (const [key, event] of pressState.metaKeyEvents) {
|
|
656
|
+
pressState.target?.dispatchEvent(
|
|
657
|
+
new KeyboardEvent('keyup', {
|
|
658
|
+
key,
|
|
659
|
+
code: event.code,
|
|
660
|
+
bubbles: true,
|
|
661
|
+
cancelable: true,
|
|
662
|
+
})
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
pressState.metaKeyEvents.clear();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const target = pressState.target!;
|
|
670
|
+
const shouldStopPropagation = triggerPressUp(e, 'keyboard');
|
|
671
|
+
const shouldStopPropagationEnd = triggerPressEnd(e, 'keyboard', pressState.isOverTarget);
|
|
672
|
+
|
|
673
|
+
pressState.isPressed = false;
|
|
674
|
+
pressState.pointerType = null;
|
|
675
|
+
|
|
676
|
+
removeAllGlobalListeners();
|
|
677
|
+
|
|
678
|
+
// Prevent default to avoid triggering native action
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
|
|
681
|
+
// Fire synthetic click for keyboard activation
|
|
682
|
+
if (pressState.isOverTarget && pressState.target) {
|
|
683
|
+
triggerSyntheticClick(e, pressState.target as HTMLElement);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Handle link activation with non-Enter keys (Space)
|
|
687
|
+
// Native links only respond to Enter, but we want Space to work too
|
|
688
|
+
if (e.key === ' ' && isHTMLAnchorLink(target) && !(target as any)[LINK_CLICKED]) {
|
|
689
|
+
(target as any)[LINK_CLICKED] = true;
|
|
690
|
+
openLink(target as HTMLAnchorElement, e);
|
|
691
|
+
// Clean up the marker
|
|
692
|
+
setTimeout(() => {
|
|
693
|
+
delete (target as any)[LINK_CLICKED];
|
|
694
|
+
}, 0);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// For Space key, the click fires after keyup
|
|
698
|
+
// Set flag to ignore it
|
|
699
|
+
if (e.key === ' ') {
|
|
700
|
+
pressState.ignoreClickAfterPress = true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (shouldStopPropagation && shouldStopPropagationEnd) {
|
|
704
|
+
e.stopPropagation();
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// --- Click Event Handler ---
|
|
709
|
+
|
|
710
|
+
const onClick: JSX.EventHandler<HTMLElement, MouseEvent> = (e) => {
|
|
711
|
+
// Don't handle click if it's not on the target
|
|
712
|
+
if (!nodeContains(e.currentTarget, e.target as Element)) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Only process left clicks that aren't from our own event triggers
|
|
717
|
+
if (e.button === 0 && !pressState.isTriggeringEvent) {
|
|
718
|
+
if (pressState.ignoreClickAfterPress) {
|
|
719
|
+
pressState.ignoreClickAfterPress = false;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (isDisabledValue(props.isDisabled)) {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Call user's onClick handler if provided
|
|
729
|
+
// This matches React-Aria's behavior for third-party library compatibility
|
|
730
|
+
props.onClick?.(e);
|
|
731
|
+
|
|
732
|
+
// If triggered from a screen reader or by using element.click(),
|
|
733
|
+
// trigger as if it were a keyboard/virtual click.
|
|
734
|
+
let shouldStopPropagation = true;
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
!pressState.ignoreEmulatedMouseEvents &&
|
|
738
|
+
!pressState.isPressed &&
|
|
739
|
+
(pressState.pointerType === 'virtual' || isVirtualClick(e))
|
|
740
|
+
) {
|
|
741
|
+
pressState.target = e.currentTarget;
|
|
742
|
+
shouldStopPropagation = triggerPressStart(e, 'virtual');
|
|
743
|
+
shouldStopPropagation = triggerPressUp(e, 'virtual') && shouldStopPropagation;
|
|
744
|
+
shouldStopPropagation = triggerPressEnd(e, 'virtual', true) && shouldStopPropagation;
|
|
745
|
+
} else if (pressState.isPressed && pressState.pointerType !== 'keyboard') {
|
|
746
|
+
// Complete the press sequence for pointer/touch/mouse events
|
|
747
|
+
const pointerType =
|
|
748
|
+
pressState.pointerType ||
|
|
749
|
+
((e as unknown as PointerEvent).pointerType as PointerType) ||
|
|
750
|
+
'virtual';
|
|
751
|
+
shouldStopPropagation = triggerPressUp(e, pointerType);
|
|
752
|
+
shouldStopPropagation = triggerPressEnd(e, pointerType, true) && shouldStopPropagation;
|
|
753
|
+
pressState.isOverTarget = false;
|
|
754
|
+
cancel(e);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
pressState.ignoreEmulatedMouseEvents = false;
|
|
758
|
+
|
|
759
|
+
if (shouldStopPropagation) {
|
|
760
|
+
e.stopPropagation();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// --- Drag Event Handler ---
|
|
766
|
+
|
|
767
|
+
const onDragStart: JSX.EventHandler<HTMLElement, DragEvent> = (e) => {
|
|
768
|
+
// Safari doesn't fire pointercancel on drag, so we need to cancel manually
|
|
769
|
+
if (pressState.isPressed) {
|
|
770
|
+
cancel(e);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// --- Build Props ---
|
|
775
|
+
// Conditionally use pointer events or mouse events based on browser support
|
|
776
|
+
// This matches React-Aria's approach exactly
|
|
777
|
+
|
|
778
|
+
const pressProps: JSX.HTMLAttributes<HTMLElement> & { 'data-solidaria-pressable': string } =
|
|
779
|
+
typeof PointerEvent !== 'undefined'
|
|
780
|
+
? {
|
|
781
|
+
// Keyboard events
|
|
782
|
+
onKeyDown,
|
|
783
|
+
onKeyUp,
|
|
784
|
+
onClick,
|
|
785
|
+
onDragStart,
|
|
786
|
+
// Pointer events (preferred when available)
|
|
787
|
+
onPointerDown,
|
|
788
|
+
onPointerEnter,
|
|
789
|
+
onPointerLeave,
|
|
790
|
+
// Mouse down only for focus prevention when using pointer events
|
|
791
|
+
onMouseDown: onMouseDownPointer,
|
|
792
|
+
// Touch events (always included for ignoreEmulatedMouseEvents handling)
|
|
793
|
+
onTouchStart,
|
|
794
|
+
onTouchMove,
|
|
795
|
+
onTouchEnd,
|
|
796
|
+
onTouchCancel,
|
|
797
|
+
// Attribute for CSS touch-action
|
|
798
|
+
'data-solidaria-pressable': '',
|
|
799
|
+
}
|
|
800
|
+
: {
|
|
801
|
+
// Keyboard events
|
|
802
|
+
onKeyDown,
|
|
803
|
+
onKeyUp,
|
|
804
|
+
onClick,
|
|
805
|
+
onDragStart,
|
|
806
|
+
// Mouse events (fallback when PointerEvent not available)
|
|
807
|
+
onMouseDown: onMouseDownFallback,
|
|
808
|
+
onMouseUp: onMouseUpFallback,
|
|
809
|
+
onMouseEnter: onMouseEnterFallback,
|
|
810
|
+
onMouseLeave: onMouseLeaveFallback,
|
|
811
|
+
// Touch events (always included)
|
|
812
|
+
onTouchStart,
|
|
813
|
+
onTouchMove,
|
|
814
|
+
onTouchEnd,
|
|
815
|
+
onTouchCancel,
|
|
816
|
+
// Attribute for CSS touch-action
|
|
817
|
+
'data-solidaria-pressable': '',
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// Clean up on unmount
|
|
821
|
+
onCleanup(() => {
|
|
822
|
+
removeAllGlobalListeners();
|
|
823
|
+
// Clean up click timeout/listener if pending
|
|
824
|
+
if (pressState.clickCleanup) {
|
|
825
|
+
pressState.clickCleanup();
|
|
826
|
+
pressState.clickCleanup = null;
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
isPressed,
|
|
832
|
+
pressProps,
|
|
833
|
+
};
|
|
834
|
+
}
|