diff options
| author | Oliver <oliver@mohlin.dev> | 2025-07-25 10:31:34 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:43 +0200 |
| commit | cf9a95d43bb748a476609d163f169dd1a775d149 (patch) | |
| tree | ffa38121575cdbe617930342ff5cb567e344f5ed | |
| parent | b30390fbddc768512a3b850cb19f8fa15ffe5669 (diff) | |
| download | mullvadvpn-cf9a95d43bb748a476609d163f169dd1a775d149.tar.xz mullvadvpn-cf9a95d43bb748a476609d163f169dd1a775d149.zip | |
Add flash animation support to ListItem component
8 files changed, 92 insertions, 35 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationListItem.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationListItem.tsx index 1696c2db05..289a71d2a1 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/NavigationListItem.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/NavigationListItem.tsx @@ -14,11 +14,11 @@ function NavigationListItem({ to, children, ...props }: NavigationListItemProps) return ( <ListItem {...props}> - <ListItem.Item> - <ListItem.Trigger onClick={navigate}> + <ListItem.Trigger onClick={navigate}> + <ListItem.Item> <ListItem.Content>{children}</ListItem.Content> - </ListItem.Trigger> - </ListItem.Item> + </ListItem.Item> + </ListItem.Trigger> </ListItem> ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItem.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItem.tsx index 1a37760d97..1371b9848e 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItem.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItem.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import React from 'react'; import { Flex } from '../flex'; import { @@ -14,22 +14,21 @@ import { import { levels } from './levels'; import { ListItemProvider } from './ListItemContext'; -export interface ListItemProps { +export type ListItemAnimation = 'flash' | 'dim'; + +export type ListItemProps = { level?: keyof typeof levels; disabled?: boolean; + animation?: ListItemAnimation; children: React.ReactNode; -} - -const StyledFlex = styled(Flex)` - margin-bottom: 1px; -`; +} & React.HtmlHTMLAttributes<HTMLDivElement>; -const ListItem = ({ level = 0, disabled, children }: ListItemProps) => { +const ListItem = ({ level = 0, disabled, animation, children, ...props }: ListItemProps) => { return ( - <ListItemProvider level={level} disabled={disabled}> - <StyledFlex $flexDirection="column" $gap="tiny" $flex={1}> + <ListItemProvider level={level} disabled={disabled} animation={animation}> + <Flex $flexDirection="column" $flex={1} {...props}> {children} - </StyledFlex> + </Flex> </ListItemProvider> ); }; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItemContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItemContext.tsx index 348f916a41..f29b58880b 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItemContext.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItemContext.tsx @@ -1,21 +1,31 @@ import { createContext, ReactNode, useContext } from 'react'; import { levels } from './levels'; +import { ListItemAnimation } from './ListItem'; interface ListItemContextType { level: keyof typeof levels; disabled?: boolean; + animation?: ListItemAnimation; } const ListItemContext = createContext<ListItemContextType | undefined>(undefined); interface ListItemProviderProps extends ListItemContextType { + animation?: ListItemAnimation; children: ReactNode; } -export const ListItemProvider = ({ level, disabled, children }: ListItemProviderProps) => { +export const ListItemProvider = ({ + level, + disabled, + animation, + children, +}: ListItemProviderProps) => { return ( - <ListItemContext.Provider value={{ level, disabled }}>{children}</ListItemContext.Provider> + <ListItemContext.Provider value={{ level, disabled, animation }}> + {children} + </ListItemContext.Provider> ); }; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-footer/ListItemFooter.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-footer/ListItemFooter.tsx index c5b0de9355..2224633df0 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-footer/ListItemFooter.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-footer/ListItemFooter.tsx @@ -3,5 +3,5 @@ import { Flex, FlexProps } from '../../../flex'; export type ListItemFooterProps = FlexProps; export const ListItemFooter = (props: ListItemFooterProps) => { - return <Flex $padding={{ horizontal: 'medium' }} {...props} />; + return <Flex $padding={{ horizontal: 'medium' }} $margin={{ top: 'tiny' }} {...props} />; }; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/ListItemItem.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/ListItemItem.tsx index 626f7327e6..40e98bd3b7 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/ListItemItem.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/ListItemItem.tsx @@ -1,15 +1,20 @@ -import styled, { css } from 'styled-components'; +import styled, { css, RuleSet } from 'styled-components'; -import { useBackgroundColor } from './hooks'; +import { useAnimation, useBackgroundColor } from './hooks'; export interface ListItemItemProps { children: React.ReactNode; } -const StyledDiv = styled.div<{ $backgroundColor: string }>` - ${({ $backgroundColor }) => { +export const StyledListItemItem = styled.div<{ + $backgroundColor: string; + $animation?: RuleSet<object>; +}>` + ${({ $backgroundColor, $animation }) => { return css` --background-color: ${$backgroundColor}; + + margin-bottom: 1px; background-color: var(--background-color); min-height: 48px; width: 100%; @@ -19,11 +24,17 @@ const StyledDiv = styled.div<{ $backgroundColor: string }>` &&:has(> :last-child:nth-child(2)) { grid-template-columns: 1fr 56px; } + ${$animation} `; }} `; -export function ListItemItem({ children }: ListItemItemProps) { +export function ListItemItem({ children, ...props }: ListItemItemProps) { const backgroundColor = useBackgroundColor(); - return <StyledDiv $backgroundColor={backgroundColor}>{children}</StyledDiv>; + const animation = useAnimation(); + return ( + <StyledListItemItem $backgroundColor={backgroundColor} $animation={animation} {...props}> + {children} + </StyledListItemItem> + ); } diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/index.ts index 9cc5be2e57..b4f31dabf3 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/index.ts @@ -1 +1,2 @@ export * from './useBackgroundColor'; +export * from './useAnimation'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/useAnimation.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/useAnimation.tsx new file mode 100644 index 0000000000..cdc46332e7 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/useAnimation.tsx @@ -0,0 +1,33 @@ +import { css, keyframes } from 'styled-components'; + +import { colors } from '../../../../../foundations'; +import { useListItem } from '../../../ListItemContext'; + +const flash = keyframes` + 0% { background-color: var(--background-color) } + 50% { background-color: ${colors.whiteOnBlue20} } + 100% { background-color: var(--background-color) } +`; + +const dim = keyframes` + 0% { opacity: 100% } + 25% { opacity: 50% } + 50% { opacity: 50% } + 75% { opacity: 50% } + 100% { opacity: 100% } +`; + +export const useAnimation = () => { + const { animation } = useListItem(); + if (animation === 'flash') { + return css` + animation: ${flash} 0.75s ease-in-out 0s 2 normal forwards; + `; + } + if (animation === 'dim') { + return css` + animation: ${dim} 1.5s ease-in-out 0s 1 normal forwards; + `; + } + return undefined; +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-trigger/ListItemTrigger.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-trigger/ListItemTrigger.tsx index 7cd7b85859..67861fa8eb 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-trigger/ListItemTrigger.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-trigger/ListItemTrigger.tsx @@ -1,14 +1,15 @@ +import { forwardRef } from 'react'; import styled, { css } from 'styled-components'; import { colors } from '../../../../foundations'; import { ListItemProps } from '../../ListItem'; import { useListItem } from '../../ListItemContext'; +import { StyledListItemItem } from '../list-item-item'; const StyledButton = styled.button<Pick<ListItemProps, 'disabled'>>` display: flex; width: 100%; - --background: transparent; - background-color: var(--background); + background-color: transparent; &&:focus-visible { outline: 2px solid ${colors.white}; @@ -19,16 +20,16 @@ const StyledButton = styled.button<Pick<ListItemProps, 'disabled'>>` ${({ disabled }) => { if (!disabled) { return css` - --background: ${colors.blue}; - &:hover { - --background: ${colors.whiteOnBlue10}; - background-color: var(--background); + ${StyledListItemItem} { + background-color: ${colors.whiteOnBlue10}; + } } &:active { - --background: ${colors.whiteOnBlue20}; - background-color: var(--background); + ${StyledListItemItem} { + background-color: ${colors.whiteOnBlue20}; + } } `; } @@ -39,7 +40,9 @@ const StyledButton = styled.button<Pick<ListItemProps, 'disabled'>>` export type ListItemTriggerProps = React.HtmlHTMLAttributes<HTMLButtonElement>; -export function ListItemTrigger(props: ListItemTriggerProps) { +export const ListItemTrigger = forwardRef<HTMLButtonElement, ListItemTriggerProps>((props, ref) => { const { disabled } = useListItem(); - return <StyledButton disabled={disabled} {...props} />; -} + return <StyledButton ref={ref} disabled={disabled} {...props} />; +}); + +ListItemTrigger.displayName = 'ListItemTrigger'; |
