summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-08-27 10:29:02 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:43 +0200
commitc52e39f1c38816667fd0949cf57354fb9a358761 (patch)
tree23c8fce1ebfb48b1a64639cd03280736210b0ea7
parent444900c3581bcc538eccf28160053110700751ca (diff)
downloadmullvadvpn-c52e39f1c38816667fd0949cf57354fb9a358761.tar.xz
mullvadvpn-c52e39f1c38816667fd0949cf57354fb9a358761.zip
Update Accordion component to support animations and disabled state
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx31
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx22
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx35
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx8
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/useAnimation.tsx33
7 files changed, 107 insertions, 35 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx
index 2ed9043972..25a8fd14cd 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx
@@ -1,35 +1,50 @@
import React from 'react';
import styled from 'styled-components';
+import { FlexColumn } from '../flex-column';
import { AccordionProvider } from './AccordionContext';
import { AccordionHeader, AccordionTrigger } from './components';
import { AccordionContent } from './components/AccordionContent';
import { AccordionIcon } from './components/AccordionIcon';
import { AccordionTitle } from './components/AccordionTitle';
+export type AccordionAnimation = 'flash' | 'dim';
+
export type AccordionProps = {
expanded?: boolean;
onExpandedChange?: (open: boolean) => void;
+ disabled?: boolean;
+ animation?: AccordionAnimation;
children?: React.ReactNode;
-};
+} & React.ComponentPropsWithRef<'div'>;
-const StyledAccordion = styled.div`
- display: flex;
- flex: 1;
- flex-direction: column;
+const StyledAccordion = styled(FlexColumn)`
width: 100%;
`;
-function Accordion({ expanded = false, onExpandedChange: onOpenChange, children }: AccordionProps) {
+function Accordion({
+ expanded = false,
+ onExpandedChange: onOpenChange,
+ disabled,
+ animation,
+ children,
+ ...props
+}: AccordionProps) {
const triggerId = React.useId();
const contentId = React.useId();
+ const titleId = React.useId();
return (
<AccordionProvider
triggerId={triggerId}
contentId={contentId}
+ titleId={titleId}
expanded={expanded}
- onExpandedChange={onOpenChange}>
- <StyledAccordion>{children}</StyledAccordion>
+ onExpandedChange={onOpenChange}
+ disabled={disabled}
+ animation={animation}>
+ <StyledAccordion $flex={1} {...props}>
+ {children}
+ </StyledAccordion>
</AccordionProvider>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx
index e39295f1a8..acc5756102 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx
@@ -1,12 +1,15 @@
import React from 'react';
-import { AccordionProps } from './Accordion';
+import { AccordionAnimation, AccordionProps } from './Accordion';
interface AccordionContextProps {
triggerId: string;
contentId: string;
+ titleId: string;
expanded: AccordionProps['expanded'];
onExpandedChange?: AccordionProps['onExpandedChange'];
+ disabled?: boolean;
+ animation?: AccordionAnimation;
}
const AccordionContext = React.createContext<AccordionContextProps | undefined>(undefined);
@@ -22,21 +25,14 @@ export const useAccordionContext = (): AccordionContextProps => {
interface AccordionProviderProps {
triggerId: string;
contentId: string;
+ titleId: string;
expanded: boolean;
onExpandedChange?: (open: boolean) => void;
+ disabled?: boolean;
+ animation?: AccordionAnimation;
children: React.ReactNode;
}
-export function AccordionProvider({
- triggerId,
- contentId,
- expanded,
- onExpandedChange,
- children,
-}: AccordionProviderProps) {
- return (
- <AccordionContext.Provider value={{ triggerId, contentId, expanded, onExpandedChange }}>
- {children}
- </AccordionContext.Provider>
- );
+export function AccordionProvider({ children, ...props }: AccordionProviderProps) {
+ return <AccordionContext.Provider value={props}>{children}</AccordionContext.Provider>;
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx
index b71effcfba..ce28143976 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx
@@ -1,29 +1,42 @@
-import styled from 'styled-components';
+import styled, { css, RuleSet } from 'styled-components';
import { colors } from '../../../foundations';
import { Flex } from '../../flex';
-import { StyledAccordionIcon } from './AccordionIcon';
+import { useAccordionContext } from '../AccordionContext';
+import { useAnimation } from '../hooks';
export type AccordionHeaderProps = {
children?: React.ReactNode;
};
-export const StyledAccordionHeader = styled(Flex)`
- background-color: ${colors.blue};
- width: 100%;
- min-height: 48px;
- margin-bottom: 1px;
+export const StyledAccordionHeader = styled(Flex)<{
+ $animation?: RuleSet<object>;
+ $disabled?: boolean;
+}>`
+ ${({ $animation, $disabled }) => {
+ const backgroundColor = $disabled ? colors.blue40 : colors.blue;
+ return css`
+ --background-color: ${backgroundColor};
- && > ${StyledAccordionIcon} {
- margin-left: auto;
- }
+ margin-bottom: 1px;
+ background-color: var(--background-color);
+ min-height: 48px;
+ width: 100%;
+ ${$animation}
+ `;
+ }}
`;
export function AccordionHeader({ children }: AccordionHeaderProps) {
+ const animation = useAnimation();
+ const { disabled } = useAccordionContext();
return (
<StyledAccordionHeader
$padding={{ horizontal: 'medium', vertical: 'small' }}
- $alignItems="center">
+ $alignItems="center"
+ $justifyContent="space-between"
+ $animation={animation}
+ $disabled={disabled}>
{children}
</StyledAccordionHeader>
);
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx
index e9d287b261..e1329dbf77 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx
@@ -1,6 +1,7 @@
import styled from 'styled-components';
import { Text } from '../../typography';
+import { useAccordionContext } from '../AccordionContext';
export type AccordionTitleProps = {
children?: React.ReactNode;
@@ -9,8 +10,13 @@ export type AccordionTitleProps = {
export const StyledTitleLabel = styled(Text)``;
export function AccordionTitle({ children }: AccordionTitleProps) {
+ const { titleId, disabled } = useAccordionContext();
return (
- <StyledTitleLabel $padding="medium" color="white" variant="titleMedium">
+ <StyledTitleLabel
+ id={titleId}
+ $padding="medium"
+ color={disabled ? 'whiteAlpha60' : 'white'}
+ variant="titleMedium">
{children}
</StyledTitleLabel>
);
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx
index ff852e4ea1..dfaffdf317 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx
@@ -4,19 +4,26 @@ import styled from 'styled-components';
import { colors } from '../../../foundations';
import { useAccordionContext } from '../AccordionContext';
import { StyledAccordionHeader } from './AccordionHeader';
+import { StyledAccordionIcon } from './AccordionIcon';
export type AccordionTriggerProps = {
children?: React.ReactNode;
-};
+} & React.ButtonHTMLAttributes<HTMLButtonElement>;
const StyledAccordionTrigger = styled.button`
background-color: transparent;
&&:hover > ${StyledAccordionHeader} {
background-color: ${colors.blue60};
}
+ &&:hover > ${StyledAccordionIcon} {
+ background-color: ${colors.whiteAlpha60};
+ }
&&:active > ${StyledAccordionHeader} {
background-color: ${colors.blue40};
}
+ &&:active > ${StyledAccordionIcon} {
+ background-color: ${colors.whiteAlpha40};
+ }
&&:focus-visible {
outline: 2px solid ${colors.white};
outline-offset: -2px;
@@ -24,7 +31,7 @@ const StyledAccordionTrigger = styled.button`
`;
export function AccordionTrigger({ children }: AccordionTriggerProps) {
- const { contentId, triggerId, expanded, onExpandedChange } = useAccordionContext();
+ const { contentId, triggerId, titleId, expanded, onExpandedChange } = useAccordionContext();
const onClick = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
@@ -37,6 +44,7 @@ export function AccordionTrigger({ children }: AccordionTriggerProps) {
return (
<StyledAccordionTrigger
id={triggerId}
+ aria-labelledby={titleId}
aria-controls={contentId}
aria-expanded={expanded ? 'true' : 'false'}
onClick={onClick}>
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/index.ts
new file mode 100644
index 0000000000..64aafcab29
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useAnimation';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/useAnimation.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/useAnimation.tsx
new file mode 100644
index 0000000000..be34d7963b
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/hooks/useAnimation.tsx
@@ -0,0 +1,33 @@
+import { css, keyframes } from 'styled-components';
+
+import { colors } from '../../../foundations';
+import { useAccordionContext } from '../AccordionContext';
+
+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 } = useAccordionContext();
+ 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;
+};