diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-09-21 15:08:42 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-09-24 15:02:20 +0200 |
| commit | ce533488629f9815fa6696e97a2b6598546c56a6 (patch) | |
| tree | 3df76b32f1c8c8d8420caea3889602e1a7664f0a /gui/src | |
| parent | ab645a398e8563aaa2b1c49ea4c4336afe895ef5 (diff) | |
| download | mullvadvpn-ce533488629f9815fa6696e97a2b6598546c56a6.tar.xz mullvadvpn-ce533488629f9815fa6696e97a2b6598546c56a6.zip | |
Make BlockingButton use a context instead of cloning children
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/components/AppButton.tsx | 146 | ||||
| -rw-r--r-- | gui/src/renderer/lib/utilityHooks.ts | 15 |
2 files changed, 82 insertions, 79 deletions
diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx index 5b2c7fa62e..f35f7b2049 100644 --- a/gui/src/renderer/components/AppButton.tsx +++ b/gui/src/renderer/components/AppButton.tsx @@ -1,7 +1,8 @@ import log from 'electron-log'; -import React, { useContext } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../config.json'; +import { useMounted } from '../lib/utilityHooks'; import { StyledButton, StyledButtonContent, @@ -10,9 +11,13 @@ import { } from './AppButtonStyles'; import ImageView from './ImageView'; -const ButtonContext = React.createContext({ +interface IButtonContext { + textAdjustment: number; + textRef?: React.Ref<HTMLDivElement>; +} + +const ButtonContext = React.createContext<IButtonContext>({ textAdjustment: 0, - textRef: React.createRef<HTMLDivElement>(), }); interface ILabelProps { @@ -46,51 +51,19 @@ export interface IProps extends React.HTMLAttributes<HTMLButtonElement> { textOffset?: number; } -interface IState { - textAdjustment: number; -} - -class BaseButton extends React.Component<IProps, IState> { - public state: IState = { - textAdjustment: 0, - }; +const BaseButton = React.memo(function BaseButtonT(props: IProps) { + const { children, disabled, onClick, ...otherProps } = props; - private buttonRef = React.createRef<HTMLButtonElement>(); - private textRef = React.createRef<HTMLDivElement>(); + const blockingContext = useContext(BlockingContext); + const [textAdjustment, setTextAdjustment] = useState(0); + const buttonRef = useRef() as React.RefObject<HTMLButtonElement>; + const textRef = useRef() as React.RefObject<HTMLDivElement>; - public componentDidMount() { - this.updateTextAdjustment(); - } + const contextValue = useMemo(() => ({ textAdjustment, textRef }), [textAdjustment, textRef]); - public componentDidUpdate() { - this.updateTextAdjustment(); - } - - public render() { - const { children, ...otherProps } = this.props; - - return ( - <ButtonContext.Provider - value={{ - textAdjustment: this.state.textAdjustment, - textRef: this.textRef, - }}> - <StyledButton ref={this.buttonRef} {...otherProps}> - <StyledButtonContent> - {React.Children.map(children, (child) => - typeof child === 'string' ? <Label>{child as string}</Label> : child, - )} - </StyledButtonContent> - </StyledButton> - </ButtonContext.Provider> - ); - } - - private updateTextAdjustment() { - const textOffset = this.props.textOffset ?? 0; - - const buttonRect = this.buttonRef.current?.getBoundingClientRect(); - const textRect = this.textRef.current?.getBoundingClientRect(); + useEffect(() => { + const buttonRect = buttonRef.current?.getBoundingClientRect(); + const textRect = textRef.current?.getBoundingClientRect(); if (buttonRect && textRect) { const leftDiff = textRect.left - buttonRect.left; @@ -99,55 +72,70 @@ class BaseButton extends React.Component<IProps, IState> { const trailingSpace = buttonRect.width - (leftDiff + textRect.width); // calculate text adjustment + const textOffset = props.textOffset ?? 0; const textAdjustment = leftDiff - trailingSpace - textOffset; // re-render the view with the new text adjustment if it changed - if (this.state.textAdjustment !== textAdjustment) { - this.setState({ textAdjustment }); - } + setTextAdjustment(textAdjustment); } - } -} + }); -interface IBlockingState { - isBlocked: boolean; + return ( + <ButtonContext.Provider value={contextValue}> + <StyledButton + ref={buttonRef} + disabled={blockingContext.disabled || disabled} + onClick={blockingContext.onClick ?? onClick} + {...otherProps}> + <StyledButtonContent> + {React.Children.map(children, (child) => + typeof child === 'string' ? <Label>{child as string}</Label> : child, + )} + </StyledButtonContent> + </StyledButton> + </ButtonContext.Provider> + ); +}); + +interface IBlockingContext { + disabled?: boolean; + onClick?: () => Promise<void>; } +const BlockingContext = React.createContext<IBlockingContext>({}); + interface IBlockingProps { children?: React.ReactNode; onClick: () => Promise<void>; disabled?: boolean; } -export class BlockingButton extends React.Component<IBlockingProps, IBlockingState> { - public state = { - isBlocked: false, - }; +export function BlockingButton(props: IBlockingProps) { + const isMounted = useMounted(); + const [isBlocked, setIsBlocked] = useState(false); - public render() { - return React.Children.map(this.props.children, (child) => { - if (React.isValidElement(child)) { - return React.cloneElement(child as React.ReactElement, { - ...child.props, - disabled: this.state.isBlocked || this.props.disabled, - onClick: this.onClick, - }); - } else { - return child; - } - }); - } + const onClick = useCallback(async () => { + setIsBlocked(true); + try { + await props.onClick(); + } catch (error) { + log.error(`onClick() failed - ${error}`); + } + + if (isMounted()) { + setIsBlocked(false); + } + }, [props.onClick]); + + const contextValue = useMemo( + () => ({ + disabled: isBlocked || props.disabled, + onClick, + }), + [isBlocked, props.disabled, onClick], + ); - private onClick = () => { - this.setState({ isBlocked: true }, async () => { - try { - await this.props.onClick(); - } catch (error) { - log.error(`onClick() failed - ${error}`); - } - this.setState({ isBlocked: false }); - }); - }; + return <BlockingContext.Provider value={contextValue}>{props.children}</BlockingContext.Provider>; } export const RedButton = styled(BaseButton)({ diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts new file mode 100644 index 0000000000..cbb5575f9b --- /dev/null +++ b/gui/src/renderer/lib/utilityHooks.ts @@ -0,0 +1,15 @@ +import { useCallback, useEffect, useRef } from 'react'; + +export function useMounted() { + const mountedRef = useRef(false); + const isMounted = useCallback(() => mountedRef.current, []); + + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + + return isMounted; +} |
