diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index 6a56144118fe53..5dba744711f7e0 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Internal
+
+- `SlotFill`: Refactor SlotFill base ([#51385](https://github.com/WordPress/gutenberg/pull/51385)).
+
## 25.1.0 (2023-06-07)
### Enhancements
diff --git a/packages/components/src/slot-fill/context.js b/packages/components/src/slot-fill/context.js
deleted file mode 100644
index 0f7961a8ffd3b6..00000000000000
--- a/packages/components/src/slot-fill/context.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// @ts-nocheck
-/**
- * WordPress dependencies
- */
-import { createContext } from '@wordpress/element';
-
-export const SlotFillContext = createContext( {
- registerSlot: () => {},
- unregisterSlot: () => {},
- registerFill: () => {},
- unregisterFill: () => {},
- getSlot: () => {},
- getFills: () => {},
- subscribe: () => {},
-} );
-
-export default SlotFillContext;
diff --git a/packages/components/src/slot-fill/context.ts b/packages/components/src/slot-fill/context.ts
new file mode 100644
index 00000000000000..c4839462fbce0c
--- /dev/null
+++ b/packages/components/src/slot-fill/context.ts
@@ -0,0 +1,21 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext } from '@wordpress/element';
+/**
+ * Internal dependencies
+ */
+import type { BaseSlotFillContext } from './types';
+
+const initialValue: BaseSlotFillContext = {
+ registerSlot: () => {},
+ unregisterSlot: () => {},
+ registerFill: () => {},
+ unregisterFill: () => {},
+ getSlot: () => undefined,
+ getFills: () => [],
+ subscribe: () => () => {},
+};
+export const SlotFillContext = createContext( initialValue );
+
+export default SlotFillContext;
diff --git a/packages/components/src/slot-fill/fill.js b/packages/components/src/slot-fill/fill.ts
similarity index 93%
rename from packages/components/src/slot-fill/fill.js
rename to packages/components/src/slot-fill/fill.ts
index e7ff943df07bff..10e56fba9fd720 100644
--- a/packages/components/src/slot-fill/fill.js
+++ b/packages/components/src/slot-fill/fill.ts
@@ -10,8 +10,9 @@ import { useContext, useLayoutEffect, useRef } from '@wordpress/element';
*/
import SlotFillContext from './context';
import useSlot from './use-slot';
+import type { FillComponentProps } from './types';
-export default function Fill( { name, children } ) {
+export default function Fill( { name, children }: FillComponentProps ) {
const { registerFill, unregisterFill } = useContext( SlotFillContext );
const slot = useSlot( name );
diff --git a/packages/components/src/slot-fill/provider.js b/packages/components/src/slot-fill/provider.js
deleted file mode 100644
index 94c83c3c350111..00000000000000
--- a/packages/components/src/slot-fill/provider.js
+++ /dev/null
@@ -1,119 +0,0 @@
-// @ts-nocheck
-/**
- * WordPress dependencies
- */
-import { Component } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import SlotFillContext from './context';
-
-export default class SlotFillProvider extends Component {
- constructor() {
- super( ...arguments );
-
- this.registerSlot = this.registerSlot.bind( this );
- this.registerFill = this.registerFill.bind( this );
- this.unregisterSlot = this.unregisterSlot.bind( this );
- this.unregisterFill = this.unregisterFill.bind( this );
- this.getSlot = this.getSlot.bind( this );
- this.getFills = this.getFills.bind( this );
- this.subscribe = this.subscribe.bind( this );
-
- this.slots = {};
- this.fills = {};
- this.listeners = [];
- this.contextValue = {
- registerSlot: this.registerSlot,
- unregisterSlot: this.unregisterSlot,
- registerFill: this.registerFill,
- unregisterFill: this.unregisterFill,
- getSlot: this.getSlot,
- getFills: this.getFills,
- subscribe: this.subscribe,
- };
- }
-
- registerSlot( name, slot ) {
- const previousSlot = this.slots[ name ];
- this.slots[ name ] = slot;
- this.triggerListeners();
-
- // Sometimes the fills are registered after the initial render of slot
- // But before the registerSlot call, we need to rerender the slot.
- this.forceUpdateSlot( name );
-
- // If a new instance of a slot is being mounted while another with the
- // same name exists, force its update _after_ the new slot has been
- // assigned into the instance, such that its own rendering of children
- // will be empty (the new Slot will subsume all fills for this name).
- if ( previousSlot ) {
- previousSlot.forceUpdate();
- }
- }
-
- registerFill( name, instance ) {
- this.fills[ name ] = [ ...( this.fills[ name ] || [] ), instance ];
- this.forceUpdateSlot( name );
- }
-
- unregisterSlot( name, instance ) {
- // If a previous instance of a Slot by this name unmounts, do nothing,
- // as the slot and its fills should only be removed for the current
- // known instance.
- if ( this.slots[ name ] !== instance ) {
- return;
- }
-
- delete this.slots[ name ];
- this.triggerListeners();
- }
-
- unregisterFill( name, instance ) {
- this.fills[ name ] =
- this.fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? [];
- this.forceUpdateSlot( name );
- }
-
- getSlot( name ) {
- return this.slots[ name ];
- }
-
- getFills( name, slotInstance ) {
- // Fills should only be returned for the current instance of the slot
- // in which they occupy.
- if ( this.slots[ name ] !== slotInstance ) {
- return [];
- }
- return this.fills[ name ];
- }
-
- forceUpdateSlot( name ) {
- const slot = this.getSlot( name );
-
- if ( slot ) {
- slot.forceUpdate();
- }
- }
-
- triggerListeners() {
- this.listeners.forEach( ( listener ) => listener() );
- }
-
- subscribe( listener ) {
- this.listeners.push( listener );
-
- return () => {
- this.listeners = this.listeners.filter( ( l ) => l !== listener );
- };
- }
-
- render() {
- return (
-
- { this.props.children }
-
- );
- }
-}
diff --git a/packages/components/src/slot-fill/provider.tsx b/packages/components/src/slot-fill/provider.tsx
new file mode 100644
index 00000000000000..659ec667e69917
--- /dev/null
+++ b/packages/components/src/slot-fill/provider.tsx
@@ -0,0 +1,131 @@
+/**
+ * WordPress dependencies
+ */
+import type { Component } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import SlotFillContext from './context';
+import type {
+ BaseFillObject,
+ BaseSlotFillContext,
+ SlotComponentProps,
+} from './types';
+import { useState } from '@wordpress/element';
+/**
+ * External dependencies
+ */
+import type { ReactNode } from 'react';
+
+export function createSlotRegistory(): BaseSlotFillContext {
+ const slots: Record< string, Component< SlotComponentProps > > = {};
+ const fills: Record< string, BaseFillObject[] > = {};
+ let listeners: Array< () => void > = [];
+
+ function registerSlot(
+ name: string,
+ slot: Component< SlotComponentProps >
+ ) {
+ const previousSlot = slots[ name ];
+ slots[ name ] = slot;
+ triggerListeners();
+
+ // Sometimes the fills are registered after the initial render of slot
+ // But before the registerSlot call, we need to rerender the slot.
+ forceUpdateSlot( name );
+
+ // If a new instance of a slot is being mounted while another with the
+ // same name exists, force its update _after_ the new slot has been
+ // assigned into the instance, such that its own rendering of children
+ // will be empty (the new Slot will subsume all fills for this name).
+ if ( previousSlot ) {
+ previousSlot.forceUpdate();
+ }
+ }
+
+ function registerFill( name: string, instance: BaseFillObject ) {
+ fills[ name ] = [ ...( fills[ name ] || [] ), instance ];
+ forceUpdateSlot( name );
+ }
+
+ function unregisterSlot(
+ name: string,
+ instance: Component< SlotComponentProps >
+ ) {
+ // If a previous instance of a Slot by this name unmounts, do nothing,
+ // as the slot and its fills should only be removed for the current
+ // known instance.
+ if ( slots[ name ] !== instance ) {
+ return;
+ }
+
+ delete slots[ name ];
+ triggerListeners();
+ }
+
+ function unregisterFill( name: string, instance: BaseFillObject ) {
+ fills[ name ] =
+ fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? [];
+ forceUpdateSlot( name );
+ }
+
+ function getSlot(
+ name: string
+ ): Component< SlotComponentProps > | undefined {
+ return slots[ name ];
+ }
+
+ function getFills(
+ name: string,
+ slotInstance: Component< SlotComponentProps >
+ ): BaseFillObject[] {
+ // Fills should only be returned for the current instance of the slot
+ // in which they occupy.
+ if ( slots[ name ] !== slotInstance ) {
+ return [];
+ }
+ return fills[ name ];
+ }
+
+ function forceUpdateSlot( name: string ) {
+ const slot = getSlot( name );
+
+ if ( slot ) {
+ slot.forceUpdate();
+ }
+ }
+
+ function triggerListeners() {
+ listeners.forEach( ( listener ) => listener() );
+ }
+
+ function subscribe( listener: () => void ) {
+ listeners.push( listener );
+
+ return () => {
+ listeners = listeners.filter( ( l ) => l !== listener );
+ };
+ }
+
+ return {
+ registerSlot,
+ unregisterSlot,
+ registerFill,
+ unregisterFill,
+ getSlot,
+ getFills,
+ subscribe,
+ };
+}
+
+export function SlotFillProvider( { children }: { children: ReactNode } ) {
+ const [ contextValue ] = useState( createSlotRegistory );
+ return (
+
+ { children }
+
+ );
+}
+
+export default SlotFillProvider;
diff --git a/packages/components/src/slot-fill/slot.js b/packages/components/src/slot-fill/slot.tsx
similarity index 68%
rename from packages/components/src/slot-fill/slot.js
rename to packages/components/src/slot-fill/slot.tsx
index a960647c3ab64e..bd844f72647773 100644
--- a/packages/components/src/slot-fill/slot.js
+++ b/packages/components/src/slot-fill/slot.tsx
@@ -1,4 +1,3 @@
-// @ts-nocheck
/**
* WordPress dependencies
*/
@@ -13,6 +12,11 @@ import {
* Internal dependencies
*/
import SlotFillContext from './context';
+import type { BaseSlotProps, SlotComponentProps } from './types';
+/**
+ * External dependencies
+ */
+import type { ReactElement } from 'react';
/**
* Whether the argument is a function.
@@ -20,13 +24,15 @@ import SlotFillContext from './context';
* @param {*} maybeFunc The argument to check.
* @return {boolean} True if the argument is a function, false otherwise.
*/
-function isFunction( maybeFunc ) {
+function isFunction( maybeFunc: any ): maybeFunc is Function {
return typeof maybeFunc === 'function';
}
-class SlotComponent extends Component {
- constructor() {
- super( ...arguments );
+class SlotComponent extends Component< SlotComponentProps > {
+ private isUnmounted: boolean;
+
+ constructor( props: SlotComponentProps ) {
+ super( props );
this.isUnmounted = false;
}
@@ -43,11 +49,12 @@ class SlotComponent extends Component {
unregisterSlot( this.props.name, this );
}
- componentDidUpdate( prevProps ) {
+ componentDidUpdate( prevProps: SlotComponentProps ) {
const { name, unregisterSlot, registerSlot } = this.props;
if ( prevProps.name !== name ) {
- unregisterSlot( prevProps.name );
+ // TODO: Check if adding `this` works correctly. @torounit
+ unregisterSlot( prevProps.name, this );
registerSlot( name, this );
}
}
@@ -62,33 +69,37 @@ class SlotComponent extends Component {
render() {
const { children, name, fillProps = {}, getFills } = this.props;
- const fills = ( getFills( name, this ) ?? [] )
+ const fills: ( ReactElement | string )[][] = (
+ getFills( name, this ) ?? []
+ )
.map( ( fill ) => {
const fillChildren = isFunction( fill.children )
? fill.children( fillProps )
: fill.children;
-
return Children.map( fillChildren, ( child, childIndex ) => {
if ( ! child || typeof child === 'string' ) {
return child;
}
- const childKey = child.key || childIndex;
- return cloneElement( child, { key: childKey } );
+ const childKey = child?.key || childIndex;
+ return cloneElement( child as ReactElement, {
+ key: childKey,
+ } );
} );
} )
.filter(
// In some cases fills are rendered only when some conditions apply.
// This ensures that we only use non-empty fills when rendering, i.e.,
// it allows us to render wrappers only when the fills are actually present.
- ( element ) => ! isEmptyElement( element )
+ ( element ): element is Exclude< typeof element, undefined > =>
+ ! isEmptyElement( element )
);
return <>{ isFunction( children ) ? children( fills ) : fills }>;
}
}
-const Slot = ( props ) => (
+const Slot = ( props: BaseSlotProps ) => (
{ ( { registerSlot, unregisterSlot, getFills } ) => (
+ ) => void;
+ unregisterSlot: (
+ name: string,
+ slot: Component< SlotComponentProps >
+ ) => void;
+ registerFill: ( name: string, instance: BaseFillObject ) => void;
+ unregisterFill: ( name: string, instance: BaseFillObject ) => void;
+ getSlot: ( name: string ) => Component< SlotComponentProps > | undefined;
+ getFills: (
+ name: string,
+ slotInstance: Component< SlotComponentProps >
+ ) => BaseFillObject[];
+ subscribe: ( listener: () => void ) => () => void;
+};
+
+export type BaseSlotProps = {
+ name: string;
+ fillProps?: any;
+ children?: ( fills: ( string | ReactElement )[][] ) => ReactNode;
+};
+
+export type SlotComponentProps = {
+ registerSlot: (
+ name: string,
+ slot: Component< SlotComponentProps >
+ ) => void;
+ unregisterSlot: (
+ name: string,
+ slot: Component< SlotComponentProps >
+ ) => void;
+ getFills: (
+ name: string,
+ slotInstance: Component< SlotComponentProps >
+ ) => BaseFillObject[];
+} & BaseSlotProps;
+
+export type BaseFillObject = {
+ name: string;
+ children?:
+ | ReactElement
+ | string
+ | ( ( props: any ) => ReactElement | string );
+};
+
+export type FillComponentProps = {
+ name: string;
+ children?:
+ | ReactElement
+ | string
+ | ( ( props: any ) => ReactElement | string );
+};
diff --git a/packages/components/src/slot-fill/use-slot.js b/packages/components/src/slot-fill/use-slot.ts
similarity index 67%
rename from packages/components/src/slot-fill/use-slot.js
rename to packages/components/src/slot-fill/use-slot.ts
index fce7c651495d5b..0cc2163ff3fd40 100644
--- a/packages/components/src/slot-fill/use-slot.js
+++ b/packages/components/src/slot-fill/use-slot.ts
@@ -1,4 +1,8 @@
-// @ts-nocheck
+/**
+ * External dependencies
+ */
+import type { Component } from 'react';
+
/**
* WordPress dependencies
*/
@@ -13,11 +17,11 @@ import SlotFillContext from './context';
* React hook returning the active slot given a name.
*
* @param {string} name Slot name.
- * @return {Object} Slot object.
+ * @return {Component|undefined} Slot object.
*/
-const useSlot = ( name ) => {
+const useSlot = ( name: string ) => {
const { getSlot, subscribe } = useContext( SlotFillContext );
- return useSyncExternalStore(
+ return useSyncExternalStore< Component | undefined >(
subscribe,
() => getSlot( name ),
() => getSlot( name )