From c4430fd22f1781857d8ffccdbfcc821f1d288576 Mon Sep 17 00:00:00 2001 From: kp Date: Tue, 12 Jan 2021 18:31:43 +0800 Subject: [PATCH 01/42] refactor(accordion): add forward ref --- lib/src/Accordion/Accordion.tsx | 64 +++++++++++++++-------------- lib/src/Accordion/AccordionItem.tsx | 62 ++++++++++++++-------------- 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/lib/src/Accordion/Accordion.tsx b/lib/src/Accordion/Accordion.tsx index 970cd857c..f3615adc9 100644 --- a/lib/src/Accordion/Accordion.tsx +++ b/lib/src/Accordion/Accordion.tsx @@ -16,37 +16,39 @@ export type AccordionProps = JSX.IntrinsicElements["div"] & { }; /** Accordions show and hide information that is not necessary at all time with one click. */ -export const Accordion: React.FC = React.memo(({ alternative, onToggle, inverted, ...props }: AccordionProps) => { - const [active, setActive] = React.useState(props.defaultValue); - const [id, setId] = React.useState(props.id); +export const Accordion: React.FC = React.memo( + React.forwardRef(({ alternative, onToggle, inverted, ...props }: AccordionProps, ref: React.ForwardedRef) => { + const [active, setActive] = React.useState(props.defaultValue); + const [id, setId] = React.useState(props.id); - /** Sets custom id if the user din't pass any */ - React.useEffect(() => setId(props.id || randomId("accordion-")), [props.id]); - React.useEffect(() => { - typeof props.defaultValue === "number" && setActive(props.defaultValue); - }, [props.defaultValue]); + /** Sets custom id if the user din't pass any */ + React.useEffect(() => setId(props.id || randomId("accordion-")), [props.id]); + React.useEffect(() => { + typeof props.defaultValue === "number" && setActive(props.defaultValue); + }, [props.defaultValue]); - /** - * Handles accordion item click event - * @param {React.MouseEvent} e MouseEvent - */ - const onToggleInner: React.MouseEventHandler = React.useCallback((e: React.MouseEvent) => { - const index: number = Number(e.currentTarget.dataset.indexNumber); - !isNaN(index) && setActive((val: number) => (val === index ? -1 : index)); - }, []); + /** + * Handles accordion item click event + * @param {React.MouseEvent} e MouseEvent + */ + const onToggleInner: React.MouseEventHandler = React.useCallback((e: React.MouseEvent) => { + const index: number = Number(e.currentTarget.dataset.indexNumber); + !isNaN(index) && setActive((val: number) => (val === index ? -1 : index)); + }, []); - return ( -
- {React.Children.map(props.children, (Child: React.ReactElement, i: number) => { - return React.isValidElement>(Child) - ? React.cloneElement(Child, { - onToggle: onToggle || onToggleInner, - defaultChecked: typeof active !== "number" ? Child.props.defaultChecked : active === i, - "data-parent-id": id, - "data-index-number": i, - }) - : Child; - })} -
- ); -}); + return ( +
+ {React.Children.map(props.children, (Child: React.ReactElement, i: number) => { + return React.isValidElement>(Child) + ? React.cloneElement(Child, { + onToggle: onToggle || onToggleInner, + defaultChecked: typeof active !== "number" ? Child.props.defaultChecked : active === i, + "data-parent-id": id, + "data-index-number": i, + }) + : Child; + })} +
+ ); + }) +); diff --git a/lib/src/Accordion/AccordionItem.tsx b/lib/src/Accordion/AccordionItem.tsx index ad46c2680..e09f92871 100644 --- a/lib/src/Accordion/AccordionItem.tsx +++ b/lib/src/Accordion/AccordionItem.tsx @@ -12,36 +12,38 @@ export type AccordionItemProps = JSX.IntrinsicElements["div"] & { onToggle?: React.MouseEventHandler; }; -export const AccordionItem: React.FC = React.memo(({ header, subHeader, onToggle, ...props }: AccordionItemProps) => { - const [uniqueId] = React.useState(randomId("accordion-item-")); +export const AccordionItem: React.FC = React.memo( + React.forwardRef(({ header, subHeader, onToggle, ...props }: AccordionItemProps, ref: React.ForwardedRef) => { + const [uniqueId] = React.useState(randomId("accordion-item-")); - return ( -
-
- +
+
-

{header}

- {subHeader &&
{subHeader}
} - + +
{props.children}
+
+
-
- -
{props.children}
-
-
- - ); -}); + ); + }) +); From 6124eef35a50ce90ccc285a10f875ecd333af8b1 Mon Sep 17 00:00:00 2001 From: kp Date: Tue, 12 Jan 2021 18:40:04 +0800 Subject: [PATCH 02/42] refactor(breadcrumb): add forward ref --- lib/src/Breadcrumb/Breadcrumb.tsx | 36 ++++++++++++++------------- lib/src/Breadcrumb/BreadcrumbItem.tsx | 24 ++++++++++-------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/src/Breadcrumb/Breadcrumb.tsx b/lib/src/Breadcrumb/Breadcrumb.tsx index 97e444115..392946103 100644 --- a/lib/src/Breadcrumb/Breadcrumb.tsx +++ b/lib/src/Breadcrumb/Breadcrumb.tsx @@ -10,20 +10,22 @@ export type BreadcrumbProps = JSX.IntrinsicElements["nav"] & { }; /** A breadcrumb is a secondary navigation showing the website hierarchy. */ -export const Breadcrumb: React.FC = React.memo(({ onNavigate, light, ...props }: BreadcrumbProps) => { - return ( - - ); -}); +export const Breadcrumb: React.FC = React.memo( + React.forwardRef(({ onNavigate, light, ...props }: BreadcrumbProps, ref: React.ForwardedRef) => { + return ( + + ); + }) +); diff --git a/lib/src/Breadcrumb/BreadcrumbItem.tsx b/lib/src/Breadcrumb/BreadcrumbItem.tsx index f131475cb..b648f81d4 100644 --- a/lib/src/Breadcrumb/BreadcrumbItem.tsx +++ b/lib/src/Breadcrumb/BreadcrumbItem.tsx @@ -11,16 +11,18 @@ export type BreadcrumbItemProps = JSX.IntrinsicElements["li"] & { onNavigate?: React.MouseEventHandler; }; -export const BreadcrumbItem: React.FC = React.memo(({ href = "#", onNavigate, ...props }: BreadcrumbItemProps) => { - const [className, setClassName] = React.useState("breadcrumb-item"); +export const BreadcrumbItem: React.FC = React.memo( + React.forwardRef(({ href = "#", onNavigate, ...props }: BreadcrumbItemProps, ref: React.ForwardedRef) => { + const [className, setClassName] = React.useState("breadcrumb-item"); - React.useEffect(() => setClassName(classnames(["breadcrumb-item", { active: props.defaultChecked }, props.className])), [props.defaultChecked, props.className]); + React.useEffect(() => setClassName(classnames(["breadcrumb-item", { active: props.defaultChecked }, props.className])), [props.defaultChecked, props.className]); - return ( -
  • - - {props.children} - -
  • - ); -}); + return ( +
  • + + {props.children} + +
  • + ); + }) +); From 7eea970c3fa8fac14da0b2f55484cd0c1c0f35b8 Mon Sep 17 00:00:00 2001 From: kp Date: Tue, 12 Jan 2021 18:42:11 +0800 Subject: [PATCH 03/42] refactor(button): add forward ref --- lib/src/Button/Button.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/src/Button/Button.tsx b/lib/src/Button/Button.tsx index 120efb5cb..a8bb428f8 100644 --- a/lib/src/Button/Button.tsx +++ b/lib/src/Button/Button.tsx @@ -13,16 +13,18 @@ export type ButtonProps = JSX.IntrinsicElements["button"] & { block?: boolean; }; /** Buttons allow users to take action with a single tap. */ -export const Button: React.FC = React.memo(({ theme = "primary", size, block, ...props }: ButtonProps) => { - const [className, setClassName] = React.useState("btn btn-primary"); +export const Button: React.FC = React.memo( + React.forwardRef(({ theme = "primary", size, block, ...props }: ButtonProps, ref: React.ForwardedRef) => { + const [className, setClassName] = React.useState("btn btn-primary"); - React.useEffect(() => { - setClassName(classnames("rc", "btn", `btn-${theme}`, { [`btn-${size}`]: size, "btn-block": block }, props.className)); - }, [size, theme, block, props.className]); + React.useEffect(() => { + setClassName(classnames("rc", "btn", `btn-${theme}`, { [`btn-${size}`]: size, "btn-block": block }, props.className)); + }, [size, theme, block, props.className]); - return ( - - ); -}); + return ( + + ); + }) +); From 0dff213b6f207c804630a55e4644e7a87ad48e8f Mon Sep 17 00:00:00 2001 From: kp Date: Tue, 12 Jan 2021 18:49:34 +0800 Subject: [PATCH 04/42] refactor(button-group): add forward ref --- lib/src/ButtonGroup/ButtonGroup.tsx | 37 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/src/ButtonGroup/ButtonGroup.tsx b/lib/src/ButtonGroup/ButtonGroup.tsx index b5ac5948d..44cb2f134 100644 --- a/lib/src/ButtonGroup/ButtonGroup.tsx +++ b/lib/src/ButtonGroup/ButtonGroup.tsx @@ -11,20 +11,23 @@ export type ButtonGroupProps = JSX.IntrinsicElements["div"] & { }; /** Button group wrapper. Use this to group multiple buttons */ -export const ButtonGroup: React.FC = React.memo(({ vertical, size, ...props }: ButtonGroupProps) => ( -
    - {props.children} -
    -)); +export const ButtonGroup: React.FC = React.memo( + React.forwardRef(({ vertical, size, ...props }: ButtonGroupProps, ref: React.ForwardedRef) => ( +
    + {props.children} +
    + )) +); From e90c6892483a076d90f1151f3837d0b9bd6016b4 Mon Sep 17 00:00:00 2001 From: kp Date: Tue, 12 Jan 2021 18:50:00 +0800 Subject: [PATCH 05/42] refactor(carousel): add forward ref --- lib/src/Carousel/Carousel.tsx | 363 ++++++++++++------------ lib/src/Carousel/CarouselIndicators.tsx | 24 +- lib/src/Carousel/CarouselItem.tsx | 88 +++--- 3 files changed, 247 insertions(+), 228 deletions(-) diff --git a/lib/src/Carousel/Carousel.tsx b/lib/src/Carousel/Carousel.tsx index 5aa29b929..34934c41f 100644 --- a/lib/src/Carousel/Carousel.tsx +++ b/lib/src/Carousel/Carousel.tsx @@ -31,186 +31,201 @@ export type NavigationDirection = "next" | "prev"; type NavigateTrigger = React.MouseEvent | React.TouchEvent | React.KeyboardEvent; type SwipeEvent = React.TouchEvent | React.MouseEvent; -export const Carousel: React.FC = ({ - afterChange, - transitionDuration = defaultTransitionDuration, - transitionStyle = "slide", - infinite = true, - showIndicators, - autoplay = false, - autoplaySpeed = defaultAutoplaySpeed, - ...props -}: CarouselProps) => { - const [active, setActive] = React.useState(0); - const [nav, setNav] = React.useState("next"); - const [id, setId] = React.useState(""); - const [className, setClassName] = React.useState("carousel"); - const [swipePos, setSwipePos] = React.useState(); - const interrupted: React.MutableRefObject = React.useRef(false); - const timer: React.MutableRefObject = React.useRef(); - - const size: number = React.Children.toArray(props.children).length; - - /** ----- Utilities ----- */ - - const findNewActive = React.useCallback( - (newNav: NavigationDirection): number => { - const isInfinite: boolean = infinite || infinite === undefined; - switch (newNav) { - case "prev": - return active === 0 ? (isInfinite ? size - 1 : undefined) : active - 1; - case "next": - return active === size - 1 ? (isInfinite ? 0 : undefined) : active + 1; - } - }, - [infinite, active, size] - ); - - /** - * Handles navigating to a slide - * @param {NavigateTrigger} e Navigation trigger event - */ - const goToSlide = React.useCallback( - (e: NavigateTrigger, slideTo?: NavigationDirection): void => { - e.cancelable && e.preventDefault(); - let newActive: number; - let newNav: NavigationDirection; - const target: EventTarget & HTMLElement = e.target as any; - if (["mousedown", "touchstart"].some((val: string) => val === e.type)) { - /** Swipe gesture */ - newNav = slideTo; - newActive = findNewActive(newNav); - } else { - if (e.type === "click") { - newNav = target.tagName === "LI" ? (Number(target.dataset.slideTo) > active ? "next" : "prev") : (target.dataset.slide as NavigationDirection); +export const Carousel: React.FC = React.forwardRef( + ( + { + afterChange, + transitionDuration = defaultTransitionDuration, + transitionStyle = "slide", + infinite = true, + showIndicators, + autoplay = false, + autoplaySpeed = defaultAutoplaySpeed, + ...props + }: CarouselProps, + ref: React.ForwardedRef + ) => { + const [active, setActive] = React.useState(0); + const [nav, setNav] = React.useState("next"); + const [id, setId] = React.useState(""); + const [className, setClassName] = React.useState("carousel"); + const [swipePos, setSwipePos] = React.useState(); + const interrupted: React.MutableRefObject = React.useRef(false); + const timer: React.MutableRefObject = React.useRef(); + + const size: number = React.Children.toArray(props.children).length; + + /** ----- Utilities ----- */ + + const findNewActive = React.useCallback( + (newNav: NavigationDirection): number => { + const isInfinite: boolean = infinite || infinite === undefined; + switch (newNav) { + case "prev": + return active === 0 ? (isInfinite ? size - 1 : undefined) : active - 1; + case "next": + return active === size - 1 ? (isInfinite ? 0 : undefined) : active + 1; + } + }, + [infinite, active, size] + ); + + /** + * Handles navigating to a slide + * @param {NavigateTrigger} e Navigation trigger event + */ + const goToSlide = React.useCallback( + (e: NavigateTrigger, slideTo?: NavigationDirection): void => { + e.cancelable && e.preventDefault(); + let newActive: number; + let newNav: NavigationDirection; + const target: EventTarget & HTMLElement = e.target as any; + if (["mousedown", "touchstart"].some((val: string) => val === e.type)) { + /** Swipe gesture */ + newNav = slideTo; + newActive = findNewActive(newNav); } else { - switch ((e as React.KeyboardEvent).key.toLowerCase()) { - case "arrowleft": - newNav = "prev"; - break; - case "arrowright": - newNav = "next"; - break; - case "space": - case " ": - newNav = target.dataset.slide as NavigationDirection; + if (e.type === "click") { + newNav = target.tagName === "LI" ? (Number(target.dataset.slideTo) > active ? "next" : "prev") : (target.dataset.slide as NavigationDirection); + } else { + switch ((e as React.KeyboardEvent).key.toLowerCase()) { + case "arrowleft": + newNav = "prev"; + break; + case "arrowright": + newNav = "next"; + break; + case "space": + case " ": + newNav = target.dataset.slide as NavigationDirection; + } } + newActive = target.tagName === "LI" ? Number(target.dataset.slideTo) : findNewActive(newNav); } - newActive = target.tagName === "LI" ? Number(target.dataset.slideTo) : findNewActive(newNav); - } - if ([newNav, newActive].every((val) => val !== undefined)) { - newNav !== nav && setNav(newNav); - newActive !== active && setActive(newActive); - } - }, - [active, infinite, nav, findNewActive] - ); - - const triggerAutoplay = (): void => { - if (autoplay && !interrupted.current) { - timer.current && clearTimeout(timer.current as number); - - timer.current = setTimeout(() => { - if (!interrupted.current) { - goToSlide(new MouseEvent("mousedown", { bubbles: true }) as any, "next"); + if ([newNav, newActive].every((val) => val !== undefined)) { + newNav !== nav && setNav(newNav); + newActive !== active && setActive(newActive); } - }, autoplaySpeed); - } - }; - - /** ----- Event handlers ----- */ - /** An event handler triggered after a transition has ended */ - const afterTransition = React.useCallback( - (e: AfterSlideEvent): void => { - triggerAutoplay(); - afterChange && afterChange(e); - }, - [afterChange, triggerAutoplay] - ); - - /** - * Handles swipe events - * @param e Touch or mouse event - */ - const handleSwipe = React.useCallback( - (e: SwipeEvent): void => { - e.persist(); - const isTouch: boolean = e.type === "touchstart"; - const startingPos: number = isTouch ? (e as React.TouchEvent).touches.item(0).clientX : (e as React.MouseEvent).clientX; - const parentWidth: number = e.currentTarget.clientWidth; - let xMovement: number; - - const movementHandler: (ev: TouchEvent | MouseEvent) => void = (ev: TouchEvent | MouseEvent) => { - xMovement = isTouch ? (ev as TouchEvent).touches.item(0).clientX : (ev as MouseEvent).clientX; - if (xMovement !== swipePos) { - setSwipePos(xMovement - startingPos); - } - }; + }, + [active, infinite, nav, findNewActive] + ); - const endingHandler: VoidFunction = () => { - if (Math.abs(xMovement - startingPos) > parentWidth / 4) { - goToSlide(e, xMovement - startingPos < 0 ? "next" : "prev"); - } - setSwipePos(undefined); - document.body.removeEventListener(isTouch ? "touchmove" : "mousemove", movementHandler); - document.body.removeEventListener(isTouch ? "touchend" : "mouseup", endingHandler); - }; + const triggerAutoplay = (): void => { + if (autoplay && !interrupted.current) { + timer.current && clearTimeout(timer.current as number); - document.body.addEventListener(isTouch ? "touchmove" : "mousemove", movementHandler); - document.body.addEventListener(isTouch ? "touchend" : "mouseup", endingHandler); - isTouch ? props.onTouchStart && props.onTouchStart(e as React.TouchEvent) : props.onMouseDown && props.onMouseDown(e as React.MouseEvent); - }, - [swipePos, props.onTouchStart, props.onMouseDown, goToSlide, setSwipePos] - ); - - const interruptionHandler = (e: React.MouseEvent): void => { - switch (e.type as keyof HTMLElementEventMap) { - case "mouseenter": - interrupted.current = true; - props.onMouseEnter && props.onMouseEnter(e); - break; - case "mouseleave": - interrupted.current = false; + timer.current = setTimeout(() => { + if (!interrupted.current) { + goToSlide(new MouseEvent("mousedown", { bubbles: true }) as any, "next"); + } + }, autoplaySpeed); + } + }; + + /** ----- Event handlers ----- */ + /** An event handler triggered after a transition has ended */ + const afterTransition = React.useCallback( + (e: AfterSlideEvent): void => { triggerAutoplay(); - props.onMouseLeave && props.onMouseLeave(e); - break; - } - }; - - /** ----- Effects ----- */ - /** Set a custom ID if there is none */ - React.useEffect(() => setId(props.id || randomId("carousel-")), [props.id]); - /** Sets the default value, if any. Otherwise default to the first item */ - React.useEffect(() => setActive(props.defaultValue || 0), [props.defaultValue]); - /** Set class names */ - React.useEffect(() => setClassName(classnames("rc", "carousel", { "carousel-fade": transitionStyle === "fade" }, props.className)), [props.className, transitionStyle]); - /** Triggers autoplay if enabled */ - React.useEffect(() => triggerAutoplay(), [autoplay]); - /** Clearing timeout */ - React.useEffect(() => { - return () => { - timer.current && clearTimeout(timer.current as number); + afterChange && afterChange(e); + }, + [afterChange, triggerAutoplay] + ); + + /** + * Handles swipe events + * @param e Touch or mouse event + */ + const handleSwipe = React.useCallback( + (e: SwipeEvent): void => { + e.persist(); + const isTouch: boolean = e.type === "touchstart"; + const startingPos: number = isTouch ? (e as React.TouchEvent).touches.item(0).clientX : (e as React.MouseEvent).clientX; + const parentWidth: number = e.currentTarget.clientWidth; + let xMovement: number; + + const movementHandler: (ev: TouchEvent | MouseEvent) => void = (ev: TouchEvent | MouseEvent) => { + xMovement = isTouch ? (ev as TouchEvent).touches.item(0).clientX : (ev as MouseEvent).clientX; + if (xMovement !== swipePos) { + setSwipePos(xMovement - startingPos); + } + }; + + const endingHandler: VoidFunction = () => { + if (Math.abs(xMovement - startingPos) > parentWidth / 4) { + goToSlide(e, xMovement - startingPos < 0 ? "next" : "prev"); + } + setSwipePos(undefined); + document.body.removeEventListener(isTouch ? "touchmove" : "mousemove", movementHandler); + document.body.removeEventListener(isTouch ? "touchend" : "mouseup", endingHandler); + }; + + document.body.addEventListener(isTouch ? "touchmove" : "mousemove", movementHandler); + document.body.addEventListener(isTouch ? "touchend" : "mouseup", endingHandler); + isTouch ? props.onTouchStart && props.onTouchStart(e as React.TouchEvent) : props.onMouseDown && props.onMouseDown(e as React.MouseEvent); + }, + [swipePos, props.onTouchStart, props.onMouseDown, goToSlide, setSwipePos] + ); + + const interruptionHandler = (e: React.MouseEvent): void => { + switch (e.type as keyof HTMLElementEventMap) { + case "mouseenter": + interrupted.current = true; + props.onMouseEnter && props.onMouseEnter(e); + break; + case "mouseleave": + interrupted.current = false; + triggerAutoplay(); + props.onMouseLeave && props.onMouseLeave(e); + break; + } }; - }, []); - - return ( -
    - {showIndicators && } -
    - {React.Children.map(props.children, (Child: React.ReactElement, i: number) => - React.isValidElement(Child) - ? React.cloneElement(Child, { - "data-index-number": i, - defaultChecked: active === i, - nav, - transitionDuration, - afterTransition, - translateX: transitionStyle === "slide" && active === i ? swipePos : undefined, - }) - : Child - )} + + /** ----- Effects ----- */ + /** Set a custom ID if there is none */ + React.useEffect(() => setId(props.id || randomId("carousel-")), [props.id]); + /** Sets the default value, if any. Otherwise default to the first item */ + React.useEffect(() => setActive(props.defaultValue || 0), [props.defaultValue]); + /** Set class names */ + React.useEffect(() => setClassName(classnames("rc", "carousel", { "carousel-fade": transitionStyle === "fade" }, props.className)), [props.className, transitionStyle]); + /** Triggers autoplay if enabled */ + React.useEffect(() => triggerAutoplay(), [autoplay]); + /** Clearing timeout */ + React.useEffect(() => { + return () => { + timer.current && clearTimeout(timer.current as number); + }; + }, []); + + return ( +
    + {showIndicators && } +
    + {React.Children.map(props.children, (Child: React.ReactElement, i: number) => + React.isValidElement(Child) + ? React.cloneElement(Child, { + "data-index-number": i, + defaultChecked: active === i, + nav, + transitionDuration, + afterTransition, + translateX: transitionStyle === "slide" && active === i ? swipePos : undefined, + }) + : Child + )} +
    +
    - -
    - ); -}; + ); + } +); diff --git a/lib/src/Carousel/CarouselIndicators.tsx b/lib/src/Carousel/CarouselIndicators.tsx index 5d81505b8..69460251c 100644 --- a/lib/src/Carousel/CarouselIndicators.tsx +++ b/lib/src/Carousel/CarouselIndicators.tsx @@ -12,16 +12,18 @@ export type CarouselIndicatorsProps = JSX.IntrinsicElements["ol"] & { onIndicatorClicked?: React.MouseEventHandler; }; -export const CarouselIndicators: React.FC = React.memo(({ active, size, parentId, onIndicatorClicked, ...props }: CarouselIndicatorsProps) => { - const [className, setClassName] = React.useState("carousel-indicators"); +export const CarouselIndicators: React.FC = React.memo( + React.forwardRef(({ active, size, parentId, onIndicatorClicked, ...props }: CarouselIndicatorsProps, ref: React.ForwardedRef) => { + const [className, setClassName] = React.useState("carousel-indicators"); - React.useEffect(() => setClassName(classnames("carousel-indicators", props.className)), [props.className]); + React.useEffect(() => setClassName(classnames("carousel-indicators", props.className)), [props.className]); - return ( -
      - {[...Array(size)].map((v: undefined, i: number) => ( -
    1. - ))} -
    - ); -}); + return ( +
      + {[...Array(size)].map((v: undefined, i: number) => ( +
    1. + ))} +
    + ); + }) +); diff --git a/lib/src/Carousel/CarouselItem.tsx b/lib/src/Carousel/CarouselItem.tsx index 593b4068c..e95179c74 100644 --- a/lib/src/Carousel/CarouselItem.tsx +++ b/lib/src/Carousel/CarouselItem.tsx @@ -16,50 +16,52 @@ export type CarouselItemProps = JSX.IntrinsicElements["div"] & { export type TransitionDirection = "right" | "left"; export type AfterSlideEvent = React.AnimationEvent | React.TransitionEvent; -export const CarouselItem: React.FC = React.memo(({ nav, transitionDuration, afterTransition, translateX, ...props }: CarouselItemProps) => { - const [className, setClassName] = React.useState("carousel-item"); - const [style, setStyle] = React.useState({}); +export const CarouselItem: React.FC = React.memo( + React.forwardRef(({ nav, transitionDuration, afterTransition, translateX, ...props }: CarouselItemProps, ref: React.ForwardedRef) => { + const [className, setClassName] = React.useState("carousel-item"); + const [style, setStyle] = React.useState({}); - /** - * Handles resetting class name after transition or animation ends - * @param {AfterSlideEvent} e Animation or transition end event - */ - const afterSlidehandler = React.useCallback( - (e: AfterSlideEvent) => { - setClassName(classnames("carousel-item", { active: props.defaultChecked }, props.className)); - if (props.defaultChecked && afterTransition) { - e.persist(); - afterTransition(e); - } - if (e.type === "transitionend") { - props.onTransitionEnd && props.onTransitionEnd(e as React.TransitionEvent); - } else { - props.onAnimationEnd && props.onAnimationEnd(e as React.AnimationEvent); - } - }, - [props.defaultChecked, props.className, afterTransition, props.onTransitionEnd, props.onAnimationEnd] - ); + /** + * Handles resetting class name after transition or animation ends + * @param {AfterSlideEvent} e Animation or transition end event + */ + const afterSlidehandler = React.useCallback( + (e: AfterSlideEvent) => { + setClassName(classnames("carousel-item", { active: props.defaultChecked }, props.className)); + if (props.defaultChecked && afterTransition) { + e.persist(); + afterTransition(e); + } + if (e.type === "transitionend") { + props.onTransitionEnd && props.onTransitionEnd(e as React.TransitionEvent); + } else { + props.onAnimationEnd && props.onAnimationEnd(e as React.AnimationEvent); + } + }, + [props.defaultChecked, props.className, afterTransition, props.onTransitionEnd, props.onAnimationEnd] + ); - /** Handles transitioning a slide in or out */ - React.useEffect(() => { - const direction: TransitionDirection = nav === "next" ? "left" : "right"; - setClassName(classnames("carousel-item", `carousel-item-${direction}`, { [`carousel-item-${nav}`]: props.defaultChecked }, { active: !props.defaultChecked }, props.className)); - }, [nav, props.defaultChecked, props.className]); + /** Handles transitioning a slide in or out */ + React.useEffect(() => { + const direction: TransitionDirection = nav === "next" ? "left" : "right"; + setClassName(classnames("carousel-item", `carousel-item-${direction}`, { [`carousel-item-${nav}`]: props.defaultChecked }, { active: !props.defaultChecked }, props.className)); + }, [nav, props.defaultChecked, props.className]); - React.useEffect(() => setClassName(classnames("carousel-item", { active: props.defaultChecked }, props.className)), []); - React.useEffect(() => { - const animationDuration: string = (transitionDuration || defaultTransitionDuration) + "ms"; - const transform: string = translateX && props.defaultChecked ? `translate3d(${translateX}px, 0, 0)` : null; - setStyle({ - transitionDuration: transform ? "0s" : animationDuration, - animationDuration, - transform, - }); - }, [transitionDuration, props.defaultChecked, translateX]); + React.useEffect(() => setClassName(classnames("carousel-item", { active: props.defaultChecked }, props.className)), []); + React.useEffect(() => { + const animationDuration: string = (transitionDuration || defaultTransitionDuration) + "ms"; + const transform: string = translateX && props.defaultChecked ? `translate3d(${translateX}px, 0, 0)` : null; + setStyle({ + transitionDuration: transform ? "0s" : animationDuration, + animationDuration, + transform, + }); + }, [transitionDuration, props.defaultChecked, translateX]); - return ( -
    - {props.children} -
    - ); -}); + return ( +
    + {props.children} +
    + ); + }) +); From df34dc05b9622f6e36951975d6165b46efaa1905 Mon Sep 17 00:00:00 2001 From: kp Date: Tue, 12 Jan 2021 18:51:09 +0800 Subject: [PATCH 06/42] refactor(chip): add forward ref --- lib/src/Chip/Chip.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/Chip/Chip.tsx b/lib/src/Chip/Chip.tsx index 955816302..e84460dd4 100644 --- a/lib/src/Chip/Chip.tsx +++ b/lib/src/Chip/Chip.tsx @@ -7,9 +7,9 @@ export type ChipProps = JSX.IntrinsicElements["div"] & { onClose: React.MouseEventHandler; }; -export const Chip: React.FC = ({ onClose, ...props }: ChipProps) => ( -
    +export const Chip: React.FC = React.forwardRef(({ onClose, ...props }: ChipProps, ref: React.ForwardedRef) => ( +
    {props.children}
    -); +)); From adf40e0dc6d44ddb7c12ca135de2ee82a2625e19 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 09:45:02 +0800 Subject: [PATCH 07/42] refactor(carousel): add forward ref --- lib/src/Carousel/Carousel.tsx | 4 +++- lib/src/index.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/Carousel/Carousel.tsx b/lib/src/Carousel/Carousel.tsx index 34934c41f..612fbbe3a 100644 --- a/lib/src/Carousel/Carousel.tsx +++ b/lib/src/Carousel/Carousel.tsx @@ -5,6 +5,7 @@ import { CarouselItemProps, AfterSlideEvent } from "./CarouselItem"; import { CarouselIndicators } from "./CarouselIndicators"; import { CarouselNavs } from "./CarouselNavs"; import "./carousel.scss"; +import { useCombinedRefs } from "../hooks"; export type CarouselProps = JSX.IntrinsicElements["div"] & { /** Event handler triggered after change have happened to the carousel returning the index of the new active carousel slide */ @@ -50,6 +51,7 @@ export const Carousel: React.FC = React.forwardRef( const [id, setId] = React.useState(""); const [className, setClassName] = React.useState("carousel"); const [swipePos, setSwipePos] = React.useState(); + const carouselRef = useCombinedRefs(ref); const interrupted: React.MutableRefObject = React.useRef(false); const timer: React.MutableRefObject = React.useRef(); @@ -200,7 +202,7 @@ export const Carousel: React.FC = React.forwardRef( return (
    Date: Fri, 15 Jan 2021 09:47:11 +0800 Subject: [PATCH 08/42] refactor(close-button): add missing type --- lib/src/CloseButton/CloseButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/CloseButton/CloseButton.tsx b/lib/src/CloseButton/CloseButton.tsx index 9b2e64d28..5a89369ca 100644 --- a/lib/src/CloseButton/CloseButton.tsx +++ b/lib/src/CloseButton/CloseButton.tsx @@ -9,7 +9,7 @@ import "./close-button.scss"; */ export const CloseButton: React.FC = React.memo( - React.forwardRef((props: JSX.IntrinsicElements["button"], ref) => { + React.forwardRef((props: JSX.IntrinsicElements["button"], ref: React.ForwardedRef) => { return - +
    +
    + + +
    -
    - -
    - ); -}); + +
    + ); + }) +); From 03978d81bc29a46db984a6dcd92eb1b459475a09 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:06:22 +0800 Subject: [PATCH 13/42] refactor(loader): add forward ref --- lib/src/Loader/Loader.tsx | 59 ++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/lib/src/Loader/Loader.tsx b/lib/src/Loader/Loader.tsx index 73e3cc964..e1c61102e 100644 --- a/lib/src/Loader/Loader.tsx +++ b/lib/src/Loader/Loader.tsx @@ -19,31 +19,34 @@ export type LoaderProps = JSX.IntrinsicElements["div"] & { toggle?: boolean; }; -export const Loader: React.FC = React.memo(({ size = "md", type = "spinner", toggle = true, fullscreen, cover, backdrop, ...props }: LoaderProps) => { - return ( - toggle && ( -
    - - {type === "spinner" && } - {type === "square" && } - - {props.children} -
    - ) - ); -}); +export const Loader: React.FC = React.memo( + React.forwardRef(({ size = "md", type = "spinner", toggle = true, fullscreen, cover, backdrop, ...props }: LoaderProps, ref: React.ForwardedRef) => { + return ( + toggle && ( +
    + + {type === "spinner" && } + {type === "square" && } + + {props.children} +
    + ) + ); + }) +); From 3c1fd9098ac3de35a12a0b3c0f9688536c355be9 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:08:42 +0800 Subject: [PATCH 14/42] refactor(modal): add forward ref --- lib/src/Modal/Modal.tsx | 227 ++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 112 deletions(-) diff --git a/lib/src/Modal/Modal.tsx b/lib/src/Modal/Modal.tsx index ce2350015..38e198558 100644 --- a/lib/src/Modal/Modal.tsx +++ b/lib/src/Modal/Modal.tsx @@ -2,6 +2,7 @@ import React from "react"; import { createPortal } from "react-dom"; import classnames from "classnames"; import "./modal.scss"; +import { useCombinedRefs } from "../hooks"; export type ModalPosition = "left" | "right" | "default"; export type ModalSize = "lg" | "md" | "sm"; @@ -32,124 +33,126 @@ export type ModalProps = JSX.IntrinsicElements["div"] & { const safeDocument: Document | null = typeof document !== "undefined" ? document : null; /** The modal component provides a solid foundation for creating dialogs or slideout modals */ -export const Modal: React.FC = React.memo(({ trapFocus, autoFocus, centered, size, fullscreen, onEscape, onBackdropDismiss, position, toggle, ...props }: ModalProps) => { - const dialogRef: React.MutableRefObject = React.useRef(); - const [isPristine, setIsPristine] = React.useState(true); - - React.useEffect(() => { - if (toggle) { - isPristine && setIsPristine(false); - document.body.classList.add("modal-open"); - } else { - document.body.classList.remove("modal-open"); - } - }, [toggle]); - - /** Focus trap */ - React.useEffect(() => { - function tabHandler(e: KeyboardEvent) { - if (e.key.toLowerCase() === "tab") { - const lastFocusable: string = "last-focusable-element"; - const focusableElements: FocusableElements[] = Array.from(dialogRef.current.querySelectorAll("input, button, a")).filter((el) => el.className !== lastFocusable); - - if (focusableElements.length) { - if (!e.shiftKey) { - // Descending focus - if (document.activeElement.className === lastFocusable && dialogRef.current.contains(document.activeElement)) { - dialogRef.current.querySelector("input, button, a").focus(); - } else if (!dialogRef.current.contains(document.activeElement)) { - dialogRef.current.querySelector("input, button, a").focus(); - } - } else { - // Ascending focus - if ((document.activeElement.className === lastFocusable && dialogRef.current.contains(document.activeElement)) || !dialogRef.current.contains(document.activeElement)) { - focusableElements[focusableElements.length - 1].focus(); +export const Modal: React.FC = React.memo( + React.forwardRef(({ trapFocus, autoFocus, centered, size, fullscreen, onEscape, onBackdropDismiss, position, toggle, ...props }: ModalProps, ref: React.ForwardedRef) => { + const dialogRef: React.MutableRefObject = useCombinedRefs(ref); + const [isPristine, setIsPristine] = React.useState(true); + + React.useEffect(() => { + if (toggle) { + isPristine && setIsPristine(false); + document.body.classList.add("modal-open"); + } else { + document.body.classList.remove("modal-open"); + } + }, [toggle]); + + /** Focus trap */ + React.useEffect(() => { + function tabHandler(e: KeyboardEvent) { + if (e.key.toLowerCase() === "tab") { + const lastFocusable: string = "last-focusable-element"; + const focusableElements: FocusableElements[] = Array.from(dialogRef.current.querySelectorAll("input, button, a")).filter((el) => el.className !== lastFocusable); + + if (focusableElements.length) { + if (!e.shiftKey) { + // Descending focus + if (document.activeElement.className === lastFocusable && dialogRef.current.contains(document.activeElement)) { + dialogRef.current.querySelector("input, button, a").focus(); + } else if (!dialogRef.current.contains(document.activeElement)) { + dialogRef.current.querySelector("input, button, a").focus(); + } + } else { + // Ascending focus + if ((document.activeElement.className === lastFocusable && dialogRef.current.contains(document.activeElement)) || !dialogRef.current.contains(document.activeElement)) { + focusableElements[focusableElements.length - 1].focus(); + } } } } } - } - - if (trapFocus && toggle) { - document.addEventListener("keyup", tabHandler); - } else { - document.removeEventListener("keyup", tabHandler); - } - - return () => document.removeEventListener("keyup", tabHandler); - }, [trapFocus, toggle]); - - // Escape key listner - React.useEffect(() => { - function keyupListener(e: KeyboardEvent) { - e.key.toLowerCase() === "escape" && onEscape(e); - } - - if (onEscape && toggle) { - document.addEventListener("keyup", keyupListener); - } else { - document.removeEventListener("keyup", keyupListener); - } - - return () => document.removeEventListener("keyup", keyupListener); - }, [onEscape, toggle]); - - return !safeDocument - ? null - : createPortal( -
    { - props.onClick && props.onClick(e); - - const target: HTMLDivElement = e.target as any; - - if (onBackdropDismiss && target.classList.contains("rc") && target.classList.contains("modal")) { - onBackdropDismiss(e); - } - }} - onAnimationEnd={(e) => { - props.onAnimationEnd && props.onAnimationEnd(e); - - if (fullscreen && autoFocus && toggle && !dialogRef.current.contains(document.activeElement)) { - dialogRef.current.querySelector("input")?.focus(); - } - }} - > + + if (trapFocus && toggle) { + document.addEventListener("keyup", tabHandler); + } else { + document.removeEventListener("keyup", tabHandler); + } + + return () => document.removeEventListener("keyup", tabHandler); + }, [trapFocus, toggle]); + + // Escape key listner + React.useEffect(() => { + function keyupListener(e: KeyboardEvent) { + e.key.toLowerCase() === "escape" && onEscape(e); + } + + if (onEscape && toggle) { + document.addEventListener("keyup", keyupListener); + } else { + document.removeEventListener("keyup", keyupListener); + } + + return () => document.removeEventListener("keyup", keyupListener); + }, [onEscape, toggle]); + + return !safeDocument + ? null + : createPortal(
    { - if (autoFocus && toggle && !dialogRef.current.contains(document.activeElement)) { + {...props} + className={classnames( + "rc", + "modal", + { + show: toggle, + hide: !toggle && !isPristine, + "modal-centered": centered, + "modal-aside": position && position !== "default" && !fullscreen, + [`modal-aside-${[position]}`]: position && position !== "default" && !fullscreen, + "modal-fullscreen": fullscreen, + }, + props.className + )} + role={props.role || "dialog"} + tabIndex={props.tabIndex || -1} + aria-modal="true" + onClick={(e) => { + props.onClick && props.onClick(e); + + const target: HTMLDivElement = e.target as any; + + if (onBackdropDismiss && target.classList.contains("rc") && target.classList.contains("modal")) { + onBackdropDismiss(e); + } + }} + onAnimationEnd={(e) => { + props.onAnimationEnd && props.onAnimationEnd(e); + + if (fullscreen && autoFocus && toggle && !dialogRef.current.contains(document.activeElement)) { dialogRef.current.querySelector("input")?.focus(); } }} > -
    {props.children}
    - {trapFocus && ( - -
    End of focus
    -
    - )} -
    -
    , - safeDocument.body - ); -}); +
    { + if (autoFocus && toggle && !dialogRef.current.contains(document.activeElement)) { + dialogRef.current.querySelector("input")?.focus(); + } + }} + > +
    {props.children}
    + {trapFocus && ( + +
    End of focus
    +
    + )} +
    + , + safeDocument.body + ); + }) +); From 688db297df408eac82c7ef92b5faa56cca1acb52 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:15:59 +0800 Subject: [PATCH 15/42] refactor(pagination): add forward ref --- lib/src/Pagination/NumberedPagination.tsx | 40 ++-- lib/src/Pagination/Page.tsx | 20 +- lib/src/Pagination/Pagination.tsx | 214 +++++++++++----------- 3 files changed, 140 insertions(+), 134 deletions(-) diff --git a/lib/src/Pagination/NumberedPagination.tsx b/lib/src/Pagination/NumberedPagination.tsx index 0153bd212..81da44ed2 100644 --- a/lib/src/Pagination/NumberedPagination.tsx +++ b/lib/src/Pagination/NumberedPagination.tsx @@ -10,28 +10,30 @@ export interface NumberedPagesProps extends PaginationProps { hrefMask?: string; } -export const NumberedPagination: React.FC = React.memo(({ start = 1, end, hrefMask, ...props }: NumberedPagesProps) => { - const [pages, setPages] = React.useState([]); +export const NumberedPagination: React.FC = React.memo( + React.forwardRef(({ start = 1, end, hrefMask, ...props }: NumberedPagesProps, ref: React.ForwardedRef) => { + const [pages, setPages] = React.useState([]); - React.useEffect(() => { - const arr: number[] = []; + React.useEffect(() => { + const arr: number[] = []; - for (let i: number = start; i <= end; i++) { - arr.push(i); - } + for (let i: number = start; i <= end; i++) { + arr.push(i); + } - setPages(arr); - }, [start, end]); + setPages(arr); + }, [start, end]); - return ( - - {pages.map((page: number, index: number) => ( - - {page} - - ))} - - ); -}); + return ( + + {pages.map((page: number, index: number) => ( + + {page} + + ))} + + ); + }) +); export default NumberedPagination; diff --git a/lib/src/Pagination/Page.tsx b/lib/src/Pagination/Page.tsx index 5f92c9de2..23aebdddc 100644 --- a/lib/src/Pagination/Page.tsx +++ b/lib/src/Pagination/Page.tsx @@ -6,12 +6,14 @@ export type PageProps = JSX.IntrinsicElements["li"] & { href?: string; }; -export const Page: React.FC = React.memo(({ href, ...props }: PageProps) => { - return ( -
  • - e.preventDefault()} aria-disabled={props["data-disabled"]}> - {props.children} - -
  • - ); -}); +export const Page: React.FC = React.memo( + React.forwardRef(({ href, ...props }: PageProps, ref: React.ForwardedRef) => { + return ( +
  • + e.preventDefault()} aria-disabled={props["data-disabled"]}> + {props.children} + +
  • + ); + }) +); diff --git a/lib/src/Pagination/Pagination.tsx b/lib/src/Pagination/Pagination.tsx index 0b560cf1c..d5cf19775 100644 --- a/lib/src/Pagination/Pagination.tsx +++ b/lib/src/Pagination/Pagination.tsx @@ -42,114 +42,116 @@ export type PaginationProps = JSX.IntrinsicElements["nav"] & { }; export const Pagination: React.FunctionComponent = React.memo( - ({ navs = {}, offset = 5, onPageChange, size = "md", useDotNav, showFirstAndLast: useFirstAndLast, value = 0, ...props }: PaginationProps) => { - const total: number = React.Children.count(props.children); - const indexOfLastItem: number = total - 1; - const disablePrev: boolean = total < 2 || value === 0; - const disableNext: boolean = total < 2 || value === indexOfLastItem; - - const renderPages = (): React.ReactElement[] => { - const childrenArray: React.ReactElement[] = - React.Children.map(props.children, (Child: React.ReactElement, i: number) => - React.isValidElement(Child) - ? React.cloneElement(Child, { - "data-active": value === i, - "data-index-number": i, - key: i, - onClick: (e: React.MouseEvent) => { - onPageChange && onPageChange(parseInt(e.currentTarget.dataset.indexNumber, 10)); - }, - }) - : Child - ) || []; - - if (offset) { - /** The distance between the current value and the offset from the left. Example: ...👉|3|4|👈|(5)|6|7|... */ - const offsetToValue: number = value - Math.floor(offset / 2); - /** The distance between the current value and the offset from the right. Example: ...|3|4|(5)|👉|6|7|👈... */ - const valueToOffset: number = value + Math.floor(offset / 2); - - let offsetFrom: number = offsetToValue; - let offsetTo: number = valueToOffset; - - if (offsetToValue < 0) { - offsetTo += 0 - offsetToValue; - offsetTo = offsetTo > indexOfLastItem ? indexOfLastItem : offsetTo; - } - if (valueToOffset > indexOfLastItem) { - offsetFrom -= valueToOffset - indexOfLastItem; - offsetFrom = offsetFrom < 0 ? 0 : offsetFrom; - } - - let filteredArray: React.ReactElement[] = childrenArray.filter((_: any, i: number) => i >= offsetFrom && i <= offsetTo); - - if (!useDotNav) { - if (parseInt(filteredArray[0]?.props["data-index-number"], 10) > 0) { - filteredArray = [ - - ... - , - ...filteredArray, - ]; + React.forwardRef( + ({ navs = {}, offset = 5, onPageChange, size = "md", useDotNav, showFirstAndLast: useFirstAndLast, value = 0, ...props }: PaginationProps, ref: React.ForwardedRef) => { + const total: number = React.Children.count(props.children); + const indexOfLastItem: number = total - 1; + const disablePrev: boolean = total < 2 || value === 0; + const disableNext: boolean = total < 2 || value === indexOfLastItem; + + const renderPages = (): React.ReactElement[] => { + const childrenArray: React.ReactElement[] = + React.Children.map(props.children, (Child: React.ReactElement, i: number) => + React.isValidElement(Child) + ? React.cloneElement(Child, { + "data-active": value === i, + "data-index-number": i, + key: i, + onClick: (e: React.MouseEvent) => { + onPageChange && onPageChange(parseInt(e.currentTarget.dataset.indexNumber, 10)); + }, + }) + : Child + ) || []; + + if (offset) { + /** The distance between the current value and the offset from the left. Example: ...👉|3|4|👈|(5)|6|7|... */ + const offsetToValue: number = value - Math.floor(offset / 2); + /** The distance between the current value and the offset from the right. Example: ...|3|4|(5)|👉|6|7|👈... */ + const valueToOffset: number = value + Math.floor(offset / 2); + + let offsetFrom: number = offsetToValue; + let offsetTo: number = valueToOffset; + + if (offsetToValue < 0) { + offsetTo += 0 - offsetToValue; + offsetTo = offsetTo > indexOfLastItem ? indexOfLastItem : offsetTo; } - if (parseInt(filteredArray[filteredArray.length - 1]?.props["data-index-number"], 10) < indexOfLastItem) { - filteredArray.push( - - ... - - ); + if (valueToOffset > indexOfLastItem) { + offsetFrom -= valueToOffset - indexOfLastItem; + offsetFrom = offsetFrom < 0 ? 0 : offsetFrom; } - } - return filteredArray; - } else { - return childrenArray; - } - }; - - const filteredPages = renderPages(); - - const showFirst: boolean = (useFirstAndLast && !useDotNav) || (useDotNav && !disablePrev && filteredPages[0].props["data-index-number"] != 0); - const showLast: boolean = (useFirstAndLast && !useDotNav) || (useDotNav && !disableNext && filteredPages[filteredPages.length - 1].props["data-index-number"] != indexOfLastItem); - const disableFirst: boolean = disablePrev || filteredPages[0].key !== "pre-ellipsis"; - const disableLast: boolean = disableNext || filteredPages[filteredPages.length - 1].key !== "post-ellipsis"; - - return ( - - ); - } + ); + } + } + return filteredArray; + } else { + return childrenArray; + } + }; + + const filteredPages = renderPages(); + + const showFirst: boolean = (useFirstAndLast && !useDotNav) || (useDotNav && !disablePrev && filteredPages[0].props["data-index-number"] != 0); + const showLast: boolean = (useFirstAndLast && !useDotNav) || (useDotNav && !disableNext && filteredPages[filteredPages.length - 1].props["data-index-number"] != indexOfLastItem); + const disableFirst: boolean = disablePrev || filteredPages[0].key !== "pre-ellipsis"; + const disableLast: boolean = disableNext || filteredPages[filteredPages.length - 1].key !== "post-ellipsis"; + + return ( + + ); + } + ) ); From ce7bd145ccadd5d2df42439f67c12ee760c9b6d5 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:17:22 +0800 Subject: [PATCH 16/42] refactor(progress): add forward ref --- lib/src/ProgressBar/ProgressBar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/ProgressBar/ProgressBar.tsx b/lib/src/ProgressBar/ProgressBar.tsx index 8edf94a39..a53d6d068 100644 --- a/lib/src/ProgressBar/ProgressBar.tsx +++ b/lib/src/ProgressBar/ProgressBar.tsx @@ -7,6 +7,6 @@ export type ProgressBarProps = JSX.IntrinsicElements["progress"] & { theme?: "purple" | "primary" | "danger" | "success" | "warning" | "inverted"; }; /** A visual representation of progress for loading content. */ -export const ProgressBar: React.FC = ({ theme = "primary", ...props }: ProgressBarProps) => { - return ; -}; +export const ProgressBar: React.FC = React.forwardRef(({ theme = "primary", ...props }: ProgressBarProps, ref: React.ForwardedRef) => { + return ; +}); From e60525f327aee58595c022fcd4f4b1e3f6cc7677 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:19:28 +0800 Subject: [PATCH 17/42] refactor(radio-button): add forward ref --- lib/src/RadioButton/RadioButton.tsx | 6 +++--- lib/src/RadioButton/RadioGroup.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/RadioButton/RadioButton.tsx b/lib/src/RadioButton/RadioButton.tsx index b486dfaff..144e69c01 100644 --- a/lib/src/RadioButton/RadioButton.tsx +++ b/lib/src/RadioButton/RadioButton.tsx @@ -12,7 +12,7 @@ export type RadioButtonProps = Omit = ({ children, indicator, wrapperProps = {}, ...props }: RadioButtonProps) => { +export const RadioButton: React.FC = React.forwardRef(({ children, indicator, wrapperProps = {}, ...props }: RadioButtonProps, ref: React.ForwardedRef) => { const [id, setId] = React.useState(""); React.useEffect(() => setId(props.id || randomId("radiobtn-")), [props.id]); @@ -21,7 +21,7 @@ export const RadioButton: React.FC = ({ children, indicator, w
    - + {children && (
    ); -}; +}); diff --git a/lib/src/RadioButton/RadioGroup.tsx b/lib/src/RadioButton/RadioGroup.tsx index c13298764..768ca413e 100644 --- a/lib/src/RadioButton/RadioGroup.tsx +++ b/lib/src/RadioButton/RadioGroup.tsx @@ -16,9 +16,9 @@ export type RadioGroupProps = JSX.IntrinsicElements["div"] onChange?: React.ChangeEventHandler; }; /** A radio button allows a user to select a single item from a predefined list of options. Radio buttons are common to use in forms, i.e when you apply for a loan and need to enter "Yes" or "No". */ -export const RadioGroup: React.FC = ({ name, indicator, disabled, value, onChange, ...props }: RadioGroupProps) => ( +export const RadioGroup: React.FC = React.forwardRef(({ name, indicator, disabled, value, onChange, ...props }: RadioGroupProps, ref: React.ForwardedRef) => ( -
    +
    {React.Children.map(props.children, (Child: React.ReactElement) => React.isValidElement>(Child) ? React.cloneElement(Child, { @@ -30,4 +30,4 @@ export const RadioGroup: React.FC = ({ name, indicator, disable )}
    -); +)); From b746522e77a5004060f96d01e1189602909b4b01 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:24:32 +0800 Subject: [PATCH 18/42] refactor(slider): add forward ref --- lib/src/Slider/Slider.tsx | 301 ++++++++++++++++++-------------------- 1 file changed, 146 insertions(+), 155 deletions(-) diff --git a/lib/src/Slider/Slider.tsx b/lib/src/Slider/Slider.tsx index c00c711a7..41e5ac99c 100644 --- a/lib/src/Slider/Slider.tsx +++ b/lib/src/Slider/Slider.tsx @@ -59,177 +59,168 @@ type AppearanceStyleMap = { }; /** The component allows for easy adjustments of a value and check the updated result immediately. */ -export const Slider: React.FC = ({ - alwaysShowTooltip, - label, - labels, - max, - min, - showTicks, - step, - theme = "primary", - alternative, - tooltipTheme = "inverted", - tooltipValue, - indicator, - ...props -}: SliderProps) => { - const [minValue, setMinValue] = React.useState(min || 0); - const [maxValue, setMaxValue] = React.useState(max || 100); - const [size, setSize] = React.useState(0); - const [labelsPositions, setLabelsPositions] = React.useState>([]); - const [thumbPosition, setThumbPosition] = React.useState(0); - const [activeTrackStyles, setActiveTrackStyles] = React.useState({}); - const appearanceSizesMap: AppearanceStyleMap = { - alternative: { width: "27px", offset: "56px" }, - normal: { width: "5px", offset: "24px" }, - }; - const appearance: SliderAppearance = alternative ? "alternative" : "normal"; +export const Slider: React.FC = React.forwardRef( + ( + { alwaysShowTooltip, label, labels, max, min, showTicks, step, theme = "primary", alternative, tooltipTheme = "inverted", tooltipValue, indicator, ...props }: SliderProps, + ref: React.ForwardedRef + ) => { + const [minValue, setMinValue] = React.useState(min || 0); + const [maxValue, setMaxValue] = React.useState(max || 100); + const [size, setSize] = React.useState(0); + const [labelsPositions, setLabelsPositions] = React.useState>([]); + const [thumbPosition, setThumbPosition] = React.useState(0); + const [activeTrackStyles, setActiveTrackStyles] = React.useState({}); + const appearanceSizesMap: AppearanceStyleMap = { + alternative: { width: "27px", offset: "56px" }, + normal: { width: "5px", offset: "24px" }, + }; + const appearance: SliderAppearance = alternative ? "alternative" : "normal"; - React.useEffect(() => { - // Checking if the min or max are not numbers, null value or undefined - const minValue: number = typeof min !== "number" ? 0 : min; - const maxValue: number = typeof max !== "number" ? 100 : max; - setMinValue(minValue); - setMaxValue(maxValue); - setSize(getSize(minValue, maxValue)); - }, [min, max]); + React.useEffect(() => { + // Checking if the min or max are not numbers, null value or undefined + const minValue: number = typeof min !== "number" ? 0 : min; + const maxValue: number = typeof max !== "number" ? 100 : max; + setMinValue(minValue); + setMaxValue(maxValue); + setSize(getSize(minValue, maxValue)); + }, [min, max]); - React.useEffect(() => { - if (labels && labels.length) { - const positions: Array = []; - labels.map((label: SliderLabel) => { - positions.push(getLabelPosition(label.position) + "%"); - }); - setLabelsPositions(positions); - } - }, [labels, minValue, maxValue]); + React.useEffect(() => { + if (labels && labels.length) { + const positions: Array = []; + labels.map((label: SliderLabel) => { + positions.push(getLabelPosition(label.position) + "%"); + }); + setLabelsPositions(positions); + } + }, [labels, minValue, maxValue]); - React.useEffect(() => { - setThumbPosition(getPercentage()); - setActiveTrackStyles(getActiveTrackStyles()); - }, [props.value, minValue, maxValue, size, appearance]); + React.useEffect(() => { + setThumbPosition(getPercentage()); + setActiveTrackStyles(getActiveTrackStyles()); + }, [props.value, minValue, maxValue, size, appearance]); - /** - * Finds the size between two numbers - * @param {number} minValue The minimum value - * @param {number} maxValue The maximum value - * @returns {number} The size - */ - function getSize(minValue: number, maxValue: number): number { - if (maxValue > minValue) { - return maxValue - minValue; - } else { - // Will calculate the size anyway, but it will show a warning since the min is larger than the max - console.warn(`The max value of the slider should be larger than the min value (Max:${maxValue}, Min: ${minValue}`); - return minValue - maxValue; + /** + * Finds the size between two numbers + * @param {number} minValue The minimum value + * @param {number} maxValue The maximum value + * @returns {number} The size + */ + function getSize(minValue: number, maxValue: number): number { + if (maxValue > minValue) { + return maxValue - minValue; + } else { + // Will calculate the size anyway, but it will show a warning since the min is larger than the max + console.warn(`The max value of the slider should be larger than the min value (Max:${maxValue}, Min: ${minValue}`); + return minValue - maxValue; + } } - } - /** - * Converts the current value to percentage based on min and max - * @returns {number} The precentage - */ - function getPercentage(): number { - if (props.value <= minValue) { - return 0; - } else if (props.value >= maxValue) { - return 100; - } else { - const distanceFromMin: number = Math.abs(props.value - minValue); - return size ? (distanceFromMin / size) * 100 : 0; + /** + * Converts the current value to percentage based on min and max + * @returns {number} The precentage + */ + function getPercentage(): number { + if (props.value <= minValue) { + return 0; + } else if (props.value >= maxValue) { + return 100; + } else { + const distanceFromMin: number = Math.abs(props.value - minValue); + return size ? (distanceFromMin / size) * 100 : 0; + } } - } - /** - * Calculates the styles needed for the active track - * @returns {React.CSSProperties} The active track styles object - */ - const getActiveTrackStyles: () => React.CSSProperties = React.useCallback(() => { - const calculatedThumbPosition: number = getPercentage(); - let zeroPosition: number; - const { width, offset }: AppearanceStyleMap[keyof AppearanceStyleMap] = appearanceSizesMap[appearance]; - const style: React.CSSProperties = {}; - if (minValue >= 0) { - zeroPosition = 0; - style.left = `${zeroPosition}%`; - style.width = `calc(${calculatedThumbPosition}% + ${width})`; - } else if (maxValue <= 0) { - zeroPosition = 100; - style.left = `calc(${zeroPosition}% + ${offset})`; - style.width = `calc(${100 - calculatedThumbPosition}% + ${width})`; - style.transform = "rotateY(180deg)"; - } else { - if (props.value <= 0) { - zeroPosition = size ? Math.abs((minValue / size) * 100) : 0; - style.left = `calc(${zeroPosition}% + ${width})`; - style.width = zeroPosition - calculatedThumbPosition + "%"; + /** + * Calculates the styles needed for the active track + * @returns {React.CSSProperties} The active track styles object + */ + const getActiveTrackStyles: () => React.CSSProperties = React.useCallback(() => { + const calculatedThumbPosition: number = getPercentage(); + let zeroPosition: number; + const { width, offset }: AppearanceStyleMap[keyof AppearanceStyleMap] = appearanceSizesMap[appearance]; + const style: React.CSSProperties = {}; + if (minValue >= 0) { + zeroPosition = 0; + style.left = `${zeroPosition}%`; + style.width = `calc(${calculatedThumbPosition}% + ${width})`; + } else if (maxValue <= 0) { + zeroPosition = 100; + style.left = `calc(${zeroPosition}% + ${offset})`; + style.width = `calc(${100 - calculatedThumbPosition}% + ${width})`; style.transform = "rotateY(180deg)"; } else { - zeroPosition = size ? Math.abs(100 - (maxValue / size) * 100) : 0; - style.left = `calc(${zeroPosition}% + ${width})`; - style.width = calculatedThumbPosition - zeroPosition + "%"; + if (props.value <= 0) { + zeroPosition = size ? Math.abs((minValue / size) * 100) : 0; + style.left = `calc(${zeroPosition}% + ${width})`; + style.width = zeroPosition - calculatedThumbPosition + "%"; + style.transform = "rotateY(180deg)"; + } else { + zeroPosition = size ? Math.abs(100 - (maxValue / size) * 100) : 0; + style.left = `calc(${zeroPosition}% + ${width})`; + style.width = calculatedThumbPosition - zeroPosition + "%"; + } } - } - return style; - }, [appearance, props.value, getPercentage]); + return style; + }, [appearance, props.value, getPercentage]); - /** - * Calculating the position of the label based on it's value - * @param {number} value The Slider value - * @returns {number} The position of the label in percentage - */ - function getLabelPosition(value: number): number { - if (value >= maxValue) { - return 100; - } else if (value <= minValue) { - return 0; + /** + * Calculating the position of the label based on it's value + * @param {number} value The Slider value + * @returns {number} The position of the label in percentage + */ + function getLabelPosition(value: number): number { + if (value >= maxValue) { + return 100; + } else if (value <= minValue) { + return 0; + } + return Math.abs(((value - minValue) / (maxValue - minValue)) * 100); } - return Math.abs(((value - minValue) / (maxValue - minValue)) * 100); - } - /** - * Determines whether to enable or disable CSS transitions based on the total amount of steps - * This is fix for a performance impact caused by rapidly updating the state when sliding - * @var maxNumberOfStepsToAllowTransition represents the maximum number of steps to have the - * transitions enabled. Transitions would be disabled when exceeding that number; - * @returns {boolean} `True` if it should transition - */ - function shouldEnableTransition(): boolean { - const maxNumberOfStepsToAllowTransition: number = 30; - return size / step <= maxNumberOfStepsToAllowTransition; - } + /** + * Determines whether to enable or disable CSS transitions based on the total amount of steps + * This is fix for a performance impact caused by rapidly updating the state when sliding + * @var maxNumberOfStepsToAllowTransition represents the maximum number of steps to have the + * transitions enabled. Transitions would be disabled when exceeding that number; + * @returns {boolean} `True` if it should transition + */ + function shouldEnableTransition(): boolean { + const maxNumberOfStepsToAllowTransition: number = 30; + return size / step <= maxNumberOfStepsToAllowTransition; + } - return ( - -
    - {label && } -
    - -
    -
    -
    -
    -
    -
    {tooltipValue || props.value}
    - {appearance === "alternative" ? ( - <> - {angleLeftIcon} - {angleRightIcon} - - ) : null} + return ( + +
    + {label && } +
    + +
    +
    +
    +
    +
    +
    {tooltipValue || props.value}
    + {appearance === "alternative" ? ( + <> + {angleLeftIcon} + {angleRightIcon} + + ) : null} +
    + {labels && labels.length + ? labels.map((label: SliderLabel, i: number) => ( +
    + {label.label} +
    + )) + : null}
    - {labels && labels.length - ? labels.map((label: SliderLabel, i: number) => ( -
    - {label.label} -
    - )) - : null}
    -
    - - ); -}; + + ); + } +); From 0db0e50a671c9a67cc38f652d2ec56af7065dcf1 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:25:02 +0800 Subject: [PATCH 19/42] refactor(rating): add forward ref --- lib/src/Rating/Rating.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/Rating/Rating.tsx b/lib/src/Rating/Rating.tsx index 2b5e1052f..9eeb7c27f 100644 --- a/lib/src/Rating/Rating.tsx +++ b/lib/src/Rating/Rating.tsx @@ -23,7 +23,7 @@ export type RatingProps = JSX.IntrinsicElements["input"] & { const initialColors: [string, string] = ["#A9A9A9", "#FFC500"]; const disabledColors: [string, string] = ["#dddddd", "#bfbfbf"]; -export const Rating: React.FC = ({ dimension = 30, colors, customSVG, wrapperProps, ...props }: RatingProps) => { +export const Rating: React.FC = React.forwardRef(({ dimension = 30, colors, customSVG, wrapperProps, ...props }: RatingProps, ref: React.ForwardedRef) => { const [displayValue, setDisplayValue] = useState(Number(props.value)); const [min, setMin] = useState(0); const [max, setMax] = useState(Number(props.max) || 5); @@ -96,8 +96,9 @@ export const Rating: React.FC = ({ dimension = 30, colors, customSV ))}
    = ({ dimension = 30, colors, customSV />
    ); -}; +}); From f3e18dfde369b21d1cfab15f22e88066d27bbef5 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:26:22 +0800 Subject: [PATCH 20/42] refactor(stepper): add forward ref --- lib/src/Stepper/Stepper.tsx | 68 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/lib/src/Stepper/Stepper.tsx b/lib/src/Stepper/Stepper.tsx index 6915f5824..0696df3b8 100644 --- a/lib/src/Stepper/Stepper.tsx +++ b/lib/src/Stepper/Stepper.tsx @@ -18,40 +18,42 @@ export type StepperProps = JSX.IntrinsicElements["input"] & { }; /** A stepper makes it easier to input values that are in a narrow range */ -export const Stepper: React.FC = ({ label, onDecrease, onIncrease, indicator, wrapperProps = {}, ...props }: StepperProps) => { - const [id, setId] = React.useState(""); +export const Stepper: React.FC = React.forwardRef( + ({ label, onDecrease, onIncrease, indicator, wrapperProps = {}, ...props }: StepperProps, ref: React.ForwardedRef) => { + const [id, setId] = React.useState(""); - React.useEffect(() => { - setId(props.id ? props.id : randomId("stepper-")); - }, [props.id]); + React.useEffect(() => { + setId(props.id ? props.id : randomId("stepper-")); + }, [props.id]); - return ( - -
    - {label && } -
    - -
    - {props.value} + return ( + +
    + {label && } +
    + +
    + {props.value} +
    +
    - +
    - -
    - - ); -}; + + ); + } +); From 2c44cb54458f1aa39018677dd1b924f5a174d47b Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:28:04 +0800 Subject: [PATCH 21/42] refactor(step-tracker): add forward ref --- lib/src/StepTracker/StepTracker.tsx | 110 ++++++++++++++-------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/lib/src/StepTracker/StepTracker.tsx b/lib/src/StepTracker/StepTracker.tsx index 104f4a9e9..bd7933c03 100644 --- a/lib/src/StepTracker/StepTracker.tsx +++ b/lib/src/StepTracker/StepTracker.tsx @@ -27,64 +27,66 @@ export type StepTrackerProps = Omit & { useNumbers?: boolean; }; /** Step trackers illustrate the steps in a multi step process */ -export const StepTracker: React.FC = React.memo(({ labelPosition = "bottom", list, onClick, orientation = "horizontal", step, useNumbers, ...props }: StepTrackerProps) => { - const [isVertical, setIsVertical] = React.useState(orientation === "vertical"); - const [stepList, setStepList] = React.useState>([]); +export const StepTracker: React.FC = React.memo( + React.forwardRef(({ labelPosition = "bottom", list, onClick, orientation = "horizontal", step, useNumbers, ...props }: StepTrackerProps, ref: React.ForwardedRef) => { + const [isVertical, setIsVertical] = React.useState(orientation === "vertical"); + const [stepList, setStepList] = React.useState>([]); - const getProgress = React.useCallback( - (pos: number): string => { - return (100 / (stepList.length - 1)) * pos + "%"; - }, - [stepList] - ); + const getProgress = React.useCallback( + (pos: number): string => { + return (100 / (stepList.length - 1)) * pos + "%"; + }, + [stepList] + ); - const getStyles = React.useCallback( - (key: keyof React.CSSProperties, pos: number): React.CSSProperties => { - return { [key]: getProgress(pos) }; - }, - [getProgress] - ); + const getStyles = React.useCallback( + (key: keyof React.CSSProperties, pos: number): React.CSSProperties => { + return { [key]: getProgress(pos) }; + }, + [getProgress] + ); - React.useEffect(() => { - setIsVertical(orientation === "vertical"); - }, [orientation]); + React.useEffect(() => { + setIsVertical(orientation === "vertical"); + }, [orientation]); - React.useEffect(() => { - setStepList((list ? list : React.Children.toArray(props.children)).map((value: null, i: number) => i)); - }, [props.children, list]); + React.useEffect(() => { + setStepList((list ? list : React.Children.toArray(props.children)).map((value: null, i: number) => i)); + }, [props.children, list]); - return ( -
    -
    -
    -
    -
    - {stepList.map((i: number) => ( -
    onClick && onClick(i)} - key={i} - > -
    - {checkIcon} -
    {i + 1}
    + return ( +
    +
    +
    +
    - ))} -
    -
    - {list?.map((item: StepLabelProps, i: number) => ( - - ))} - {React.Children.map(props.children, (Child: React.ReactElement, i: number) => - React.isValidElement>(Child) - ? React.cloneElement(Child, { - isActive: step === i, - style: isVertical ? null : getStyles("width", 1), - }) - : Child - )} + {stepList.map((i: number) => ( +
    onClick && onClick(i)} + key={i} + > +
    + {checkIcon} +
    {i + 1}
    +
    + ))} +
    +
    + {list?.map((item: StepLabelProps, i: number) => ( + + ))} + {React.Children.map(props.children, (Child: React.ReactElement, i: number) => + React.isValidElement>(Child) + ? React.cloneElement(Child, { + isActive: step === i, + style: isVertical ? null : getStyles("width", 1), + }) + : Child + )} +
    -
    - ); -}); + ); + }) +); From 16f2b79852d746218d6d6b330e5c664eb134a9f2 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:43:32 +0800 Subject: [PATCH 22/42] refactor(table): add forward ref --- lib/src/Table/Table.tsx | 18 +-- lib/src/Table/parts/TableBody.tsx | 6 +- lib/src/Table/parts/TableCell.tsx | 6 +- lib/src/Table/parts/TableHeader.tsx | 6 +- lib/src/Table/parts/TableHeaderCell.tsx | 98 +++++++------- lib/src/Table/parts/TableRow.tsx | 168 ++++++++++++------------ 6 files changed, 150 insertions(+), 152 deletions(-) diff --git a/lib/src/Table/Table.tsx b/lib/src/Table/Table.tsx index a2755b756..15ffd8b64 100644 --- a/lib/src/Table/Table.tsx +++ b/lib/src/Table/Table.tsx @@ -17,12 +17,14 @@ export type TableProps = JSX.IntrinsicElements["table"] & { }; export const Table: React.FunctionComponent = React.memo( - ({ onRowSelect, onRowExpand, onSort, theme = "light", ...props }: TableProps): React.ReactElement => { - const [tableState, setTableState] = React.useState({ expandedRows: [], sortedColumn: null }); - return ( - - - - ); - } + React.forwardRef( + ({ onRowSelect, onRowExpand, onSort, theme = "light", ...props }: TableProps, ref: React.ForwardedRef): React.ReactElement => { + const [tableState, setTableState] = React.useState({ expandedRows: [], sortedColumn: null }); + return ( + +
    + + ); + } + ) ); diff --git a/lib/src/Table/parts/TableBody.tsx b/lib/src/Table/parts/TableBody.tsx index b24c0c096..609d250bc 100644 --- a/lib/src/Table/parts/TableBody.tsx +++ b/lib/src/Table/parts/TableBody.tsx @@ -3,7 +3,7 @@ import { TableRowProps } from "./TableRow"; export type TableBodyProps = JSX.IntrinsicElements["tbody"]; -const TableBody: React.FC = ({ ...props }: TableBodyProps) => { +const TableBody: React.FC = React.forwardRef(({ ...props }: TableBodyProps, ref: React.ForwardedRef) => { let parentKey: string; /** @@ -20,7 +20,7 @@ const TableBody: React.FC = ({ ...props }: TableBodyProps) => { }, []); return ( - + {React.Children.map(props.children, (Child: React.ReactElement, i: number) => { if (Child?.type === React.Fragment) { return React.cloneElement(Child, { @@ -32,7 +32,7 @@ const TableBody: React.FC = ({ ...props }: TableBodyProps) => { })} ); -}; +}); TableBody.displayName = "TableBody"; diff --git a/lib/src/Table/parts/TableCell.tsx b/lib/src/Table/parts/TableCell.tsx index cce750e34..d153adc80 100644 --- a/lib/src/Table/parts/TableCell.tsx +++ b/lib/src/Table/parts/TableCell.tsx @@ -2,9 +2,9 @@ import React from "react"; export type TableCellProps = JSX.IntrinsicElements["td"]; -const TableCell: React.FC = ({ ...props }: TableCellProps) => { - return + {React.Children.count(props.children) === 1 && React.isValidElement(props.children) ? React.cloneElement(props.children, { isHeaderRow: true, index: -1 }) : React.Children.map(props.children, (Child: React.ReactElement, i: number) => { @@ -18,7 +18,7 @@ const TableHeader: React.FC = ({ ...props }: TableHeaderProps) })} ); -}; +}); TableHeader.displayName = "TableHeader"; diff --git a/lib/src/Table/parts/TableHeaderCell.tsx b/lib/src/Table/parts/TableHeaderCell.tsx index dfe84a6cb..1922bea0c 100644 --- a/lib/src/Table/parts/TableHeaderCell.tsx +++ b/lib/src/Table/parts/TableHeaderCell.tsx @@ -22,61 +22,63 @@ const defaultSort: JSX.Element = ( ); -const TableHeaderCell: React.FC = ({ accessor, disableSort, className, sortDirection, ...props }: TableHeaderCellProps) => { - const context = React.useContext(TableContext); - const [sortedColumn, setSortedColumn] = React.useState(context.tableState.sortedColumn); - const [sortOrder, setSortOrder] = React.useState(SortDirection.ASC); - const [sortable, setSortable] = React.useState(!disableSort || context.onSort); +const TableHeaderCell: React.FC = React.forwardRef( + ({ accessor, disableSort, className, sortDirection, ...props }: TableHeaderCellProps, ref: React.ForwardedRef) => { + const context = React.useContext(TableContext); + const [sortedColumn, setSortedColumn] = React.useState(context.tableState.sortedColumn); + const [sortOrder, setSortOrder] = React.useState(SortDirection.ASC); + const [sortable, setSortable] = React.useState(!disableSort || context.onSort); - /** - * get latest sort direction - * @param oldSortDirection current sort direction - */ - const getSortDirection = (oldSortDirection: SortDirection): SortDirection => { - return oldSortDirection === SortDirection.ASC ? SortDirection.DESC : SortDirection.ASC; - }; + /** + * get latest sort direction + * @param oldSortDirection current sort direction + */ + const getSortDirection = (oldSortDirection: SortDirection): SortDirection => { + return oldSortDirection === SortDirection.ASC ? SortDirection.DESC : SortDirection.ASC; + }; - /** on column sort */ - const onSort = React.useCallback(() => { - const newSortedColumn: SortedColumn = - sortedColumn && sortedColumn.accessor === accessor ? { ...sortedColumn, sortDirection: getSortDirection(sortedColumn.sortDirection) } : { accessor, sortDirection: SortDirection.DESC }; - context.setTableState({ ...context.tableState, sortedColumn: newSortedColumn }); - !!newSortedColumn && context.onSort(newSortedColumn); - }, [sortedColumn, context]); + /** on column sort */ + const onSort = React.useCallback(() => { + const newSortedColumn: SortedColumn = + sortedColumn && sortedColumn.accessor === accessor ? { ...sortedColumn, sortDirection: getSortDirection(sortedColumn.sortDirection) } : { accessor, sortDirection: SortDirection.DESC }; + context.setTableState({ ...context.tableState, sortedColumn: newSortedColumn }); + !!newSortedColumn && context.onSort(newSortedColumn); + }, [sortedColumn, context]); - React.useEffect(() => { - setSortable(!disableSort && !!context.onSort); - }, [disableSort, context.onSort]); + React.useEffect(() => { + setSortable(!disableSort && !!context.onSort); + }, [disableSort, context.onSort]); - React.useEffect(() => { - setSortedColumn(context.tableState.sortedColumn); - }, [context.tableState]); + React.useEffect(() => { + setSortedColumn(context.tableState.sortedColumn); + }, [context.tableState]); - React.useEffect(() => { - if (sortDirection && context.onSort) { - context.setTableState({ ...context.tableState, sortedColumn: { accessor, sortDirection } }); - } - }, [sortDirection, context.onSort]); + React.useEffect(() => { + if (sortDirection && context.onSort) { + context.setTableState({ ...context.tableState, sortedColumn: { accessor, sortDirection } }); + } + }, [sortDirection, context.onSort]); - React.useEffect(() => { - setSortOrder(sortable && sortedColumn?.accessor === accessor ? sortedColumn?.sortDirection : null); - }, [sortable, sortedColumn]); + React.useEffect(() => { + setSortOrder(sortable && sortedColumn?.accessor === accessor ? sortedColumn?.sortDirection : null); + }, [sortable, sortedColumn]); - return ( - - ); -}; + return ( + + ); + } +); TableHeaderCell.displayName = "TableHeaderCell"; diff --git a/lib/src/Table/parts/TableRow.tsx b/lib/src/Table/parts/TableRow.tsx index 21c56347f..1d9fc1ad8 100644 --- a/lib/src/Table/parts/TableRow.tsx +++ b/lib/src/Table/parts/TableRow.tsx @@ -28,103 +28,97 @@ const angleRightIcon: JSX.Element = ( ); -const TableRow: React.FC = ({ - className, - isHeaderRow, - hideSelect, - uniqueKey, - parentKey, - checked = false, - indeterminate = false, - isSubRow = false, - isExpanded = false, - ...props -}: TableRowProps) => { - const context = React.useContext(TableContext); - const [uniqueId, setUniqueId] = React.useState(uniqueKey); - const [isShown, setIsShown] = React.useState(false); - const [expanded, setExpanded] = React.useState(isExpanded); - const [isParentRow, setIsParentRow] = React.useState(isExpanded); - const [columnProps, setColumnProps] = React.useState(null); - const [expandedRows, setExpandedRows] = React.useState>(context.tableState?.expandedRows || []); +const TableRow: React.FC = React.forwardRef( + ( + { className, isHeaderRow, hideSelect, uniqueKey, parentKey, checked = false, indeterminate = false, isSubRow = false, isExpanded = false, ...props }: TableRowProps, + ref: React.ForwardedRef + ) => { + const context = React.useContext(TableContext); + const [uniqueId, setUniqueId] = React.useState(uniqueKey); + const [isShown, setIsShown] = React.useState(false); + const [expanded, setExpanded] = React.useState(isExpanded); + const [isParentRow, setIsParentRow] = React.useState(isExpanded); + const [columnProps, setColumnProps] = React.useState(null); + const [expandedRows, setExpandedRows] = React.useState>(context.tableState?.expandedRows || []); - /** initiate default expanded row */ - const initiateExpandedRows = React.useCallback(() => { - const newExpandedRows: Array = [...expandedRows]; - const expandedIndex: number = newExpandedRows.indexOf(uniqueId); - if (isExpanded && expandedIndex === -1) { - newExpandedRows.push(uniqueId); - } else if (expandedIndex > -1) { + /** initiate default expanded row */ + const initiateExpandedRows = React.useCallback(() => { + const newExpandedRows: Array = [...expandedRows]; const expandedIndex: number = newExpandedRows.indexOf(uniqueId); - newExpandedRows.splice(expandedIndex, 1); - } - context.setTableState({ ...context.tableState, expandedRows: newExpandedRows }); - setExpandedRows(newExpandedRows); - }, [isExpanded, uniqueId]); + if (isExpanded && expandedIndex === -1) { + newExpandedRows.push(uniqueId); + } else if (expandedIndex > -1) { + const expandedIndex: number = newExpandedRows.indexOf(uniqueId); + newExpandedRows.splice(expandedIndex, 1); + } + context.setTableState({ ...context.tableState, expandedRows: newExpandedRows }); + setExpandedRows(newExpandedRows); + }, [isExpanded, uniqueId]); - React.useEffect(() => { - setUniqueId(isHeaderRow ? "all" : uniqueKey || randomId("table-row")); - }, [uniqueKey, isHeaderRow]); + React.useEffect(() => { + setUniqueId(isHeaderRow ? "all" : uniqueKey || randomId("table-row")); + }, [uniqueKey, isHeaderRow]); - React.useEffect(() => { - setExpandedRows(context.tableState.expandedRows || []); - }, [context.tableState.expandedRows]); + React.useEffect(() => { + setExpandedRows(context.tableState.expandedRows || []); + }, [context.tableState.expandedRows]); - React.useEffect(() => { - setExpanded(isExpanded); - if (!isSubRow && !isHeaderRow && context.onRowExpand) { - initiateExpandedRows(); - } - }, [isExpanded, initiateExpandedRows]); + React.useEffect(() => { + setExpanded(isExpanded); + if (!isSubRow && !isHeaderRow && context.onRowExpand) { + initiateExpandedRows(); + } + }, [isExpanded, initiateExpandedRows]); - React.useEffect(() => { - setColumnProps(isHeaderRow ? { disableSort: true } : null); - }, [isHeaderRow]); + React.useEffect(() => { + setColumnProps(isHeaderRow ? { disableSort: true } : null); + }, [isHeaderRow]); - React.useEffect(() => { - setIsParentRow(!(isHeaderRow || isSubRow)); - }, [isHeaderRow, isSubRow]); + React.useEffect(() => { + setIsParentRow(!(isHeaderRow || isSubRow)); + }, [isHeaderRow, isSubRow]); - React.useEffect(() => { - if (context.onRowExpand) { - setIsShown(isSubRow && expandedRows.indexOf(parentKey) > -1); - } - }, [expandedRows]); + React.useEffect(() => { + if (context.onRowExpand) { + setIsShown(isSubRow && expandedRows.indexOf(parentKey) > -1); + } + }, [expandedRows]); - const Cell: React.FC = isHeaderRow ? TableHeaderCell : TableCell; + const Cell: React.FC = isHeaderRow ? TableHeaderCell : TableCell; - return ( - - {!!context.onRowExpand && ( - - {isParentRow && ( - - )} - - )} - {!!context.onRowSelect && ( - - {!(hideSelect || isSubRow) && ( - { - if (input) { - input.indeterminate = indeterminate && !checked; - } - }} - name={`tb_checkbox_${uniqueId}`} - id={`tb_checkbox_${uniqueId}`} - onChange={(event: React.ChangeEvent) => context.onRowSelect(event, uniqueId)} - /> - )} - - )} - {props.children} - - ); -}; + return ( + + {!!context.onRowExpand && ( + + {isParentRow && ( + + )} + + )} + {!!context.onRowSelect && ( + + {!(hideSelect || isSubRow) && ( + { + if (input) { + input.indeterminate = indeterminate && !checked; + } + }} + name={`tb_checkbox_${uniqueId}`} + id={`tb_checkbox_${uniqueId}`} + onChange={(event: React.ChangeEvent) => context.onRowSelect(event, uniqueId)} + /> + )} + + )} + {props.children} + + ); + } +); TableRow.displayName = "TableRow"; From 744b0577f0e9b4bc30c01ac4562197ea2068992e Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:44:49 +0800 Subject: [PATCH 23/42] refactor(tabs): add foraward ref --- lib/src/Tabs/Tabs.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/Tabs/Tabs.tsx b/lib/src/Tabs/Tabs.tsx index 3cb8acf78..c8133957c 100644 --- a/lib/src/Tabs/Tabs.tsx +++ b/lib/src/Tabs/Tabs.tsx @@ -9,14 +9,14 @@ export type TabsProps = JSX.IntrinsicElements["ul"] & { onTabChange?: (index: number) => void; }; /** Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy. */ -export const Tabs: React.FC = ({ value, onTabChange, ...props }: TabsProps) => { +export const Tabs: React.FC = React.forwardRef(({ value, onTabChange, ...props }: TabsProps, ref: React.ForwardedRef) => { const onClick = (event: React.MouseEvent) => { event.currentTarget.href.endsWith("#") && event.preventDefault(); onTabChange && onTabChange(parseInt(event.currentTarget.dataset.indexNumber)); }; return ( -
      +
        {React.Children.map(props.children, (Child: React.ReactElement, index: number) => { return React.isValidElement>(Child) ? React.cloneElement(Child, { @@ -28,4 +28,4 @@ export const Tabs: React.FC = ({ value, onTabChange, ...props }: Tabs })}
      ); -}; +}); From 159eb8060cba5b9f66f2e10eada1c552ce807353 Mon Sep 17 00:00:00 2001 From: kp Date: Fri, 15 Jan 2021 10:47:01 +0800 Subject: [PATCH 24/42] refactor(textarea): add forward ref --- lib/src/Textarea/Textarea.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/Textarea/Textarea.tsx b/lib/src/Textarea/Textarea.tsx index 546bf69e8..8d60b8b37 100644 --- a/lib/src/Textarea/Textarea.tsx +++ b/lib/src/Textarea/Textarea.tsx @@ -15,7 +15,7 @@ export type TextareaProps = JSX.IntrinsicElements["textarea"] & { wrapperProps?: JSX.IntrinsicElements["div"]; }; /** Textarea is a component that allows user to add or edit text in multiline */ -export const Textarea: React.FC = ({ indicator, label, resizable, wrapperProps = {}, ...props }: TextareaProps) => { +export const Textarea: React.FC = React.forwardRef(({ indicator, label, resizable, wrapperProps = {}, ...props }: TextareaProps, ref: React.ForwardedRef) => { const [id, setId] = React.useState(); React.useEffect(() => setId(props.id ? props.id : label ? randomId("textarea-") : null), [props.id, label]); @@ -24,8 +24,8 @@ export const Textarea: React.FC = ({ indicator, label, resizable,
      {label && } -
    ; -}; +const TableCell: React.FC = React.forwardRef(({ ...props }: TableCellProps, ref: React.ForwardedRef) => { + return ; +}); TableCell.displayName = "TableCell"; diff --git a/lib/src/Table/parts/TableHeader.tsx b/lib/src/Table/parts/TableHeader.tsx index efe877212..da8781991 100644 --- a/lib/src/Table/parts/TableHeader.tsx +++ b/lib/src/Table/parts/TableHeader.tsx @@ -3,9 +3,9 @@ import { TableRowProps } from "./TableRow"; export type TableHeaderProps = JSX.IntrinsicElements["thead"]; -const TableHeader: React.FC = ({ ...props }: TableHeaderProps) => { +const TableHeader: React.FC = React.forwardRef(({ ...props }: TableHeaderProps, ref: React.ForwardedRef) => { return ( -
    - {React.Children.map(props.children, (Child: React.ReactElement, i: number) => { - return sortable ? ( -
    onSort()}> -
    {Child}
    -
    {defaultSort}
    -
    - ) : ( - Child - ); - })} -
    + {React.Children.map(props.children, (Child: React.ReactElement, i: number) => { + return sortable ? ( +
    onSort()}> +
    {Child}
    +
    {defaultSort}
    +
    + ) : ( + Child + ); + })} +