@react-aria/interactions 3.9.0 → 3.11.0

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/src/usePress.ts CHANGED
@@ -16,11 +16,11 @@
16
16
  // See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
17
17
 
18
18
  import {disableTextSelection, restoreTextSelection} from './textSelection';
19
+ import {DOMAttributes, FocusableElement, PointerType, PressEvents} from '@react-types/shared';
19
20
  import {focusWithoutScrolling, mergeProps, useGlobalListeners, useSyncRef} from '@react-aria/utils';
20
- import {HTMLAttributes, RefObject, useContext, useEffect, useMemo, useRef, useState} from 'react';
21
21
  import {isVirtualClick} from './utils';
22
- import {PointerType, PressEvents} from '@react-types/shared';
23
22
  import {PressResponderContext} from './context';
23
+ import {RefObject, useContext, useEffect, useMemo, useRef, useState} from 'react';
24
24
 
25
25
  export interface PressProps extends PressEvents {
26
26
  /** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
@@ -42,7 +42,7 @@ export interface PressProps extends PressEvents {
42
42
 
43
43
  export interface PressHookProps extends PressProps {
44
44
  /** A ref to the target element. */
45
- ref?: RefObject<HTMLElement>
45
+ ref?: RefObject<Element>
46
46
  }
47
47
 
48
48
  interface PressState {
@@ -51,7 +51,7 @@ interface PressState {
51
51
  ignoreClickAfterPress: boolean,
52
52
  didFirePressStart: boolean,
53
53
  activePointerId: any,
54
- target: HTMLElement | null,
54
+ target: FocusableElement | null,
55
55
  isOverTarget: boolean,
56
56
  pointerType: PointerType,
57
57
  userSelect?: string
@@ -69,7 +69,7 @@ export interface PressResult {
69
69
  /** Whether the target is currently pressed. */
70
70
  isPressed: boolean,
71
71
  /** Props to spread on the target element. */
72
- pressProps: HTMLAttributes<HTMLElement>
72
+ pressProps: DOMAttributes
73
73
  }
74
74
 
75
75
  function usePressResponderContext(props: PressHookProps): PressHookProps {
@@ -135,7 +135,7 @@ export function usePress(props: PressHookProps): PressResult {
135
135
  onPressStart({
136
136
  type: 'pressstart',
137
137
  pointerType,
138
- target: originalEvent.currentTarget as HTMLElement,
138
+ target: originalEvent.currentTarget as Element,
139
139
  shiftKey: originalEvent.shiftKey,
140
140
  metaKey: originalEvent.metaKey,
141
141
  ctrlKey: originalEvent.ctrlKey,
@@ -164,7 +164,7 @@ export function usePress(props: PressHookProps): PressResult {
164
164
  onPressEnd({
165
165
  type: 'pressend',
166
166
  pointerType,
167
- target: originalEvent.currentTarget as HTMLElement,
167
+ target: originalEvent.currentTarget as Element,
168
168
  shiftKey: originalEvent.shiftKey,
169
169
  metaKey: originalEvent.metaKey,
170
170
  ctrlKey: originalEvent.ctrlKey,
@@ -182,7 +182,7 @@ export function usePress(props: PressHookProps): PressResult {
182
182
  onPress({
183
183
  type: 'press',
184
184
  pointerType,
185
- target: originalEvent.currentTarget as HTMLElement,
185
+ target: originalEvent.currentTarget as Element,
186
186
  shiftKey: originalEvent.shiftKey,
187
187
  metaKey: originalEvent.metaKey,
188
188
  ctrlKey: originalEvent.ctrlKey,
@@ -201,7 +201,7 @@ export function usePress(props: PressHookProps): PressResult {
201
201
  onPressUp({
202
202
  type: 'pressup',
203
203
  pointerType,
204
- target: originalEvent.currentTarget as HTMLElement,
204
+ target: originalEvent.currentTarget as Element,
205
205
  shiftKey: originalEvent.shiftKey,
206
206
  metaKey: originalEvent.metaKey,
207
207
  ctrlKey: originalEvent.ctrlKey,
@@ -226,10 +226,10 @@ export function usePress(props: PressHookProps): PressResult {
226
226
  }
227
227
  };
228
228
 
229
- let pressProps: HTMLAttributes<HTMLElement> = {
229
+ let pressProps: DOMAttributes = {
230
230
  onKeyDown(e) {
231
- if (isValidKeyboardEvent(e.nativeEvent) && e.currentTarget.contains(e.target as HTMLElement)) {
232
- if (shouldPreventDefaultKeyboard(e.target as Element)) {
231
+ if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && e.currentTarget.contains(e.target as Element)) {
232
+ if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
233
233
  e.preventDefault();
234
234
  }
235
235
  e.stopPropagation();
@@ -238,7 +238,7 @@ export function usePress(props: PressHookProps): PressResult {
238
238
  // after which focus moved to the current element. Ignore these events and
239
239
  // only handle the first key down event.
240
240
  if (!state.isPressed && !e.repeat) {
241
- state.target = e.currentTarget as HTMLElement;
241
+ state.target = e.currentTarget;
242
242
  state.isPressed = true;
243
243
  triggerPressStart(e, 'keyboard');
244
244
 
@@ -246,15 +246,20 @@ export function usePress(props: PressHookProps): PressResult {
246
246
  // instead of the same element where the key down event occurred.
247
247
  addGlobalListener(document, 'keyup', onKeyUp, false);
248
248
  }
249
+ } else if (e.key === 'Enter' && isHTMLAnchorLink(e.currentTarget)) {
250
+ // If the target is a link, we won't have handled this above because we want the default
251
+ // browser behavior to open the link when pressing Enter. But we still need to prevent
252
+ // default so that elements above do not also handle it (e.g. table row).
253
+ e.stopPropagation();
249
254
  }
250
255
  },
251
256
  onKeyUp(e) {
252
- if (isValidKeyboardEvent(e.nativeEvent) && !e.repeat && e.currentTarget.contains(e.target as HTMLElement)) {
257
+ if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && !e.repeat && e.currentTarget.contains(e.target as Element)) {
253
258
  triggerPressUp(createEvent(state.target, e), 'keyboard');
254
259
  }
255
260
  },
256
261
  onClick(e) {
257
- if (e && !e.currentTarget.contains(e.target as HTMLElement)) {
262
+ if (e && !e.currentTarget.contains(e.target as Element)) {
258
263
  return;
259
264
  }
260
265
 
@@ -284,20 +289,20 @@ export function usePress(props: PressHookProps): PressResult {
284
289
  };
285
290
 
286
291
  let onKeyUp = (e: KeyboardEvent) => {
287
- if (state.isPressed && isValidKeyboardEvent(e)) {
288
- if (shouldPreventDefaultKeyboard(e.target as Element)) {
292
+ if (state.isPressed && isValidKeyboardEvent(e, state.target)) {
293
+ if (shouldPreventDefaultKeyboard(e.target as Element, e.key)) {
289
294
  e.preventDefault();
290
295
  }
291
296
  e.stopPropagation();
292
297
 
293
298
  state.isPressed = false;
294
- let target = e.target as HTMLElement;
299
+ let target = e.target as Element;
295
300
  triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
296
301
  removeAllGlobalListeners();
297
302
 
298
303
  // If the target is a link, trigger the click method to open the URL,
299
304
  // but defer triggering pressEnd until onClick event handler.
300
- if (state.target.contains(target) && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
305
+ if (state.target instanceof HTMLElement && state.target.contains(target) && (isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link')) {
301
306
  state.target.click();
302
307
  }
303
308
  }
@@ -306,7 +311,7 @@ export function usePress(props: PressHookProps): PressResult {
306
311
  if (typeof PointerEvent !== 'undefined') {
307
312
  pressProps.onPointerDown = (e) => {
308
313
  // Only handle left clicks, and ignore events that bubbled through portals.
309
- if (e.button !== 0 || !e.currentTarget.contains(e.target as HTMLElement)) {
314
+ if (e.button !== 0 || !e.currentTarget.contains(e.target as Element)) {
310
315
  return;
311
316
  }
312
317
 
@@ -321,7 +326,7 @@ export function usePress(props: PressHookProps): PressResult {
321
326
 
322
327
  // Due to browser inconsistencies, especially on mobile browsers, we prevent
323
328
  // default on pointer down and handle focusing the pressable element ourselves.
324
- if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
329
+ if (shouldPreventDefault(e.currentTarget as Element)) {
325
330
  e.preventDefault();
326
331
  }
327
332
 
@@ -351,7 +356,7 @@ export function usePress(props: PressHookProps): PressResult {
351
356
  };
352
357
 
353
358
  pressProps.onMouseDown = (e) => {
354
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
359
+ if (!e.currentTarget.contains(e.target as Element)) {
355
360
  return;
356
361
  }
357
362
 
@@ -359,7 +364,7 @@ export function usePress(props: PressHookProps): PressResult {
359
364
  // Chrome and Firefox on touch Windows devices require mouse down events
360
365
  // to be canceled in addition to pointer events, or an extra asynchronous
361
366
  // focus event will be fired.
362
- if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
367
+ if (shouldPreventDefault(e.currentTarget as Element)) {
363
368
  e.preventDefault();
364
369
  }
365
370
 
@@ -369,7 +374,7 @@ export function usePress(props: PressHookProps): PressResult {
369
374
 
370
375
  pressProps.onPointerUp = (e) => {
371
376
  // iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
372
- if (!e.currentTarget.contains(e.target as HTMLElement) || state.pointerType === 'virtual') {
377
+ if (!e.currentTarget.contains(e.target as Element) || state.pointerType === 'virtual') {
373
378
  return;
374
379
  }
375
380
 
@@ -427,7 +432,7 @@ export function usePress(props: PressHookProps): PressResult {
427
432
  };
428
433
 
429
434
  pressProps.onDragStart = (e) => {
430
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
435
+ if (!e.currentTarget.contains(e.target as Element)) {
431
436
  return;
432
437
  }
433
438
 
@@ -437,13 +442,13 @@ export function usePress(props: PressHookProps): PressResult {
437
442
  } else {
438
443
  pressProps.onMouseDown = (e) => {
439
444
  // Only handle left clicks
440
- if (e.button !== 0 || !e.currentTarget.contains(e.target as HTMLElement)) {
445
+ if (e.button !== 0 || !e.currentTarget.contains(e.target as Element)) {
441
446
  return;
442
447
  }
443
448
 
444
449
  // Due to browser inconsistencies, especially on mobile browsers, we prevent
445
450
  // default on mouse down and handle focusing the pressable element ourselves.
446
- if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
451
+ if (shouldPreventDefault(e.currentTarget)) {
447
452
  e.preventDefault();
448
453
  }
449
454
 
@@ -467,7 +472,7 @@ export function usePress(props: PressHookProps): PressResult {
467
472
  };
468
473
 
469
474
  pressProps.onMouseEnter = (e) => {
470
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
475
+ if (!e.currentTarget.contains(e.target as Element)) {
471
476
  return;
472
477
  }
473
478
 
@@ -479,7 +484,7 @@ export function usePress(props: PressHookProps): PressResult {
479
484
  };
480
485
 
481
486
  pressProps.onMouseLeave = (e) => {
482
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
487
+ if (!e.currentTarget.contains(e.target as Element)) {
483
488
  return;
484
489
  }
485
490
 
@@ -494,7 +499,7 @@ export function usePress(props: PressHookProps): PressResult {
494
499
  };
495
500
 
496
501
  pressProps.onMouseUp = (e) => {
497
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
502
+ if (!e.currentTarget.contains(e.target as Element)) {
498
503
  return;
499
504
  }
500
505
 
@@ -527,7 +532,7 @@ export function usePress(props: PressHookProps): PressResult {
527
532
  };
528
533
 
529
534
  pressProps.onTouchStart = (e) => {
530
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
535
+ if (!e.currentTarget.contains(e.target as Element)) {
531
536
  return;
532
537
  }
533
538
 
@@ -559,7 +564,7 @@ export function usePress(props: PressHookProps): PressResult {
559
564
  };
560
565
 
561
566
  pressProps.onTouchMove = (e) => {
562
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
567
+ if (!e.currentTarget.contains(e.target as Element)) {
563
568
  return;
564
569
  }
565
570
 
@@ -584,7 +589,7 @@ export function usePress(props: PressHookProps): PressResult {
584
589
  };
585
590
 
586
591
  pressProps.onTouchEnd = (e) => {
587
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
592
+ if (!e.currentTarget.contains(e.target as Element)) {
588
593
  return;
589
594
  }
590
595
 
@@ -612,7 +617,7 @@ export function usePress(props: PressHookProps): PressResult {
612
617
  };
613
618
 
614
619
  pressProps.onTouchCancel = (e) => {
615
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
620
+ if (!e.currentTarget.contains(e.target as Element)) {
616
621
  return;
617
622
  }
618
623
 
@@ -623,7 +628,7 @@ export function usePress(props: PressHookProps): PressResult {
623
628
  };
624
629
 
625
630
  let onScroll = (e: Event) => {
626
- if (state.isPressed && (e.target as HTMLElement).contains(state.target)) {
631
+ if (state.isPressed && (e.target as Element).contains(state.target)) {
627
632
  cancel({
628
633
  currentTarget: state.target,
629
634
  shiftKey: false,
@@ -635,7 +640,7 @@ export function usePress(props: PressHookProps): PressResult {
635
640
  };
636
641
 
637
642
  pressProps.onDragStart = (e) => {
638
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
643
+ if (!e.currentTarget.contains(e.target as Element)) {
639
644
  return;
640
645
  }
641
646
 
@@ -662,22 +667,21 @@ export function usePress(props: PressHookProps): PressResult {
662
667
  };
663
668
  }
664
669
 
665
- function isHTMLAnchorLink(target: HTMLElement): boolean {
670
+ function isHTMLAnchorLink(target: Element): boolean {
666
671
  return target.tagName === 'A' && target.hasAttribute('href');
667
672
  }
668
673
 
669
- function isValidKeyboardEvent(event: KeyboardEvent): boolean {
670
- const {key, code, target} = event;
671
- const element = target as HTMLElement;
672
- const {tagName, isContentEditable} = element;
674
+ function isValidKeyboardEvent(event: KeyboardEvent, currentTarget: Element): boolean {
675
+ const {key, code} = event;
676
+ const element = currentTarget as HTMLElement;
673
677
  const role = element.getAttribute('role');
674
678
  // Accessibility for keyboards. Space and Enter only.
675
679
  // "Spacebar" is for IE 11
676
680
  return (
677
681
  (key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') &&
678
- (tagName !== 'INPUT' &&
679
- tagName !== 'TEXTAREA' &&
680
- isContentEditable !== true) &&
682
+ !((element instanceof HTMLInputElement && !isValidInputKey(element, key)) ||
683
+ element instanceof HTMLTextAreaElement ||
684
+ element.isContentEditable) &&
681
685
  // A link with a valid href should be handled natively,
682
686
  // unless it also has role='button' and was triggered using Space.
683
687
  (!isHTMLAnchorLink(element) || (role === 'button' && key !== 'Enter')) &&
@@ -708,7 +712,7 @@ function getTouchById(
708
712
  return null;
709
713
  }
710
714
 
711
- function createEvent(target: HTMLElement, e: EventBase): EventBase {
715
+ function createEvent(target: FocusableElement, e: EventBase): EventBase {
712
716
  return {
713
717
  currentTarget: target,
714
718
  shiftKey: e.shiftKey,
@@ -758,19 +762,46 @@ function areRectanglesOverlapping(a: Rect, b: Rect) {
758
762
  return true;
759
763
  }
760
764
 
761
- function isOverTarget(point: EventPoint, target: HTMLElement) {
765
+ function isOverTarget(point: EventPoint, target: Element) {
762
766
  let rect = target.getBoundingClientRect();
763
767
  let pointRect = getPointClientRect(point);
764
768
  return areRectanglesOverlapping(rect, pointRect);
765
769
  }
766
770
 
767
- function shouldPreventDefault(target: HTMLElement) {
771
+ function shouldPreventDefault(target: Element) {
768
772
  // We cannot prevent default if the target is a draggable element.
769
- return !target.draggable;
773
+ return !(target instanceof HTMLElement) || !target.draggable;
774
+ }
775
+
776
+ function shouldPreventDefaultKeyboard(target: Element, key: string) {
777
+ if (target instanceof HTMLInputElement) {
778
+ return !isValidInputKey(target, key);
779
+ }
780
+
781
+ if (target instanceof HTMLButtonElement) {
782
+ return target.type !== 'submit';
783
+ }
784
+
785
+ return true;
770
786
  }
771
787
 
772
- function shouldPreventDefaultKeyboard(target: Element) {
773
- return !((target.tagName === 'INPUT' || target.tagName === 'BUTTON') && (target as HTMLButtonElement | HTMLInputElement).type === 'submit');
788
+ const nonTextInputTypes = new Set([
789
+ 'checkbox',
790
+ 'radio',
791
+ 'range',
792
+ 'color',
793
+ 'file',
794
+ 'image',
795
+ 'button',
796
+ 'submit',
797
+ 'reset'
798
+ ]);
799
+
800
+ function isValidInputKey(target: HTMLInputElement, key: string) {
801
+ // Only space should toggle checkboxes and radios, not enter.
802
+ return target.type === 'checkbox' || target.type === 'radio'
803
+ ? key === ' '
804
+ : nonTextInputTypes.has(target.type);
774
805
  }
775
806
 
776
807
  function isVirtualPointerEvent(event: PointerEvent) {