summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-05-29 09:15:13 +0200
committerOliver <oliver@mohlin.dev>2025-06-05 10:26:46 +0200
commitdc8fcd776db1668b378c69988d88f12cc0bfe695 (patch)
treef6bbb270c1cb8c9a7d80e53524c708b4ccc14c31
parentfe325b16c5fb14235d4f8f76d59c1815add1fc67 (diff)
downloadmullvadvpn-dc8fcd776db1668b378c69988d88f12cc0bfe695.tar.xz
mullvadvpn-dc8fcd776db1668b378c69988d88f12cc0bfe695.zip
Add Accordion
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx45
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx42
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionContent.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx30
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionIcon.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx17
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx47
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/index.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/index.ts1
9 files changed, 228 insertions, 0 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
new file mode 100644
index 0000000000..2ed9043972
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/Accordion.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import styled from 'styled-components';
+
+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 AccordionProps = {
+ expanded?: boolean;
+ onExpandedChange?: (open: boolean) => void;
+ children?: React.ReactNode;
+};
+
+const StyledAccordion = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ width: 100%;
+`;
+
+function Accordion({ expanded = false, onExpandedChange: onOpenChange, children }: AccordionProps) {
+ const triggerId = React.useId();
+ const contentId = React.useId();
+ return (
+ <AccordionProvider
+ triggerId={triggerId}
+ contentId={contentId}
+ expanded={expanded}
+ onExpandedChange={onOpenChange}>
+ <StyledAccordion>{children}</StyledAccordion>
+ </AccordionProvider>
+ );
+}
+
+const AccordionNamespace = Object.assign(Accordion, {
+ Trigger: AccordionTrigger,
+ Header: AccordionHeader,
+ Content: AccordionContent,
+ Title: AccordionTitle,
+ Icon: AccordionIcon,
+});
+
+export { AccordionNamespace as Accordion };
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
new file mode 100644
index 0000000000..e39295f1a8
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/AccordionContext.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import { AccordionProps } from './Accordion';
+
+interface AccordionContextProps {
+ triggerId: string;
+ contentId: string;
+ expanded: AccordionProps['expanded'];
+ onExpandedChange?: AccordionProps['onExpandedChange'];
+}
+
+const AccordionContext = React.createContext<AccordionContextProps | undefined>(undefined);
+
+export const useAccordionContext = (): AccordionContextProps => {
+ const context = React.useContext(AccordionContext);
+ if (!context) {
+ throw new Error('useAccordionContext must be used within a AccordionProvider');
+ }
+ return context;
+};
+
+interface AccordionProviderProps {
+ triggerId: string;
+ contentId: string;
+ expanded: boolean;
+ onExpandedChange?: (open: boolean) => void;
+ children: React.ReactNode;
+}
+
+export function AccordionProvider({
+ triggerId,
+ contentId,
+ expanded,
+ onExpandedChange,
+ children,
+}: AccordionProviderProps) {
+ return (
+ <AccordionContext.Provider value={{ triggerId, contentId, expanded, onExpandedChange }}>
+ {children}
+ </AccordionContext.Provider>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionContent.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionContent.tsx
new file mode 100644
index 0000000000..beeb3ff238
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionContent.tsx
@@ -0,0 +1,26 @@
+import styled from 'styled-components';
+
+import { Animate } from '../../animate';
+import { useAccordionContext } from '../AccordionContext';
+
+export type AccordionContentProps = {
+ children?: React.ReactNode;
+};
+
+const StyledAccordionContent = styled.div`
+ width: 100%;
+`;
+
+export function AccordionContent({ children }: AccordionContentProps) {
+ const { contentId, triggerId, expanded } = useAccordionContext();
+ return (
+ <Animate
+ present={expanded}
+ animations={[{ type: 'wipe', direction: 'vertical' }]}
+ duration="0.35s">
+ <StyledAccordionContent id={contentId} aria-labelledby={triggerId} role="region">
+ {children}
+ </StyledAccordionContent>
+ </Animate>
+ );
+}
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
new file mode 100644
index 0000000000..b71effcfba
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionHeader.tsx
@@ -0,0 +1,30 @@
+import styled from 'styled-components';
+
+import { colors } from '../../../foundations';
+import { Flex } from '../../flex';
+import { StyledAccordionIcon } from './AccordionIcon';
+
+export type AccordionHeaderProps = {
+ children?: React.ReactNode;
+};
+
+export const StyledAccordionHeader = styled(Flex)`
+ background-color: ${colors.blue};
+ width: 100%;
+ min-height: 48px;
+ margin-bottom: 1px;
+
+ && > ${StyledAccordionIcon} {
+ margin-left: auto;
+ }
+`;
+
+export function AccordionHeader({ children }: AccordionHeaderProps) {
+ return (
+ <StyledAccordionHeader
+ $padding={{ horizontal: 'medium', vertical: 'small' }}
+ $alignItems="center">
+ {children}
+ </StyledAccordionHeader>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionIcon.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionIcon.tsx
new file mode 100644
index 0000000000..043a72fa00
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionIcon.tsx
@@ -0,0 +1,18 @@
+import styled from 'styled-components';
+
+import { Icon, IconProps } from '../../icon';
+import { useAccordionContext } from '../AccordionContext';
+
+export type AccordionIconProps = Omit<IconProps, 'icon'> & {
+ icon?: IconProps['icon'];
+};
+
+export const StyledAccordionIcon = styled(Icon)`
+ flex-shrink: 0;
+`;
+
+export function AccordionIcon({ icon, color = 'whiteAlpha80', ...props }: AccordionIconProps) {
+ const { expanded: open } = useAccordionContext();
+ const iconName = icon || (open ? 'chevron-up' : 'chevron-down');
+ return <StyledAccordionIcon icon={iconName} color={color} {...props} />;
+}
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
new file mode 100644
index 0000000000..e9d287b261
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTitle.tsx
@@ -0,0 +1,17 @@
+import styled from 'styled-components';
+
+import { Text } from '../../typography';
+
+export type AccordionTitleProps = {
+ children?: React.ReactNode;
+};
+
+export const StyledTitleLabel = styled(Text)``;
+
+export function AccordionTitle({ children }: AccordionTitleProps) {
+ return (
+ <StyledTitleLabel $padding="medium" color="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
new file mode 100644
index 0000000000..30ba2092c0
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/AccordionTrigger.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { colors } from '../../../foundations';
+import { ButtonBase } from '../../button';
+import { useAccordionContext } from '../AccordionContext';
+import { StyledAccordionHeader } from './AccordionHeader';
+
+export type AccordionTriggerProps = {
+ children?: React.ReactNode;
+};
+
+const StyledAccordionTrigger = styled(ButtonBase)`
+ background-color: transparent;
+ &&:hover > ${StyledAccordionHeader} {
+ background-color: ${colors.blue60};
+ }
+ &&:active > ${StyledAccordionHeader} {
+ background-color: ${colors.blue40};
+ }
+ &&:focus-visible {
+ outline: 2px solid ${colors.white};
+ outline-offset: -2px;
+ }
+`;
+
+export function AccordionTrigger({ children }: AccordionTriggerProps) {
+ const { contentId, triggerId, expanded, onExpandedChange } = useAccordionContext();
+
+ const onClick = React.useCallback(
+ (e: React.MouseEvent<HTMLButtonElement>) => {
+ e.preventDefault();
+ onExpandedChange?.(!expanded);
+ },
+ [onExpandedChange, expanded],
+ );
+
+ return (
+ <StyledAccordionTrigger
+ id={triggerId}
+ aria-controls={contentId}
+ aria-expanded={expanded ? 'true' : 'false'}
+ onClick={onClick}>
+ {children}
+ </StyledAccordionTrigger>
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/index.ts
new file mode 100644
index 0000000000..abd6b8865b
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/components/index.ts
@@ -0,0 +1,2 @@
+export * from './AccordionHeader';
+export * from './AccordionTrigger';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/index.ts
new file mode 100644
index 0000000000..63f62bc659
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/accordion/index.ts
@@ -0,0 +1 @@
+export * from './Accordion';