summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-03-05 13:16:58 +0100
committerMarkus Pettersson <markus.pettersson@mullvad.net>2025-03-05 13:16:58 +0100
commitdd607dd8f3b07a70d327fc48a8f14cb0e707e8b4 (patch)
treea5309739ddfa8efb8462411d0b12d896652cc2f6
parentec359a9b014e2e9c82bc3cff32378647bd92f8d6 (diff)
parentbe02c62e44defdc5b32596b0ef2fa86f0737fdb0 (diff)
downloadmullvadvpn-dd607dd8f3b07a70d327fc48a8f14cb0e707e8b4.tar.xz
mullvadvpn-dd607dd8f3b07a70d327fc48a8f14cb0e707e8b4.zip
Merge branch 'make-button-component-composable-des-1754'
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx4
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx14
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionActionButton.tsx6
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx10
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx192
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonBase.tsx12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx24
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx11
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx13
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/button/index.ts2
14 files changed, 202 insertions, 105 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx
index 4c4b991829..0fe1150139 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/InfoButton.tsx
@@ -27,7 +27,7 @@ export default function InfoButton({ title, message, children, ...props }: InfoB
type={ModalAlertType.info}
buttons={[
<Button key="back" onClick={hide}>
- {messages.gettext('Got it!')}
+ <Button.Text>{messages.gettext('Got it!')}</Button.Text>
</Button>,
]}
close={hide}>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
index bbd4a32584..34a04d1dc0 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Login.tsx
@@ -374,11 +374,8 @@ class Login extends React.Component<IProps, IState> {
<LabelTiny color={Colors.white60}>
{messages.pgettext('login-view', 'Don’t have an account number?')}
</LabelTiny>
- <Button
- size="full"
- onClick={this.props.createNewAccount}
- disabled={!this.allowCreateAccount()}>
- {messages.pgettext('login-view', 'Create account')}
+ <Button onClick={this.props.createNewAccount} disabled={!this.allowCreateAccount()}>
+ <Button.Text>{messages.pgettext('login-view', 'Create account')}</Button.Text>
</Button>
</Flex>
);
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx
index 5a50224d80..53dded0411 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/Settings.tsx
@@ -238,9 +238,11 @@ function QuitButton() {
return (
<Button variant="destructive" onClick={quit}>
- {tunnelState.state === 'disconnected'
- ? messages.gettext('Quit')
- : messages.gettext('Disconnect & quit')}
+ <Button.Text>
+ {tunnelState.state === 'disconnected'
+ ? messages.gettext('Quit')
+ : messages.gettext('Disconnect & quit')}
+ </Button.Text>
</Button>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
index c80d7a1276..fe543317ca 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/SplitTunnelingSettings.tsx
@@ -537,7 +537,9 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro
{canEditSplitTunneling && (
<Container size="3">
<Button onClick={addWithFilePicker}>
- {messages.pgettext('split-tunneling-view', 'Find another app')}
+ <Button.Text>
+ {messages.pgettext('split-tunneling-view', 'Find another app')}
+ </Button.Text>
</Button>
</Container>
)}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
index ce1f4f3fd1..f843f4a08a 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/TooManyDevices.tsx
@@ -136,12 +136,16 @@ export default function TooManyDevices() {
variant="success"
onClick={continueLogin}
disabled={continueButtonDisabled}>
- {
- // TRANSLATORS: Button for continuing login process.
- messages.pgettext('device-management', 'Continue with login')
- }
+ <Button.Text>
+ {
+ // TRANSLATORS: Button for continuing login process.
+ messages.pgettext('device-management', 'Continue with login')
+ }
+ </Button.Text>
+ </Button>
+ <Button onClick={cancel}>
+ <Button.Text>{messages.gettext('Back')}</Button.Text>
</Button>
- <Button onClick={cancel}>{messages.gettext('Back')}</Button>
</AppButton.ButtonGroup>
</Footer>
)}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionActionButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionActionButton.tsx
index c403ddb849..c7b6c61d29 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionActionButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/ConnectionActionButton.tsx
@@ -30,7 +30,7 @@ function ConnectButton(props: Partial<Parameters<typeof Button>[0]>) {
return (
<Button variant="success" onClick={onConnect} {...props}>
- {messages.pgettext('tunnel-control', 'Connect')}
+ <Button.Text>{messages.pgettext('tunnel-control', 'Connect')}</Button.Text>
</Button>
);
}
@@ -52,7 +52,9 @@ function DisconnectButton() {
return (
<Button variant="destructive" onClick={onDisconnect}>
- {displayAsCancel ? messages.gettext('Cancel') : messages.gettext('Disconnect')}
+ <Button.Text>
+ {displayAsCancel ? messages.gettext('Cancel') : messages.gettext('Disconnect')}
+ </Button.Text>
</Button>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx
index c484c0312d..df1c9246e3 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/main-view/SelectLocationButton.tsx
@@ -42,17 +42,17 @@ function SelectLocationButton(props: ButtonProps) {
return (
<Button
- variant="primary"
- size="full"
onClick={onSelectLocation}
aria-label={sprintf(
messages.pgettext('accessibility', 'Select location. Current location is %(location)s'),
{ location: selectedRelayName },
)}
{...props}>
- {tunnelState === 'disconnected'
- ? selectedRelayName
- : messages.pgettext('tunnel-control', 'Switch location')}
+ <Button.Text>
+ {tunnelState === 'disconnected'
+ ? selectedRelayName
+ : messages.pgettext('tunnel-control', 'Switch location')}
+ </Button.Text>
</Button>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx
index 5c8f9d05b3..f8fe5f7879 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/Button.tsx
@@ -1,105 +1,131 @@
import React, { forwardRef } from 'react';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { Colors, Radius, Spacings } from '../../foundations';
-import { buttonReset } from '../../styles';
import { Flex } from '../flex';
-import { BodySmallSemiBold } from '../typography';
+import { ButtonBase } from './ButtonBase';
+import { ButtonProvider } from './ButtonContext';
+import { ButtonIcon, ButtonText, StyledIcon, StyledText } from './components';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'success' | 'destructive';
size?: 'auto' | 'full' | '1/2';
- leading?: React.ReactNode;
- trailing?: React.ReactNode;
}
-const variants = {
- primary: {
- background: Colors.blue,
- hover: Colors.blue60,
- disabled: Colors.blue50,
+const styles = {
+ radius: Radius.radius4,
+ variants: {
+ primary: {
+ background: Colors.blue,
+ hover: Colors.blue60,
+ disabled: Colors.blue50,
+ },
+ success: {
+ background: Colors.green,
+ hover: Colors.green90,
+ disabled: Colors.green40,
+ },
+ destructive: {
+ background: Colors.red,
+ hover: Colors.red80,
+ disabled: Colors.red60,
+ },
},
- success: {
- background: Colors.green,
- hover: Colors.green90,
- disabled: Colors.green40,
+ sizes: {
+ auto: 'auto',
+ full: '100%',
+ '1/2': '50%',
},
- destructive: {
- background: Colors.red,
- hover: Colors.red80,
- disabled: Colors.red60,
- },
-} as const;
-
-const sizes = {
- auto: 'auto',
- full: '100%',
- '1/2': '50%',
};
-const StyledButton = styled.button({
- ...buttonReset,
+const StyledButton = styled(ButtonBase)<ButtonProps>`
+ ${({ size: sizeProp = 'full', variant: variantProp = 'primary' }) => {
+ const variant = styles.variants[variantProp];
+ const size = styles.sizes[sizeProp];
- minHeight: '32px',
- borderRadius: Radius.radius4,
- minWidth: '60px',
- width: 'var(--size)',
- background: 'var(--background)',
- '&:not(:disabled):hover': {
- background: 'var(--hover)',
- },
- '&:disabled': {
- background: 'var(--disabled)',
- },
- '&:focus-visible': {
- outline: `2px solid ${Colors.white}`,
- outlineOffset: '2px',
- },
-});
+ return css`
+ --background: ${variant.background};
+ --hover: ${variant.hover};
+ --disabled: ${variant.disabled};
+ --radius: ${styles.radius};
+ --size: ${size};
+
+ min-height: 32px;
+ min-width: 60px;
+ border-radius: var(--radius);
+ width: var(--size);
+ background: var(--background);
-export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
- (
- { variant = 'primary', size = 'full', leading, trailing, children, disabled, style, ...props },
- ref,
- ) => {
- const styles = variants[variant];
+ &:not(:disabled):hover {
+ background: var(--hover);
+ }
+
+ &:disabled {
+ background: var(--disabled);
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${Colors.white};
+ outline-offset: 2px;
+ }
+ `;
+ }}
+`;
+
+const StyledFlex = styled(Flex)`
+ justify-content: space-between;
+ &&:has(${StyledText}:only-child) {
+ justify-content: center;
+ }
+ &&:has(${StyledText} + ${StyledIcon}) {
+ &::before {
+ content: ' ';
+ display: inline-block;
+ width: 24px;
+ }
+ }
+ &&:has(${StyledIcon} + ${StyledText}) {
+ &::after {
+ content: ' ';
+ display: inline-block;
+ width: 24px;
+ }
+ }
+ &&:has(${StyledIcon} + ${StyledText} + ${StyledIcon}) {
+ &::before {
+ display: none;
+ }
+ &::after {
+ display: none;
+ }
+ }
+`;
+
+const Button = forwardRef<HTMLButtonElement, ButtonProps>(
+ ({ children, disabled = false, style, ...props }, ref) => {
return (
- <StyledButton
- ref={ref}
- style={
- {
- '--background': styles.background,
- '--hover': styles.hover,
- '--disabled': styles.disabled,
- '--size': sizes[size],
- ...style,
- } as React.CSSProperties
- }
- disabled={disabled}
- {...props}>
- <Flex
- $flex={1}
- $gap={Spacings.spacing3}
- $justifyContent="space-between"
- $padding={{
- horizontal: Spacings.spacing3,
- }}
- $alignItems="center">
- {leading}
- <Flex $flex={1} $justifyContent="center" $alignItems="center">
- {typeof children === 'string' ? (
- <BodySmallSemiBold color={disabled ? Colors.white40 : Colors.white}>
- {children}
- </BodySmallSemiBold>
- ) : (
- children
- )}
- </Flex>
- {trailing}
- </Flex>
- </StyledButton>
+ <ButtonProvider disabled={disabled}>
+ <StyledButton ref={ref} disabled={disabled} {...props}>
+ <StyledFlex
+ $flex={1}
+ $gap={Spacings.spacing3}
+ $alignItems="center"
+ $padding={{
+ horizontal: Spacings.spacing3,
+ }}>
+ {children}
+ </StyledFlex>
+ </StyledButton>
+ </ButtonProvider>
);
},
);
Button.displayName = 'Button';
+
+const ButtonNamespace = Object.assign(Button, {
+ Text: ButtonText,
+ Icon: ButtonIcon,
+});
+
+export { ButtonNamespace as Button };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonBase.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonBase.tsx
new file mode 100644
index 0000000000..239a73196d
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonBase.tsx
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+
+export const ButtonBase = styled.button({
+ border: 'none',
+ padding: 0,
+ margin: 0,
+ font: 'inherit',
+ color: 'inherit',
+ textAlign: 'inherit',
+ lineHeight: 'inherit',
+ cursor: 'default',
+});
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx
new file mode 100644
index 0000000000..4ff4876723
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/ButtonContext.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+interface ButtonContextProps {
+ disabled: boolean;
+}
+
+const ButtonContext = React.createContext<ButtonContextProps | undefined>(undefined);
+
+export const useButtonContext = (): ButtonContextProps => {
+ const context = React.useContext(ButtonContext);
+ if (!context) {
+ throw new Error('useButtonContext must be used within a ButtonProvider');
+ }
+ return context;
+};
+
+interface ButtonProviderProps {
+ disabled: boolean;
+ children: React.ReactNode;
+}
+
+export const ButtonProvider = ({ disabled, children }: ButtonProviderProps) => {
+ return <ButtonContext.Provider value={{ disabled }}>{children}</ButtonContext.Provider>;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx
new file mode 100644
index 0000000000..fcf0959a82
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonIcon.tsx
@@ -0,0 +1,11 @@
+import styled from 'styled-components';
+
+import { Icon, IconProps } from '../../icon';
+
+type ButtonIconProps = Omit<IconProps, 'size'>;
+
+export const StyledIcon = styled(Icon)({});
+
+export const ButtonIcon = ({ ...props }: ButtonIconProps) => {
+ return <StyledIcon size="medium" {...props} />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx
new file mode 100644
index 0000000000..de117fccb2
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/ButtonText.tsx
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+import { Colors } from '../../../foundations';
+import { BodySmallSemiBold, BodySmallSemiBoldProps } from '../../typography';
+import { useButtonContext } from '../ButtonContext';
+
+export type ButtonTextProps = Omit<BodySmallSemiBoldProps, 'color'>;
+export const StyledText = styled(BodySmallSemiBold)``;
+
+export const ButtonText = (props: ButtonTextProps) => {
+ const { disabled } = useButtonContext();
+ return <StyledText color={disabled ? Colors.white40 : Colors.white} {...props} />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/index.ts
new file mode 100644
index 0000000000..904258378e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/components/index.ts
@@ -0,0 +1,2 @@
+export * from './ButtonIcon';
+export * from './ButtonText';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/index.ts
index 8b166a86e4..8a5e64742d 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/button/index.ts
@@ -1 +1,3 @@
export * from './Button';
+export * from './ButtonContext';
+export * from './ButtonBase';