summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-01-24 16:56:23 +0100
committerOskar Nyberg <oskar@mullvad.net>2022-01-24 16:56:23 +0100
commitb7e6eff5f3354f858c3da4ada40240d08fb40ef4 (patch)
tree9fc62af3116d069861604abade598f4af3c932fd
parentf418bb9692aa639ac77e6462a7cebffea71436c1 (diff)
parent28017307e83ebfda4768c8a6476be7f0c31933e8 (diff)
downloadmullvadvpn-b7e6eff5f3354f858c3da4ada40240d08fb40ef4.tar.xz
mullvadvpn-b7e6eff5f3354f858c3da4ada40240d08fb40ef4.zip
Merge branch 'position-button-content-with-css'
-rw-r--r--gui/src/renderer/components/AppButton.tsx97
-rw-r--r--gui/src/renderer/components/AppButtonStyles.tsx40
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx15
-rw-r--r--gui/src/renderer/components/TunnelControl.tsx4
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>
);
};