-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathcontext-connect.ts
160 lines (141 loc) · 4.57 KB
/
context-connect.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
* External dependencies
*/
import type { ForwardedRef, ReactChild, ReactNode } from 'react';
/**
* WordPress dependencies
*/
import { forwardRef } from '@wordpress/element';
import warn from '@wordpress/warning';
/**
* Internal dependencies
*/
import { CONNECT_STATIC_NAMESPACE } from './constants';
import { getStyledClassNameFromKey } from './get-styled-class-name-from-key';
import type { WordPressComponentFromProps } from '.';
type AcceptsTwoArgs<
F extends ( ...args: any ) => any,
ErrorMessage = never
> = Parameters< F >[ 'length' ] extends 2 ? {} : ErrorMessage;
type ContextConnectOptions = {
forwardsRef?: boolean;
};
/**
* Forwards ref (React.ForwardRef) and "Connects" (or registers) a component
* within the Context system under a specified namespace.
*
* @param Component The component to register into the Context system.
* @param namespace The namespace to register the component under.
* @return The connected WordPressComponent
*/
export function contextConnect<
C extends ( props: any, ref: ForwardedRef< any > ) => JSX.Element | null
>(
Component: C &
AcceptsTwoArgs<
C,
'Warning: Your component function does not take a ref as the second argument. Did you mean to use `contextConnectWithoutRef`?'
>,
namespace: string
) {
return _contextConnect( Component, namespace, { forwardsRef: true } );
}
/**
* "Connects" (or registers) a component within the Context system under a specified namespace.
* Does not forward a ref.
*
* @param Component The component to register into the Context system.
* @param namespace The namespace to register the component under.
* @return The connected WordPressComponent
*/
export function contextConnectWithoutRef< P >(
Component: ( props: P ) => JSX.Element | null,
namespace: string
) {
return _contextConnect( Component, namespace );
}
// This is an (experimental) evolution of the initial connect() HOC.
// The hope is that we can improve render performance by removing functional
// component wrappers.
function _contextConnect<
C extends ( props: any, ref: ForwardedRef< any > ) => JSX.Element | null,
O extends ContextConnectOptions
>(
Component: C,
namespace: string,
options?: O
): WordPressComponentFromProps<
Parameters< C >[ 0 ],
O[ 'forwardsRef' ] extends true ? true : false
> {
const WrappedComponent = options?.forwardsRef
? forwardRef< any, Parameters< C >[ 0 ] >( Component )
: Component;
if ( typeof namespace === 'undefined' ) {
warn( 'contextConnect: Please provide a namespace' );
}
// @ts-expect-error internal property
let mergedNamespace = WrappedComponent[ CONNECT_STATIC_NAMESPACE ] || [
namespace,
];
/**
* Consolidate (merge) namespaces before attaching it to the WrappedComponent.
*/
if ( Array.isArray( namespace ) ) {
mergedNamespace = [ ...mergedNamespace, ...namespace ];
}
if ( typeof namespace === 'string' ) {
mergedNamespace = [ ...mergedNamespace, namespace ];
}
// @ts-expect-error We can't rely on inferred types here because of the
// `as` prop polymorphism we're handling in https://github.com/WordPress/gutenberg/blob/9620bae6fef4fde7cc2b7833f416e240207cda29/packages/components/src/ui/context/wordpress-component.ts#L32-L33
return Object.assign( WrappedComponent, {
[ CONNECT_STATIC_NAMESPACE ]: [ ...new Set( mergedNamespace ) ],
displayName: namespace,
selector: `.${ getStyledClassNameFromKey( namespace ) }`,
} );
}
/**
* Attempts to retrieve the connected namespace from a component.
*
* @param Component The component to retrieve a namespace from.
* @return The connected namespaces.
*/
export function getConnectNamespace(
Component: ReactChild | undefined | {}
): string[] {
if ( ! Component ) return [];
let namespaces = [];
// @ts-ignore internal property
if ( Component[ CONNECT_STATIC_NAMESPACE ] ) {
// @ts-ignore internal property
namespaces = Component[ CONNECT_STATIC_NAMESPACE ];
}
// @ts-ignore
if ( Component.type && Component.type[ CONNECT_STATIC_NAMESPACE ] ) {
// @ts-ignore
namespaces = Component.type[ CONNECT_STATIC_NAMESPACE ];
}
return namespaces;
}
/**
* Checks to see if a component is connected within the Context system.
*
* @param Component The component to retrieve a namespace from.
* @param match The namespace to check.
*/
export function hasConnectNamespace(
Component: ReactNode,
match: string[] | string
): boolean {
if ( ! Component ) return false;
if ( typeof match === 'string' ) {
return getConnectNamespace( Component ).includes( match );
}
if ( Array.isArray( match ) ) {
return match.some( ( result ) =>
getConnectNamespace( Component ).includes( result )
);
}
return false;
}