Skip to content

Commit 99c8867

Browse files
committed
Better collapsible sections, and use them on the account page
1 parent 4ca76be commit 99c8867

File tree

6 files changed

+452
-257
lines changed

6 files changed

+452
-257
lines changed

frontend/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"cancel": "Cancel",
55
"clear": "Clear",
66
"close": "Close",
7+
"collapse": "Collapse",
78
"continue": "Continue",
89
"edit": "Edit",
10+
"expand": "Expand",
911
"save": "Save",
1012
"save_and_continue": "Save and continue",
1113
"start_over": "Start over"
@@ -30,6 +32,8 @@
3032
},
3133
"frontend": {
3234
"account": {
35+
"account_password": "Account password",
36+
"contact_info": "Contact info",
3337
"edit_profile": {
3438
"display_name_help": "This is what others will see wherever you’re signed in.",
3539
"display_name_label": "Display name",
Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,52 @@
1-
/* Copyright 2024 New Vector Ltd.
2-
* Copyright 2024 The Matrix.org Foundation C.I.C.
3-
*
4-
* SPDX-License-Identifier: AGPL-3.0-only
5-
* Please see LICENSE in the repository root for full details.
1+
/* Copyright 2024, 2025 New Vector Ltd.
2+
* Copyright 2024 The Matrix.org Foundation C.I.C.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
66
*/
77

8+
.root {
9+
display: flex;
10+
flex-direction: column;
11+
gap: var(--cpd-space-6x);
12+
}
13+
14+
.heading {
15+
display: flex;
16+
flex-direction: column;
17+
gap: var(--cpd-space-2x);
18+
}
19+
820
.trigger {
921
display: flex;
1022
width: 100%;
23+
align-items: center;
24+
justify-content: space-between;
25+
text-align: start;
26+
gap: var(--cpd-space-2x);
1127
}
1228

1329
.trigger-title {
30+
cursor: pointer;
1431
flex-grow: 1;
15-
text-align: start;
1632
}
1733

18-
[data-state="closed"] .trigger-icon {
34+
.trigger-icon {
35+
transition: transform 0.1s ease-out;
36+
}
37+
38+
.root[data-state="closed"] .trigger-icon {
1939
transform: rotate(180deg);
2040
}
2141

42+
.description {
43+
color: var(--cpd-color-text-secondary);
44+
font: var(--cpd-font-body-md-regular);
45+
letter-spacing: var(--cpd-font-letter-spacing-body-md);
46+
}
47+
2248
.content {
23-
margin-top: var(--cpd-space-2x);
49+
display: flex;
50+
flex-direction: column;
51+
gap: var(--cpd-space-6x);
2452
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2025 New Vector Ltd.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
// Please see LICENSE in the repository root for full details.
5+
6+
import type { Meta, StoryObj } from "@storybook/react";
7+
import * as Collapsible from "./Collapsible";
8+
9+
const meta = {
10+
title: "UI/Collapsible",
11+
component: Collapsible.Section,
12+
tags: ["autodocs"],
13+
} satisfies Meta<typeof Collapsible.Section>;
14+
15+
export default meta;
16+
type Story = StoryObj<typeof Collapsible.Section>;
17+
18+
export const Basic: Story = {
19+
args: {
20+
title: "Section name",
21+
description: "Optional section description",
22+
children: (
23+
<div>
24+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
25+
<p>Sed id felis eget orci aliquet tincidunt.</p>
26+
</div>
27+
),
28+
},
29+
};

frontend/src/components/Collapsible/Collapsible.tsx

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,67 @@
66

77
import * as Collapsible from "@radix-ui/react-collapsible";
88
import IconChevronUp from "@vector-im/compound-design-tokens/assets/web/icons/chevron-up";
9+
import { H4, IconButton } from "@vector-im/compound-web";
910
import classNames from "classnames";
11+
import { useCallback, useId, useState } from "react";
12+
import { useTranslation } from "react-i18next";
1013

1114
import styles from "./Collapsible.module.css";
1215

13-
export const Trigger: React.FC<
14-
React.ComponentProps<typeof Collapsible.Trigger>
15-
> = ({ children, className, ...props }) => {
16+
export const Section: React.FC<
17+
{
18+
title: string;
19+
description?: string;
20+
} & Omit<
21+
React.ComponentProps<typeof Collapsible.Root>,
22+
"asChild" | "aria-labelledby" | "aria-describedby" | "open"
23+
>
24+
> = ({ title, description, defaultOpen, className, children, ...props }) => {
25+
const { t } = useTranslation();
26+
const [open, setOpen] = useState(defaultOpen || false);
27+
const titleId = useId();
28+
const descriptionId = useId();
29+
const onClick = useCallback((e: React.MouseEvent<HTMLElement>) => {
30+
e.preventDefault();
31+
setOpen((open) => !open);
32+
}, []);
33+
1634
return (
17-
<Collapsible.Trigger
35+
<Collapsible.Root
1836
{...props}
19-
className={classNames(styles.trigger, className)}
37+
open={open}
38+
onOpenChange={setOpen}
39+
asChild
40+
aria-labelledby={titleId}
41+
aria-describedby={description ? descriptionId : undefined}
42+
className={classNames(styles.root, className)}
2043
>
21-
<div className={styles.triggerTitle}>{children}</div>
22-
<IconChevronUp
23-
className={styles.triggerIcon}
24-
height="24px"
25-
width="24px"
26-
/>
27-
</Collapsible.Trigger>
28-
);
29-
};
44+
<section>
45+
<header className={styles.heading}>
46+
<div className={styles.trigger}>
47+
<H4 onClick={onClick} id={titleId} className={styles.triggerTitle}>
48+
{title}
49+
</H4>
50+
<Collapsible.Trigger className={styles.triggerIcon} asChild>
51+
<IconButton
52+
tooltip={open ? t("action.collapse") : t("action.expand")}
53+
>
54+
<IconChevronUp />
55+
</IconButton>
56+
</Collapsible.Trigger>
57+
</div>
3058

31-
export const Content: React.FC<
32-
React.ComponentProps<typeof Collapsible.Content>
33-
> = ({ className, ...props }) => {
34-
return (
35-
<Collapsible.Content
36-
{...props}
37-
className={classNames(styles.content, className)}
38-
/>
59+
{description && (
60+
<p className={styles.description} id={descriptionId}>
61+
{description}
62+
</p>
63+
)}
64+
</header>
65+
66+
<Collapsible.Content asChild>
67+
<article className={styles.content}>{children}</article>
68+
</Collapsible.Content>
69+
</section>
70+
</Collapsible.Root>
3971
);
4072
};
41-
42-
export const Root = Collapsible.Root;

frontend/src/routes/_account.index.lazy.tsx

Lines changed: 45 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import {
1010
notFound,
1111
useNavigate,
1212
} from "@tanstack/react-router";
13-
import { Alert, Heading, Separator, Text } from "@vector-im/compound-web";
13+
import { Alert, Separator, Text } from "@vector-im/compound-web";
1414
import { Suspense } from "react";
1515
import { useTranslation } from "react-i18next";
1616

1717
import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview";
18-
import BlockList from "../components/BlockList";
1918
import { ButtonLink } from "../components/ButtonLink";
2019
import * as Collapsible from "../components/Collapsible";
2120
import LoadingSpinner from "../components/LoadingSpinner";
@@ -43,64 +42,55 @@ function Index(): React.ReactElement {
4342
};
4443

4544
return (
46-
<>
47-
<BlockList>
48-
{/* This wrapper is only needed for the anchor link */}
49-
<div className="flex flex-col gap-4" id="emails">
50-
{viewer.primaryEmail ? (
51-
<UserEmail
52-
email={viewer.primaryEmail}
53-
isPrimary
54-
siteConfig={siteConfig}
55-
/>
56-
) : (
57-
<Alert
58-
type="critical"
59-
title={t("frontend.user_email_list.no_primary_email_alert")}
60-
/>
61-
)}
62-
63-
<Suspense fallback={<LoadingSpinner mini className="self-center" />}>
64-
<UserEmailList siteConfig={siteConfig} user={viewer} />
65-
</Suspense>
45+
<div className="flex flex-col gap-4 mb-4">
46+
<Collapsible.Section
47+
defaultOpen
48+
title={t("frontend.account.contact_info")}
49+
>
50+
{viewer.primaryEmail ? (
51+
<UserEmail
52+
email={viewer.primaryEmail}
53+
isPrimary
54+
siteConfig={siteConfig}
55+
/>
56+
) : (
57+
<Alert
58+
type="critical"
59+
title={t("frontend.user_email_list.no_primary_email_alert")}
60+
/>
61+
)}
6662

67-
{siteConfig.emailChangeAllowed && (
68-
<AddEmailForm userId={viewer.id} onAdd={onAdd} />
69-
)}
70-
</div>
63+
<Suspense fallback={<LoadingSpinner mini className="self-center" />}>
64+
<UserEmailList siteConfig={siteConfig} user={viewer} />
65+
</Suspense>
7166

72-
{siteConfig.passwordLoginEnabled && (
73-
<>
74-
<Separator />
67+
{siteConfig.emailChangeAllowed && (
68+
<AddEmailForm userId={viewer.id} onAdd={onAdd} />
69+
)}
70+
</Collapsible.Section>
7571

72+
{siteConfig.passwordLoginEnabled && (
73+
<>
74+
<Separator kind="section" />
75+
<Collapsible.Section
76+
defaultOpen
77+
title={t("frontend.account.account_password")}
78+
>
7679
<AccountManagementPasswordPreview siteConfig={siteConfig} />
77-
</>
78-
)}
80+
</Collapsible.Section>
81+
</>
82+
)}
7983

80-
<Separator />
84+
<Separator kind="section" />
8185

82-
<Collapsible.Root>
83-
<Collapsible.Trigger>
84-
<Heading size="sm" weight="semibold">
85-
{t("common.e2ee")}
86-
</Heading>
87-
</Collapsible.Trigger>
88-
<Collapsible.Content>
89-
<BlockList>
90-
<Text className="text-secondary" size="md">
91-
{t("frontend.reset_cross_signing.description")}
92-
</Text>
93-
<ButtonLink
94-
to="/reset-cross-signing"
95-
kind="secondary"
96-
destructive
97-
>
98-
{t("frontend.reset_cross_signing.start_reset")}
99-
</ButtonLink>
100-
</BlockList>
101-
</Collapsible.Content>
102-
</Collapsible.Root>
103-
</BlockList>
104-
</>
86+
<Collapsible.Section title={t("common.e2ee")}>
87+
<Text className="text-secondary" size="md">
88+
{t("frontend.reset_cross_signing.description")}
89+
</Text>
90+
<ButtonLink to="/reset-cross-signing" kind="secondary" destructive>
91+
{t("frontend.reset_cross_signing.start_reset")}
92+
</ButtonLink>
93+
</Collapsible.Section>
94+
</div>
10595
);
10696
}

0 commit comments

Comments
 (0)