diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-03-05 13:16:58 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2025-03-05 13:16:58 +0100 |
| commit | dd607dd8f3b07a70d327fc48a8f14cb0e707e8b4 (patch) | |
| tree | a5309739ddfa8efb8462411d0b12d896652cc2f6 | |
| parent | ec359a9b014e2e9c82bc3cff32378647bd92f8d6 (diff) | |
| parent | be02c62e44defdc5b32596b0ef2fa86f0737fdb0 (diff) | |
| download | mullvadvpn-dd607dd8f3b07a70d327fc48a8f14cb0e707e8b4.tar.xz mullvadvpn-dd607dd8f3b07a70d327fc48a8f14cb0e707e8b4.zip | |
Merge branch 'make-button-component-composable-des-1754'
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'; |
