From 4b14dca9a3c3ae47b83cdbd5dc85f788aeda4842 Mon Sep 17 00:00:00 2001 From: alex <44968616+nderkim@users.noreply.github.com> Date: Mon, 24 Oct 2022 17:46:41 +1100 Subject: [PATCH 1/4] Refactor class components to functional components --- docs/ExampleWrapper.tsx | 140 +++++++---------- packages/react-select/src/NonceProvider.tsx | 29 ++-- .../react-select/src/animated/transitions.tsx | 141 +++++++----------- 3 files changed, 116 insertions(+), 194 deletions(-) diff --git a/docs/ExampleWrapper.tsx b/docs/ExampleWrapper.tsx index c290c5f973..b1e10b108c 100644 --- a/docs/ExampleWrapper.tsx +++ b/docs/ExampleWrapper.tsx @@ -1,7 +1,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/react'; // eslint-disable-line no-unused-vars import { CSSObject } from '@emotion/serialize'; -import { Component } from 'react'; +import { ReactNode, useState } from 'react'; import CodeSandboxer, { GitInfo } from 'react-codesandboxer'; import { CodeBlock } from './markdown/renderer'; import pkg from '../packages/react-select/package.json'; @@ -20,99 +20,69 @@ const gitInfo: GitInfo = { const sourceUrl = `https://github.com/${gitInfo.account}/react-select/tree/${gitInfo.branch}`; interface Props { + children?: ReactNode; readonly label: string; readonly raw: { readonly default: string }; readonly urlPath: string; readonly isEditable?: boolean; } -interface State { - readonly showCode: boolean; -} - -export default class ExampleWrapper extends Component { - state: State = { showCode: false }; - static defaultProps = { isEditable: true }; - - renderCodeSample = () => { - let { raw } = this.props; - let { showCode } = this.state; - - if (!showCode || !raw) { - return null; - } else { - return ; - } - }; - - renderSourceViewOption = () => { - let { raw } = this.props; - let { showCode } = this.state; - - if (!raw) { - return ( - - - - ); - } else { - return ( - this.setState({ showCode: !showCode })} - title="View Source" - > - - - ); - } - }; - - renderCSBButton = () => { - let { isEditable, raw, urlPath } = this.props; +export default ({ + children, + label, + raw, + urlPath, + isEditable = true, +}: Props) => { + const [showCode, setShowCode] = useState(false); - if (isEditable) { - return ( - - {({ isLoading }) => ( - - {isLoading ? : } + return ( +
+ +

{label}

+ + {raw ? ( + setShowCode((prev) => !prev)} + title="View Source" + > + + ) : ( + + + )} - - ); - } else { - return null; - } - }; - - render() { - return ( -
- -

{this.props.label}

- - {this.renderSourceViewOption()} - {this.renderCSBButton()} - -
- {this.renderCodeSample()} - {this.props.children} -
- ); - } -} + {isEditable ? ( + + {({ isLoading }) => ( + + {isLoading ? : } + + )} + + ) : null} +
+
+ {showCode && raw ? ( + + ) : null} + {children} +
+ ); +}; const ExampleHeading = (props: any) => (
{ - constructor(props: NonceProviderProps) { - super(props); - this.createEmotionCache = memoizeOne(this.createEmotionCache); - } - createEmotionCache = (nonce: string, key: string) => { - return createCache({ nonce, key }); - }; - render() { - const emotionCache = this.createEmotionCache( - this.props.nonce, - this.props.cacheKey - ); - return ( - {this.props.children} - ); - } -} +export default ({ nonce, children, cacheKey }: NonceProviderProps) => { + const emotionCache = useMemo( + () => createCache({ key: cacheKey, nonce }), + [cacheKey, nonce] + ); + return {children}; +}; diff --git a/packages/react-select/src/animated/transitions.tsx b/packages/react-select/src/animated/transitions.tsx index f1cd0bc5e9..30a4e592d4 100644 --- a/packages/react-select/src/animated/transitions.tsx +++ b/packages/react-select/src/animated/transitions.tsx @@ -1,12 +1,6 @@ import * as React from 'react'; -import { - Component, - ComponentType, - createRef, - CSSProperties, - ReactNode, - useRef, -} from 'react'; +import { useEffect, useState } from 'react'; +import { ComponentType, CSSProperties, ReactNode, useRef } from 'react'; import { Transition } from 'react-transition-group'; import { ExitHandler, @@ -72,93 +66,62 @@ interface CollapseProps { in?: boolean; onExited?: ExitHandler; } -interface CollapseState { - width: Width; -} // wrap each MultiValue with a collapse transition; decreases width until // finally removing from DOM -export class Collapse extends Component { - duration = collapseDuration; - rafID?: number | null; - state: CollapseState = { width: 'auto' }; - transition: { [K in TransitionStatus]?: CSSProperties } = { - exiting: { width: 0, transition: `width ${this.duration}ms ease-out` }, - exited: { width: 0 }, - }; - nodeRef = createRef(); +export const Collapse = ({ children, in: _in, onExited }: CollapseProps) => { + const ref = useRef(null); + const [width, setWidth] = useState('auto'); - componentDidMount() { - const { current: ref } = this.nodeRef; + useEffect(() => { + const { current } = ref; + if (!current) return; /* - A check on existence of ref should not be necessary at this point, - but TypeScript demands it. + Here we're invoking requestAnimationFrame with a callback invoking our + call to getBoundingClientRect and setState in order to resolve an edge case + around portalling. Certain portalling solutions briefly remove children from the DOM + before appending them to the target node. This is to avoid us trying to call getBoundingClientrect + while the Select component is in this state. */ - if (ref) { - /* - Here we're invoking requestAnimationFrame with a callback invoking our - call to getBoundingClientRect and setState in order to resolve an edge case - around portalling. Certain portalling solutions briefly remove children from the DOM - before appending them to the target node. This is to avoid us trying to call getBoundingClientrect - while the Select component is in this state. - */ - // cannot use `offsetWidth` because it is rounded - this.rafID = window.requestAnimationFrame(() => { - const { width } = ref.getBoundingClientRect(); - this.setState({ width }); - }); - } - } - - componentWillUnmount() { - if (this.rafID) { - window.cancelAnimationFrame(this.rafID); - } - } - - // get base styles - getStyle = (width: Width): CSSProperties => ({ - overflow: 'hidden', - whiteSpace: 'nowrap', - width, - }); - - // get transition styles - getTransition = (state: TransitionStatus) => this.transition[state]; - - render() { - const { children, in: inProp, onExited } = this.props; - const exitedProp = () => { - if (this.nodeRef.current && onExited) { - onExited(this.nodeRef.current); - } - }; + // cannot use `offsetWidth` because it is rounded + const rafId = window.requestAnimationFrame(() => + setWidth(current.getBoundingClientRect().width) + ); - const { width } = this.state; + return () => window.cancelAnimationFrame(rafId); + }, []); - return ( - - {(state) => { - const style = { - ...this.getStyle(width), - ...this.getTransition(state), - }; - return ( -
- {children} -
- ); - }} -
- ); - } -} + return ( + { + const { current } = ref; + if (!current) return; + onExited?.(current); + }} + timeout={collapseDuration} + nodeRef={ref} + > + {(state) => ( +
+ {children} +
+ )} +
+ ); +}; From 61f8ee986b8ac4466a5844577b244d0158751aa7 Mon Sep 17 00:00:00 2001 From: alex <44968616+nderkim@users.noreply.github.com> Date: Wed, 26 Oct 2022 14:55:10 +1100 Subject: [PATCH 2/4] Refactor MenuPlacer to functional component --- packages/react-select/src/components/Menu.tsx | 164 +++++++++--------- 1 file changed, 79 insertions(+), 85 deletions(-) diff --git a/packages/react-select/src/components/Menu.tsx b/packages/react-select/src/components/Menu.tsx index 602e13938e..0678e8f282 100644 --- a/packages/react-select/src/components/Menu.tsx +++ b/packages/react-select/src/components/Menu.tsx @@ -1,13 +1,13 @@ /** @jsx jsx */ import { createContext, - Component, + Fragment, ReactNode, - RefCallback, - ContextType, - useState, + Ref, useCallback, + useContext, useRef, + useState, } from 'react'; import { jsx } from '@emotion/react'; import { createPortal } from 'react-dom'; @@ -55,10 +55,10 @@ interface PlacementArgs { } export function getMenuPlacement({ - maxHeight, + maxHeight: desiredMaxHeight, menuEl, minHeight, - placement, + placement: desiredPlacement, shouldScroll, isFixedPosition, theme, @@ -67,7 +67,7 @@ export function getMenuPlacement({ const scrollParent = getScrollParent(menuEl!); const defaultState: CalculatedMenuPlacementAndHeight = { placement: 'bottom', - maxHeight, + maxHeight: desiredMaxHeight, }; // something went wrong, return default state @@ -99,12 +99,12 @@ export function getMenuPlacement({ const scrollUp = scrollTop + menuTop - marginTop; const scrollDuration = 160; - switch (placement) { + switch (desiredPlacement) { case 'auto': case 'bottom': // 1: the menu will fit, do nothing if (viewSpaceBelow >= menuHeight) { - return { placement: 'bottom', maxHeight }; + return { placement: 'bottom', maxHeight: desiredMaxHeight }; } // 2: the menu will fit, if scrolled @@ -113,7 +113,7 @@ export function getMenuPlacement({ animatedScrollTo(scrollParent, scrollDown, scrollDuration); } - return { placement: 'bottom', maxHeight }; + return { placement: 'bottom', maxHeight: desiredMaxHeight }; } // 3: the menu will fit, if constrained @@ -140,15 +140,15 @@ export function getMenuPlacement({ // 4. Forked beviour when there isn't enough space below // AUTO: flip the menu, render above - if (placement === 'auto' || isFixedPosition) { + if (desiredPlacement === 'auto' || isFixedPosition) { // may need to be constrained after flipping - let constrainedHeight = maxHeight; + let constrainedHeight = desiredMaxHeight; const spaceAbove = isFixedPosition ? viewSpaceAbove : scrollSpaceAbove; if (spaceAbove >= minHeight) { constrainedHeight = Math.min( spaceAbove - marginBottom - spacing.controlHeight, - maxHeight + desiredMaxHeight ); } @@ -156,17 +156,17 @@ export function getMenuPlacement({ } // BOTTOM: allow browser to increase scrollable area and immediately set scroll - if (placement === 'bottom') { + if (desiredPlacement === 'bottom') { if (shouldScroll) { scrollTo(scrollParent, scrollDown); } - return { placement: 'bottom', maxHeight }; + return { placement: 'bottom', maxHeight: desiredMaxHeight }; } break; case 'top': // 1: the menu will fit, do nothing if (viewSpaceAbove >= menuHeight) { - return { placement: 'top', maxHeight }; + return { placement: 'top', maxHeight: desiredMaxHeight }; } // 2: the menu will fit, if scrolled @@ -175,7 +175,7 @@ export function getMenuPlacement({ animatedScrollTo(scrollParent, scrollUp, scrollDuration); } - return { placement: 'top', maxHeight }; + return { placement: 'top', maxHeight: desiredMaxHeight }; } // 3: the menu will fit, if constrained @@ -183,7 +183,7 @@ export function getMenuPlacement({ (!isFixedPosition && scrollSpaceAbove >= minHeight) || (isFixedPosition && viewSpaceAbove >= minHeight) ) { - let constrainedHeight = maxHeight; + let constrainedHeight = desiredMaxHeight; // we want to provide as much of the menu as possible to the user, // so give them whatever is available below rather than the minHeight. @@ -209,9 +209,9 @@ export function getMenuPlacement({ // 4. not enough space, the browser WILL NOT increase scrollable area when // absolutely positioned element rendered above the viewport (only below). // Flip the menu, render below - return { placement: 'bottom', maxHeight }; + return { placement: 'bottom', maxHeight: desiredMaxHeight }; default: - throw new Error(`Invalid placement provided "${placement}".`); + throw new Error(`Invalid placement provided "${desiredPlacement}".`); } return defaultState; @@ -240,7 +240,7 @@ export interface MenuProps< > extends CommonPropsAndClassName, MenuPlacementProps { /** Reference to the internal element, consumed by the MenuPlacer component */ - innerRef: RefCallback; + innerRef: Ref; innerProps: JSX.IntrinsicElements['div']; isLoading: boolean; placement: CoercedMenuPlacement; @@ -254,7 +254,7 @@ interface PlacerProps { } interface ChildrenProps { - ref: RefCallback; + ref: Ref; placerProps: PlacerProps; } @@ -294,41 +294,40 @@ export const menuCSS = < zIndex: 1, }); -const PortalPlacementContext = createContext<{ - getPortalPlacement: - | ((menuState: CalculatedMenuPlacementAndHeight) => void) - | null; -}>({ getPortalPlacement: null }); - -interface MenuState { - placement: CoercedMenuPlacement | null; - maxHeight: number; -} +const PortalPlacementContext = + createContext< + | { + setPortalPlacement: (placement: CoercedMenuPlacement) => void; + } + | undefined + >(undefined); // NOTE: internal only -export class MenuPlacer< +export const MenuPlacer = < Option, IsMulti extends boolean, Group extends GroupBase