summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-07-25 10:31:34 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:43 +0200
commitcf9a95d43bb748a476609d163f169dd1a775d149 (patch)
treeffa38121575cdbe617930342ff5cb567e344f5ed
parentb30390fbddc768512a3b850cb19f8fa15ffe5669 (diff)
downloadmullvadvpn-cf9a95d43bb748a476609d163f169dd1a775d149.tar.xz
mullvadvpn-cf9a95d43bb748a476609d163f169dd1a775d149.zip
Add flash animation support to ListItem component
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/NavigationListItem.tsx8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItem.tsx21
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/ListItemContext.tsx14
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-footer/ListItemFooter.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/ListItemItem.tsx23
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-item/hooks/useAnimation.tsx33
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/list-item/components/list-item-trigger/ListItemTrigger.tsx25
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';