diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-01-24 16:56:23 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-01-24 16:56:23 +0100 |
| commit | b7e6eff5f3354f858c3da4ada40240d08fb40ef4 (patch) | |
| tree | 9fc62af3116d069861604abade598f4af3c932fd | |
| parent | f418bb9692aa639ac77e6462a7cebffea71436c1 (diff) | |
| parent | 28017307e83ebfda4768c8a6476be7f0c31933e8 (diff) | |
| download | mullvadvpn-b7e6eff5f3354f858c3da4ada40240d08fb40ef4.tar.xz mullvadvpn-b7e6eff5f3354f858c3da4ada40240d08fb40ef4.zip | |
Merge branch 'position-button-content-with-css'
| -rw-r--r-- | gui/src/renderer/components/AppButton.tsx | 97 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppButtonStyles.tsx | 40 | ||||
| -rw-r--r-- | gui/src/renderer/components/TransitionContainer.tsx | 15 | ||||
| -rw-r--r-- | gui/src/renderer/components/TunnelControl.tsx | 4 |
4 files changed, 85 insertions, 71 deletions
diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx index a761179924..b4329bed61 100644 --- a/gui/src/renderer/components/AppButton.tsx +++ b/gui/src/renderer/components/AppButton.tsx @@ -1,36 +1,26 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../config.json'; import log from '../../shared/logging'; import { useMounted } from '../lib/utilityHooks'; import { StyledButtonContent, + StyledHiddenSide, StyledLabel, - StyledLabelContainer, + StyledLeft, + StyledRight, + StyledVisibleSide, transparentButton, } from './AppButtonStyles'; import ImageView from './ImageView'; -interface IButtonContext { - textAdjustment: number; - textRef?: React.Ref<HTMLDivElement>; -} - -const ButtonContext = React.createContext<IButtonContext>({ - textAdjustment: 0, -}); - interface ILabelProps { - children?: React.ReactText; + textOffset?: number; + children?: React.ReactNode; } export function Label(props: ILabelProps) { - const { textAdjustment, textRef } = useContext(ButtonContext); - return ( - <StyledLabelContainer ref={textRef} textAdjustment={textAdjustment}> - <StyledLabel>{props.children}</StyledLabel> - </StyledLabelContainer> - ); + return <StyledLabel textOffset={props.textOffset ?? 0}>{props.children}</StyledLabel>; } interface IIconProps { @@ -51,55 +41,52 @@ export interface IProps extends React.HTMLAttributes<HTMLButtonElement> { textOffset?: number; } +type ChildrenGroups = { left: React.ReactNode[]; label: React.ReactNode; right: React.ReactNode[] }; + const BaseButton = React.memo(function BaseButtonT(props: IProps) { const { children, textOffset, ...otherProps } = props; - const [textAdjustment, setTextAdjustment] = useState(0); - const buttonRef = useRef() as React.RefObject<HTMLButtonElement>; - const textRef = useRef() as React.RefObject<HTMLDivElement>; - - const contextValue = useMemo(() => ({ textAdjustment, textRef }), [textAdjustment, textRef]); - - useEffect(() => { - const buttonRect = buttonRef.current?.getBoundingClientRect(); - const textRect = textRef.current?.getBoundingClientRect(); - - if (buttonRect && textRect) { - const leftDiff = textRect.left - buttonRect.left; + const groupedChildren = useMemo(() => { + return React.Children.toArray(children).reduce( + (groups: ChildrenGroups, child) => { + if (groups.label === undefined && typeof child === 'string') { + return { ...groups, label: <Label textOffset={textOffset}>{child}</Label> }; + } else if (React.isValidElement(child) && child.type === Label) { + return { ...groups, label: React.cloneElement(child, { textOffset }) }; + } else if (groups.label === undefined) { + return { ...groups, left: [...groups.left, child] }; + } else { + return { ...groups, right: [...groups.right, child] }; + } + }, + { left: [], label: undefined, right: [] }, + ); + }, [children, textOffset]); - // calculate the remaining space at the right hand side - const trailingSpace = buttonRect.width - (leftDiff + textRect.width); - - // calculate text adjustment - const textAdjustment = leftDiff - trailingSpace - (textOffset ?? 0); + return ( + <StyledSimpleButton {...otherProps}> + <StyledButtonContent> + <StyledLeft> + <StyledVisibleSide>{groupedChildren.left}</StyledVisibleSide> + <StyledHiddenSide>{groupedChildren.right}</StyledHiddenSide> + </StyledLeft> - // re-render the view with the new text adjustment if it changed - setTextAdjustment(textAdjustment); - } - }); + {groupedChildren.label ?? <Label />} - return ( - <ButtonContext.Provider value={contextValue}> - <StyledSimpleButton ref={buttonRef} {...otherProps}> - <StyledButtonContent> - {React.Children.map(children, (child) => - typeof child === 'string' ? <Label>{child as string}</Label> : child, - )} - </StyledButtonContent> - </StyledSimpleButton> - </ButtonContext.Provider> + <StyledRight> + <StyledVisibleSide>{groupedChildren.right}</StyledVisibleSide> + <StyledHiddenSide>{groupedChildren.left}</StyledHiddenSide> + </StyledRight> + </StyledButtonContent> + </StyledSimpleButton> ); }); -function SimpleButtonT( - props: React.ButtonHTMLAttributes<HTMLButtonElement>, - ref: React.Ref<HTMLButtonElement>, -) { +function SimpleButtonT(props: React.ButtonHTMLAttributes<HTMLButtonElement>) { const blockingContext = useContext(BlockingContext); return ( <button - ref={ref} {...props} disabled={props.disabled || blockingContext.disabled} onClick={blockingContext.onClick ?? props.onClick}> @@ -108,7 +95,7 @@ function SimpleButtonT( ); } -export const SimpleButton = React.memo(React.forwardRef(SimpleButtonT)); +export const SimpleButton = React.memo(SimpleButtonT); const StyledSimpleButton = styled(SimpleButton)({ display: 'flex', diff --git a/gui/src/renderer/components/AppButtonStyles.tsx b/gui/src/renderer/components/AppButtonStyles.tsx index 5446e89b31..6322a33783 100644 --- a/gui/src/renderer/components/AppButtonStyles.tsx +++ b/gui/src/renderer/components/AppButtonStyles.tsx @@ -1,27 +1,41 @@ import styled from 'styled-components'; import { buttonText } from './common-styles'; -export const StyledLabelContainer = styled.div((props: { textAdjustment: number }) => ({ - display: 'flex', - flex: 1, - paddingRight: `${props.textAdjustment > 0 ? props.textAdjustment : 0}px`, - paddingLeft: `${props.textAdjustment < 0 ? Math.abs(props.textAdjustment) : 0}px`, -})); - -export const StyledLabel = styled.span(buttonText, { - flex: 1, +export const StyledLabel = styled.span(buttonText, (props: { textOffset: number }) => ({ + paddingLeft: props.textOffset > 0 ? `${props.textOffset}px` : 0, + paddingRight: props.textOffset < 0 ? `${-props.textOffset}px` : 0, textAlign: 'center', -}); + wordBreak: 'break-word', +})); export const StyledButtonContent = styled.div({ - display: 'flex', flex: 1, - flexDirection: 'row', + display: 'grid', + gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', - justifyContent: 'center', padding: '9px', }); export const transparentButton = { backdropFilter: 'blur(4px)', }; + +export const StyledLeft = styled.div({ + justifySelf: 'start', + display: 'flex', + flexDirection: 'column', +}); + +export const StyledRight = styled(StyledLeft)({ + justifySelf: 'end', +}); + +export const StyledVisibleSide = styled.div({ + display: 'flex', + flexDirection: 'row', +}); + +export const StyledHiddenSide = styled(StyledVisibleSide).attrs({ 'aria-hidden': true })({ + height: 0, + visibility: 'hidden', +}); diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx index 92aec7828e..4dc354d924 100644 --- a/gui/src/renderer/components/TransitionContainer.tsx +++ b/gui/src/renderer/components/TransitionContainer.tsx @@ -92,11 +92,14 @@ export default class TransitionContainer extends React.Component<IProps, IState> const candidate = props.children; if (candidate && state.currentItem) { - // synchronize updates to the last added child. + // Synchronize updates to the last added child. Although the queue doesn't change, the child + // itself might need to change. That's why the queue-/next item is replaced by it again after + // calling `makeItem`. const itemQueueCount = state.itemQueue.length; const lastItemInQueue = itemQueueCount > 0 ? state.itemQueue[itemQueueCount - 1] : undefined; if (lastItemInQueue && lastItemInQueue.view.props.viewId === candidate.props.viewId) { + // Child is last item in queue. No change to the queue needed. return { itemQueue: [...state.itemQueue.slice(0, -1), TransitionContainer.makeItem(props)], }; @@ -105,18 +108,21 @@ export default class TransitionContainer extends React.Component<IProps, IState> state.nextItem && state.nextItem.view.props.viewId === candidate.props.viewId ) { + // Child is next item, no change to the queue needed. return { nextItem: TransitionContainer.makeItem(props) }; } else if ( itemQueueCount === 0 && !state.nextItem && state.currentItem.view.props.viewId === candidate.props.viewId ) { + // Child is current item and there's no new child, no change to the queue needed. return { currentItem: TransitionContainer.makeItem(props) }; } else { - // add new item + // Child is a new item and is added to the queue. return { itemQueue: [...state.itemQueue, TransitionContainer.makeItem(props)] }; } } else if (candidate && !state.currentItem) { + // Child is set as current item if there's no item already. return { currentItem: TransitionContainer.makeItem(props) }; } else { return null; @@ -130,6 +136,11 @@ export default class TransitionContainer extends React.Component<IProps, IState> this.state.nextItemStyle && this.state.nextItemTransition ) { + // Force browser reflow before starting transition. Without this animations won't run since + // the next view content hasn't been painted yet. It will just appear without a transition. + void this.nextContentRef.current?.offsetHeight; + + // Start transition this.setState((state) => ({ currentItemStyle: Object.assign({}, state.currentItemStyle, state.currentItemTransition), nextItemStyle: Object.assign({}, state.nextItemStyle, state.nextItemTransition), diff --git a/gui/src/renderer/components/TunnelControl.tsx b/gui/src/renderer/components/TunnelControl.tsx index c0fa5f8452..441e95bbb1 100644 --- a/gui/src/renderer/components/TunnelControl.tsx +++ b/gui/src/renderer/components/TunnelControl.tsx @@ -268,7 +268,9 @@ export default class TunnelControl extends React.Component<ITunnelControlProps> onClick={this.props.onReconnect} aria-label={messages.gettext('Reconnect')} {...props}> - <ImageView height={22} width={22} source="icon-reload" tintColor="white" /> + <AppButton.Label> + <ImageView height={22} width={22} source="icon-reload" tintColor="white" /> + </AppButton.Label> </AppButton.RedTransparentButton> ); }; |
