@wordpress/compose 8.1.0 → 8.1.1
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/CHANGELOG.md +20 -1
- package/README.md +5 -5
- package/build/higher-order/pure/index.cjs +15 -0
- package/build/higher-order/pure/index.cjs.map +3 -3
- package/build/hooks/use-media-query/index.cjs +2 -2
- package/build/hooks/use-media-query/index.cjs.map +2 -2
- package/build/hooks/use-merge-refs/index.cjs +28 -8
- package/build/hooks/use-merge-refs/index.cjs.map +2 -2
- package/build/hooks/use-viewport-match/index.cjs +1 -1
- package/build/hooks/use-viewport-match/index.cjs.map +2 -2
- package/build-module/higher-order/pure/index.mjs +5 -0
- package/build-module/higher-order/pure/index.mjs.map +2 -2
- package/build-module/hooks/use-media-query/index.mjs +2 -2
- package/build-module/hooks/use-media-query/index.mjs.map +2 -2
- package/build-module/hooks/use-merge-refs/index.mjs +28 -8
- package/build-module/hooks/use-merge-refs/index.mjs.map +2 -2
- package/build-module/hooks/use-viewport-match/index.mjs +1 -1
- package/build-module/hooks/use-viewport-match/index.mjs.map +2 -2
- package/build-types/higher-order/pure/index.d.ts.map +1 -1
- package/build-types/hooks/use-media-query/index.d.ts +1 -1
- package/build-types/hooks/use-media-query/index.d.ts.map +1 -1
- package/build-types/hooks/use-merge-refs/index.d.ts +7 -5
- package/build-types/hooks/use-merge-refs/index.d.ts.map +1 -1
- package/build-types/hooks/use-viewport-match/index.d.ts +1 -1
- package/build-types/hooks/use-viewport-match/index.d.ts.map +1 -1
- package/package.json +16 -12
- package/src/higher-order/pure/index.tsx +6 -0
- package/src/higher-order/pure/test/index.js +14 -81
- package/src/hooks/use-drop-zone/README.md +1 -1
- package/src/hooks/use-media-query/index.ts +10 -3
- package/src/hooks/use-media-query/test/ssr.js +47 -0
- package/src/hooks/use-merge-refs/index.ts +49 -18
- package/src/hooks/use-merge-refs/test/index.js +288 -0
- package/src/hooks/use-viewport-match/index.js +8 -2
- package/src/higher-order/with-network-connectivity/README.md +0 -20
- package/src/higher-order/with-network-connectivity/index.native.js +0 -19
- package/src/higher-order/with-preferred-color-scheme/index.native.js +0 -40
- package/src/hooks/use-constrained-tabbing/index.native.js +0 -14
- package/src/hooks/use-focus-outside/index.native.js +0 -170
- package/src/hooks/use-keyboard-shortcut/index.native.js +0 -2
- package/src/hooks/use-network-connectivity/index.native.js +0 -59
- package/src/hooks/use-network-connectivity/test/index.native.js +0 -87
- package/src/hooks/use-preferred-color-scheme/index.android.js +0 -30
- package/src/hooks/use-preferred-color-scheme/index.ios.js +0 -13
- package/src/hooks/use-preferred-color-scheme-style/index.native.js +0 -33
- package/src/hooks/use-resize-observer/index.native.js +0 -1
- package/src/hooks/use-resize-observer/legacy/index.native.js +0 -59
- package/src/hooks/use-resize-observer/legacy/test/index.native.js +0 -46
- package/src/index.native.js +0 -44
|
@@ -345,3 +345,291 @@ describe( 'useMergeRefs', () => {
|
|
|
345
345
|
] );
|
|
346
346
|
} );
|
|
347
347
|
} );
|
|
348
|
+
|
|
349
|
+
describe( 'useMergeRefs with cleanup-returning ref callbacks', () => {
|
|
350
|
+
// Setup
|
|
351
|
+
// =====
|
|
352
|
+
//
|
|
353
|
+
// Same structure as the suite above, but the ref callbacks return a
|
|
354
|
+
// cleanup function. React 19 (and `useMergeRefs`) invokes the cleanup at
|
|
355
|
+
// teardown instead of calling the callback with `null`. The cleanup pushes
|
|
356
|
+
// the string `'cleanup'` into the same history array, so each call site's
|
|
357
|
+
// full lifecycle is captured as `[ node, 'cleanup', newNode, 'cleanup' ]`.
|
|
358
|
+
// The strict invariant is that `null` never appears in the history of a
|
|
359
|
+
// cleanup-returning ref.
|
|
360
|
+
|
|
361
|
+
function renderCallback( args ) {
|
|
362
|
+
renderCallback.history.push( args );
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
renderCallback.history = [];
|
|
366
|
+
|
|
367
|
+
function MergedRefs( {
|
|
368
|
+
count,
|
|
369
|
+
tagName: TagName = 'ul',
|
|
370
|
+
disable1,
|
|
371
|
+
disable2,
|
|
372
|
+
} ) {
|
|
373
|
+
function refCallback1( value ) {
|
|
374
|
+
refCallback1.history.push( value );
|
|
375
|
+
return () => {
|
|
376
|
+
refCallback1.history.push( 'cleanup' );
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
refCallback1.history = [];
|
|
381
|
+
|
|
382
|
+
function refCallback2( value ) {
|
|
383
|
+
refCallback2.history.push( value );
|
|
384
|
+
return () => {
|
|
385
|
+
refCallback2.history.push( 'cleanup' );
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
refCallback2.history = [];
|
|
390
|
+
|
|
391
|
+
renderCallback( [ refCallback1.history, refCallback2.history ] );
|
|
392
|
+
|
|
393
|
+
const ref1 = useCallback( refCallback1, [] );
|
|
394
|
+
const ref2 = useCallback( refCallback2, [ count ] );
|
|
395
|
+
const mergedRefs = useMergeRefs( [
|
|
396
|
+
! disable1 && ref1,
|
|
397
|
+
! disable2 && ref2,
|
|
398
|
+
] );
|
|
399
|
+
|
|
400
|
+
return <TagName ref={ mergedRefs } />;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
afterEach( () => {
|
|
404
|
+
renderCallback.history = [];
|
|
405
|
+
} );
|
|
406
|
+
|
|
407
|
+
it( 'should invoke cleanup on unmount instead of calling ref with null', () => {
|
|
408
|
+
const { unmount } = render( <MergedRefs /> );
|
|
409
|
+
|
|
410
|
+
const originalElement = screen.getByRole( 'list' );
|
|
411
|
+
|
|
412
|
+
expect( renderCallback.history ).toEqual( [
|
|
413
|
+
[ [ originalElement ], [ originalElement ] ],
|
|
414
|
+
] );
|
|
415
|
+
|
|
416
|
+
unmount();
|
|
417
|
+
|
|
418
|
+
// Cleanups run; refs are NOT called with `null`.
|
|
419
|
+
expect( renderCallback.history ).toEqual( [
|
|
420
|
+
[
|
|
421
|
+
[ originalElement, 'cleanup' ],
|
|
422
|
+
[ originalElement, 'cleanup' ],
|
|
423
|
+
],
|
|
424
|
+
] );
|
|
425
|
+
} );
|
|
426
|
+
|
|
427
|
+
it( 'should invoke cleanup on node change instead of calling ref with null', () => {
|
|
428
|
+
const { rerender, unmount } = render( <MergedRefs /> );
|
|
429
|
+
|
|
430
|
+
const originalElement = screen.getByRole( 'list' );
|
|
431
|
+
|
|
432
|
+
rerender( <MergedRefs tagName="button" /> );
|
|
433
|
+
|
|
434
|
+
const newElement = screen.getByRole( 'button' );
|
|
435
|
+
|
|
436
|
+
// Node change triggers the outer callback first with `null` (which
|
|
437
|
+
// runs the cleanups) and then with the new node (which sets up new
|
|
438
|
+
// cleanups).
|
|
439
|
+
expect( renderCallback.history ).toEqual( [
|
|
440
|
+
[
|
|
441
|
+
[ originalElement, 'cleanup', newElement ],
|
|
442
|
+
[ originalElement, 'cleanup', newElement ],
|
|
443
|
+
],
|
|
444
|
+
[ [], [] ],
|
|
445
|
+
] );
|
|
446
|
+
|
|
447
|
+
unmount();
|
|
448
|
+
|
|
449
|
+
expect( renderCallback.history ).toEqual( [
|
|
450
|
+
[
|
|
451
|
+
[ originalElement, 'cleanup', newElement, 'cleanup' ],
|
|
452
|
+
[ originalElement, 'cleanup', newElement, 'cleanup' ],
|
|
453
|
+
],
|
|
454
|
+
[ [], [] ],
|
|
455
|
+
] );
|
|
456
|
+
} );
|
|
457
|
+
|
|
458
|
+
it( 'should invoke cleanup on dependency change instead of calling ref with null', () => {
|
|
459
|
+
const { rerender, unmount } = render( <MergedRefs count={ 1 } /> );
|
|
460
|
+
|
|
461
|
+
const originalElement = screen.getByRole( 'list' );
|
|
462
|
+
|
|
463
|
+
expect( renderCallback.history ).toEqual( [
|
|
464
|
+
[ [ originalElement ], [ originalElement ] ],
|
|
465
|
+
] );
|
|
466
|
+
|
|
467
|
+
rerender( <MergedRefs count={ 2 } /> );
|
|
468
|
+
|
|
469
|
+
// Only ref2's deps changed. Its previous cleanup runs and the new ref
|
|
470
|
+
// callback is called with the still-attached node. ref1 is untouched.
|
|
471
|
+
expect( renderCallback.history ).toEqual( [
|
|
472
|
+
[ [ originalElement ], [ originalElement, 'cleanup' ] ],
|
|
473
|
+
[ [], [ originalElement ] ],
|
|
474
|
+
] );
|
|
475
|
+
|
|
476
|
+
unmount();
|
|
477
|
+
|
|
478
|
+
expect( renderCallback.history ).toEqual( [
|
|
479
|
+
[
|
|
480
|
+
[ originalElement, 'cleanup' ],
|
|
481
|
+
[ originalElement, 'cleanup' ],
|
|
482
|
+
],
|
|
483
|
+
[ [], [ originalElement, 'cleanup' ] ],
|
|
484
|
+
] );
|
|
485
|
+
} );
|
|
486
|
+
|
|
487
|
+
it( 'should invoke cleanup on simultaneous node and dependency change', () => {
|
|
488
|
+
const { rerender, unmount } = render( <MergedRefs count={ 1 } /> );
|
|
489
|
+
|
|
490
|
+
const originalElement = screen.getByRole( 'list' );
|
|
491
|
+
|
|
492
|
+
rerender( <MergedRefs count={ 2 } tagName="button" /> );
|
|
493
|
+
|
|
494
|
+
const newElement = screen.getByRole( 'button' );
|
|
495
|
+
|
|
496
|
+
// Outer callback's null/new-node pair runs both cleanups and sets
|
|
497
|
+
// up the new ones (ref1 reused, ref2 has a new identity).
|
|
498
|
+
expect( renderCallback.history ).toEqual( [
|
|
499
|
+
[
|
|
500
|
+
[ originalElement, 'cleanup', newElement ],
|
|
501
|
+
[ originalElement, 'cleanup' ],
|
|
502
|
+
],
|
|
503
|
+
[ [], [ newElement ] ],
|
|
504
|
+
] );
|
|
505
|
+
|
|
506
|
+
unmount();
|
|
507
|
+
|
|
508
|
+
expect( renderCallback.history ).toEqual( [
|
|
509
|
+
[
|
|
510
|
+
[ originalElement, 'cleanup', newElement, 'cleanup' ],
|
|
511
|
+
[ originalElement, 'cleanup' ],
|
|
512
|
+
],
|
|
513
|
+
[ [], [ newElement, 'cleanup' ] ],
|
|
514
|
+
] );
|
|
515
|
+
} );
|
|
516
|
+
|
|
517
|
+
it( 'should invoke cleanup when a ref becomes disabled', () => {
|
|
518
|
+
const { rerender, unmount } = render( <MergedRefs /> );
|
|
519
|
+
|
|
520
|
+
const originalElement = screen.getByRole( 'list' );
|
|
521
|
+
|
|
522
|
+
rerender( <MergedRefs disable1 /> );
|
|
523
|
+
|
|
524
|
+
// ref1 is disabled: its stored cleanup must fire. ref2 is unchanged.
|
|
525
|
+
expect( renderCallback.history ).toEqual( [
|
|
526
|
+
[ [ originalElement, 'cleanup' ], [ originalElement ] ],
|
|
527
|
+
[ [], [] ],
|
|
528
|
+
] );
|
|
529
|
+
|
|
530
|
+
unmount();
|
|
531
|
+
|
|
532
|
+
expect( renderCallback.history ).toEqual( [
|
|
533
|
+
[
|
|
534
|
+
[ originalElement, 'cleanup' ],
|
|
535
|
+
[ originalElement, 'cleanup' ],
|
|
536
|
+
],
|
|
537
|
+
[ [], [] ],
|
|
538
|
+
] );
|
|
539
|
+
} );
|
|
540
|
+
|
|
541
|
+
it( 'should support mixing a cleanup-returning ref with a void-returning ref', () => {
|
|
542
|
+
function MergedRefsMixed() {
|
|
543
|
+
function refCleanup( value ) {
|
|
544
|
+
refCleanup.history.push( value );
|
|
545
|
+
return () => {
|
|
546
|
+
refCleanup.history.push( 'cleanup' );
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
refCleanup.history = [];
|
|
551
|
+
|
|
552
|
+
function refVoid( value ) {
|
|
553
|
+
refVoid.history.push( value );
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
refVoid.history = [];
|
|
557
|
+
|
|
558
|
+
renderCallback( [ refCleanup.history, refVoid.history ] );
|
|
559
|
+
|
|
560
|
+
const r1 = useCallback( refCleanup, [] );
|
|
561
|
+
const r2 = useCallback( refVoid, [] );
|
|
562
|
+
const merged = useMergeRefs( [ r1, r2 ] );
|
|
563
|
+
|
|
564
|
+
return <ul ref={ merged } />;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const { unmount } = render( <MergedRefsMixed /> );
|
|
568
|
+
|
|
569
|
+
const el = screen.getByRole( 'list' );
|
|
570
|
+
|
|
571
|
+
expect( renderCallback.history ).toEqual( [ [ [ el ], [ el ] ] ] );
|
|
572
|
+
|
|
573
|
+
unmount();
|
|
574
|
+
|
|
575
|
+
// Cleanup ref sees the cleanup; void ref still sees null. Independent.
|
|
576
|
+
expect( renderCallback.history ).toEqual( [
|
|
577
|
+
[
|
|
578
|
+
[ el, 'cleanup' ],
|
|
579
|
+
[ el, null ],
|
|
580
|
+
],
|
|
581
|
+
] );
|
|
582
|
+
} );
|
|
583
|
+
|
|
584
|
+
it( 'should support mixing an object ref with a cleanup-returning ref', () => {
|
|
585
|
+
const objectRef = { current: null };
|
|
586
|
+
|
|
587
|
+
function MergedRefsMixed( { tagName: TagName = 'ul' } ) {
|
|
588
|
+
function refCleanup( value ) {
|
|
589
|
+
refCleanup.history.push( value );
|
|
590
|
+
return () => {
|
|
591
|
+
refCleanup.history.push( 'cleanup' );
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
refCleanup.history = [];
|
|
596
|
+
|
|
597
|
+
renderCallback( [ refCleanup.history ] );
|
|
598
|
+
|
|
599
|
+
const r = useCallback( refCleanup, [] );
|
|
600
|
+
const merged = useMergeRefs( [ objectRef, r ] );
|
|
601
|
+
|
|
602
|
+
return <TagName ref={ merged } />;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const { rerender, unmount } = render( <MergedRefsMixed /> );
|
|
606
|
+
|
|
607
|
+
const originalElement = screen.getByRole( 'list' );
|
|
608
|
+
|
|
609
|
+
// Object ref's .current is set; cleanup ref is called with the node.
|
|
610
|
+
expect( objectRef.current ).toBe( originalElement );
|
|
611
|
+
expect( renderCallback.history ).toEqual( [ [ [ originalElement ] ] ] );
|
|
612
|
+
|
|
613
|
+
rerender( <MergedRefsMixed tagName="button" /> );
|
|
614
|
+
|
|
615
|
+
const newElement = screen.getByRole( 'button' );
|
|
616
|
+
|
|
617
|
+
// Node change: object ref retargets, callback ref's cleanup fires
|
|
618
|
+
// and the callback is re-invoked with the new node.
|
|
619
|
+
expect( objectRef.current ).toBe( newElement );
|
|
620
|
+
expect( renderCallback.history ).toEqual( [
|
|
621
|
+
[ [ originalElement, 'cleanup', newElement ] ],
|
|
622
|
+
[ [] ],
|
|
623
|
+
] );
|
|
624
|
+
|
|
625
|
+
unmount();
|
|
626
|
+
|
|
627
|
+
// Object ref nulled via the fallback assignRef path; callback ref
|
|
628
|
+
// sees its cleanup, never null.
|
|
629
|
+
expect( objectRef.current ).toBe( null );
|
|
630
|
+
expect( renderCallback.history ).toEqual( [
|
|
631
|
+
[ [ originalElement, 'cleanup', newElement, 'cleanup' ] ],
|
|
632
|
+
[ [] ],
|
|
633
|
+
] );
|
|
634
|
+
} );
|
|
635
|
+
} );
|
|
@@ -64,7 +64,7 @@ ViewportMatchWidthContext.displayName = 'ViewportMatchWidthContext';
|
|
|
64
64
|
*
|
|
65
65
|
* @param {WPBreakpoint} breakpoint Breakpoint size name.
|
|
66
66
|
* @param {WPViewportOperator} [operator=">="] Viewport operator.
|
|
67
|
-
* @param {Window}
|
|
67
|
+
* @param {Window|undefined} [view=window] Window instance in which to perform viewport matching.
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
70
70
|
*
|
|
@@ -75,7 +75,13 @@ ViewportMatchWidthContext.displayName = 'ViewportMatchWidthContext';
|
|
|
75
75
|
*
|
|
76
76
|
* @return {boolean} Whether viewport matches query.
|
|
77
77
|
*/
|
|
78
|
-
const useViewportMatch = (
|
|
78
|
+
const useViewportMatch = (
|
|
79
|
+
breakpoint,
|
|
80
|
+
operator = '>=',
|
|
81
|
+
// Resolve the default lazily so SSR (where `window` is undeclared) does not
|
|
82
|
+
// throw a ReferenceError when this default expression is evaluated.
|
|
83
|
+
view = typeof window !== 'undefined' ? window : undefined
|
|
84
|
+
) => {
|
|
79
85
|
const simulatedWidth = useContext( ViewportMatchWidthContext );
|
|
80
86
|
const mediaQuery =
|
|
81
87
|
! simulatedWidth &&
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# withNetworkConnectivity
|
|
2
|
-
|
|
3
|
-
`withNetworkConnectivity` provides a true/false mobile connectivity status based on the `useNetworkConnectivity` hook.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```jsx
|
|
8
|
-
/**
|
|
9
|
-
* WordPress dependencies
|
|
10
|
-
*/
|
|
11
|
-
import { withNetworkConnectivity } from '@wordpress/compose';
|
|
12
|
-
|
|
13
|
-
export class MyComponent extends Component {
|
|
14
|
-
if ( this.props.isConnected !== true ) {
|
|
15
|
-
console.log( 'You are currently offline.' )
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default withNetworkConnectivity( MyComponent )
|
|
20
|
-
```
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal dependencies
|
|
3
|
-
*/
|
|
4
|
-
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';
|
|
5
|
-
import useNetworkConnectivity from '../../hooks/use-network-connectivity';
|
|
6
|
-
|
|
7
|
-
const withNetworkConnectivity = createHigherOrderComponent(
|
|
8
|
-
( WrappedComponent ) => {
|
|
9
|
-
return ( props ) => {
|
|
10
|
-
const { isConnected } = useNetworkConnectivity();
|
|
11
|
-
return (
|
|
12
|
-
<WrappedComponent { ...props } isConnected={ isConnected } />
|
|
13
|
-
);
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
'withNetworkConnectivity'
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
export default withNetworkConnectivity;
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal dependencies
|
|
3
|
-
*/
|
|
4
|
-
import { createHigherOrderComponent } from '../../utils/create-higher-order-component';
|
|
5
|
-
import usePreferredColorScheme from '../../hooks/use-preferred-color-scheme';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* WordPress dependencies
|
|
9
|
-
*/
|
|
10
|
-
import { useCallback } from '@wordpress/element';
|
|
11
|
-
|
|
12
|
-
const withPreferredColorScheme = createHigherOrderComponent(
|
|
13
|
-
( WrappedComponent ) => ( props ) => {
|
|
14
|
-
const colorScheme = usePreferredColorScheme();
|
|
15
|
-
const isDarkMode = colorScheme === 'dark';
|
|
16
|
-
|
|
17
|
-
const getStyles = useCallback(
|
|
18
|
-
( lightStyles, darkStyles ) => {
|
|
19
|
-
const finalDarkStyles = {
|
|
20
|
-
...lightStyles,
|
|
21
|
-
...darkStyles,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
return isDarkMode ? finalDarkStyles : lightStyles;
|
|
25
|
-
},
|
|
26
|
-
[ isDarkMode ]
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<WrappedComponent
|
|
31
|
-
preferredColorScheme={ colorScheme }
|
|
32
|
-
getStylesFromColorScheme={ getStyles }
|
|
33
|
-
{ ...props }
|
|
34
|
-
/>
|
|
35
|
-
);
|
|
36
|
-
},
|
|
37
|
-
'withPreferredColorScheme'
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
export default withPreferredColorScheme;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WordPress dependencies
|
|
3
|
-
*/
|
|
4
|
-
import { useRef } from '@wordpress/element';
|
|
5
|
-
|
|
6
|
-
function useConstrainedTabbing() {
|
|
7
|
-
const ref = useRef();
|
|
8
|
-
|
|
9
|
-
// Do nothing on mobile as tabbing is not a mobile behavior.
|
|
10
|
-
|
|
11
|
-
return ref;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export default useConstrainedTabbing;
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WordPress dependencies
|
|
3
|
-
*/
|
|
4
|
-
import { useCallback, useEffect, useRef } from '@wordpress/element';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Input types which are classified as button types, for use in considering
|
|
8
|
-
* whether element is a (focus-normalized) button.
|
|
9
|
-
*
|
|
10
|
-
* @type {string[]}
|
|
11
|
-
*/
|
|
12
|
-
const INPUT_BUTTON_TYPES = [ 'button', 'submit' ];
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {HTMLButtonElement | HTMLLinkElement | HTMLInputElement} FocusNormalizedButton
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Returns true if the given element is a button element subject to focus
|
|
20
|
-
* normalization, or false otherwise.
|
|
21
|
-
*
|
|
22
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
|
|
23
|
-
*
|
|
24
|
-
* @param {EventTarget} eventTarget The target from a mouse or touch event.
|
|
25
|
-
*
|
|
26
|
-
* @return {eventTarget is FocusNormalizedButton} Whether element is a button.
|
|
27
|
-
*/
|
|
28
|
-
function isFocusNormalizedButton( eventTarget ) {
|
|
29
|
-
switch ( eventTarget.nodeName ) {
|
|
30
|
-
case 'A':
|
|
31
|
-
case 'BUTTON':
|
|
32
|
-
return true;
|
|
33
|
-
|
|
34
|
-
case 'INPUT':
|
|
35
|
-
return INPUT_BUTTON_TYPES.includes(
|
|
36
|
-
/** @type {HTMLInputElement} */ ( eventTarget ).type
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {React.SyntheticEvent} SyntheticEvent
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @callback EventCallback
|
|
49
|
-
* @param {SyntheticEvent} event input related event.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @typedef FocusOutsideReactElement
|
|
54
|
-
* @property {EventCallback} handleFocusOutside callback for a focus outside event.
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @typedef {React.MutableRefObject<FocusOutsideReactElement | undefined>} FocusOutsideRef
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @typedef {Object} FocusOutsideReturnValue
|
|
63
|
-
* @property {EventCallback} onFocus An event handler for focus events.
|
|
64
|
-
* @property {EventCallback} onBlur An event handler for blur events.
|
|
65
|
-
* @property {EventCallback} onMouseDown An event handler for mouse down events.
|
|
66
|
-
* @property {EventCallback} onMouseUp An event handler for mouse up events.
|
|
67
|
-
* @property {EventCallback} onTouchStart An event handler for touch start events.
|
|
68
|
-
* @property {EventCallback} onTouchEnd An event handler for touch end events.
|
|
69
|
-
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A react hook that can be used to check whether focus has moved outside the
|
|
73
|
-
* element the event handlers are bound to.
|
|
74
|
-
*
|
|
75
|
-
* @param {EventCallback} onFocusOutside A callback triggered when focus moves outside
|
|
76
|
-
* the element the event handlers are bound to.
|
|
77
|
-
*
|
|
78
|
-
* @return {FocusOutsideReturnValue} An object containing event handlers. Bind the event handlers
|
|
79
|
-
* to a wrapping element element to capture when focus moves
|
|
80
|
-
* outside that element.
|
|
81
|
-
*/
|
|
82
|
-
export default function useFocusOutside( onFocusOutside ) {
|
|
83
|
-
const currentOnFocusOutside = useRef( onFocusOutside );
|
|
84
|
-
useEffect( () => {
|
|
85
|
-
currentOnFocusOutside.current = onFocusOutside;
|
|
86
|
-
}, [ onFocusOutside ] );
|
|
87
|
-
|
|
88
|
-
const preventBlurCheck = useRef( false );
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @type {React.MutableRefObject<number | undefined>}
|
|
92
|
-
*/
|
|
93
|
-
const blurCheckTimeoutId = useRef();
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Cancel a blur check timeout.
|
|
97
|
-
*/
|
|
98
|
-
const cancelBlurCheck = useCallback( () => {
|
|
99
|
-
clearTimeout( blurCheckTimeoutId.current );
|
|
100
|
-
}, [] );
|
|
101
|
-
|
|
102
|
-
// Cancel blur checks on unmount.
|
|
103
|
-
useEffect( () => {
|
|
104
|
-
return () => cancelBlurCheck();
|
|
105
|
-
}, [] );
|
|
106
|
-
|
|
107
|
-
// Cancel a blur check if the callback or ref is no longer provided.
|
|
108
|
-
useEffect( () => {
|
|
109
|
-
if ( ! onFocusOutside ) {
|
|
110
|
-
cancelBlurCheck();
|
|
111
|
-
}
|
|
112
|
-
}, [ onFocusOutside, cancelBlurCheck ] );
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Handles a mousedown or mouseup event to respectively assign and
|
|
116
|
-
* unassign a flag for preventing blur check on button elements. Some
|
|
117
|
-
* browsers, namely Firefox and Safari, do not emit a focus event on
|
|
118
|
-
* button elements when clicked, while others do. The logic here
|
|
119
|
-
* intends to normalize this as treating click on buttons as focus.
|
|
120
|
-
*
|
|
121
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
|
|
122
|
-
*
|
|
123
|
-
* @param {SyntheticEvent} event Event for mousedown or mouseup.
|
|
124
|
-
*/
|
|
125
|
-
const normalizeButtonFocus = useCallback( ( event ) => {
|
|
126
|
-
const { type, target } = event;
|
|
127
|
-
const isInteractionEnd = [ 'mouseup', 'touchend' ].includes( type );
|
|
128
|
-
|
|
129
|
-
if ( isInteractionEnd ) {
|
|
130
|
-
preventBlurCheck.current = false;
|
|
131
|
-
} else if ( isFocusNormalizedButton( target ) ) {
|
|
132
|
-
preventBlurCheck.current = true;
|
|
133
|
-
}
|
|
134
|
-
}, [] );
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* A callback triggered when a blur event occurs on the element the handler
|
|
138
|
-
* is bound to.
|
|
139
|
-
*
|
|
140
|
-
* Calls the `onFocusOutside` callback in an immediate timeout if focus has
|
|
141
|
-
* move outside the bound element and is still within the document.
|
|
142
|
-
*
|
|
143
|
-
* @param {SyntheticEvent} event Blur event.
|
|
144
|
-
*/
|
|
145
|
-
const queueBlurCheck = useCallback( ( event ) => {
|
|
146
|
-
// React does not allow using an event reference asynchronously
|
|
147
|
-
// due to recycling behavior, except when explicitly persisted.
|
|
148
|
-
event.persist();
|
|
149
|
-
|
|
150
|
-
// Skip blur check if clicking button. See `normalizeButtonFocus`.
|
|
151
|
-
if ( preventBlurCheck.current ) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
blurCheckTimeoutId.current = setTimeout( () => {
|
|
156
|
-
if ( 'function' === typeof currentOnFocusOutside.current ) {
|
|
157
|
-
currentOnFocusOutside.current( event );
|
|
158
|
-
}
|
|
159
|
-
}, 0 );
|
|
160
|
-
}, [] );
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
onFocus: cancelBlurCheck,
|
|
164
|
-
onMouseDown: normalizeButtonFocus,
|
|
165
|
-
onMouseUp: normalizeButtonFocus,
|
|
166
|
-
onTouchStart: normalizeButtonFocus,
|
|
167
|
-
onTouchEnd: normalizeButtonFocus,
|
|
168
|
-
onBlur: queueBlurCheck,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WordPress dependencies
|
|
3
|
-
*/
|
|
4
|
-
import { useEffect, useState } from '@wordpress/element';
|
|
5
|
-
import {
|
|
6
|
-
requestConnectionStatus,
|
|
7
|
-
subscribeConnectionStatus,
|
|
8
|
-
} from '@wordpress/react-native-bridge';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {Object} NetworkInformation
|
|
12
|
-
*
|
|
13
|
-
* @property {boolean} [isConnected] Whether the device is connected to a network.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Returns the current network connectivity status provided by the native bridge.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
*
|
|
21
|
-
* ```jsx
|
|
22
|
-
* const { isConnected } = useNetworkConnectivity();
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @return {NetworkInformation} Network information.
|
|
26
|
-
*/
|
|
27
|
-
export default function useNetworkConnectivity() {
|
|
28
|
-
const [ isConnected, setIsConnected ] = useState( true );
|
|
29
|
-
|
|
30
|
-
useEffect( () => {
|
|
31
|
-
let isCurrent = true;
|
|
32
|
-
|
|
33
|
-
requestConnectionStatus( ( isBridgeConnected ) => {
|
|
34
|
-
if ( ! isCurrent ) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
setIsConnected( isBridgeConnected );
|
|
39
|
-
} );
|
|
40
|
-
|
|
41
|
-
return () => {
|
|
42
|
-
isCurrent = false;
|
|
43
|
-
};
|
|
44
|
-
}, [] );
|
|
45
|
-
|
|
46
|
-
useEffect( () => {
|
|
47
|
-
const subscription = subscribeConnectionStatus(
|
|
48
|
-
( { isConnected: isBridgeConnected } ) => {
|
|
49
|
-
setIsConnected( isBridgeConnected );
|
|
50
|
-
}
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
return () => {
|
|
54
|
-
subscription.remove();
|
|
55
|
-
};
|
|
56
|
-
}, [] );
|
|
57
|
-
|
|
58
|
-
return { isConnected };
|
|
59
|
-
}
|