Skip to content

Commit 65bb3f1

Browse files
JonasBaandrewshie-sentry
authored andcommitted
core: add disclosure component (#98895)
We have a disclosure component that is very tightly coupled to issues, this PR adds a design system disclosure component that will provide a base for others to specialize on. I will followup with a PR to change and add tests to the FoldSection component so that it is using the new primitive.
1 parent 0cf2105 commit 65bb3f1

File tree

8 files changed

+490
-0
lines changed

8 files changed

+490
-0
lines changed

knip.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const config: KnipConfig = {
5656
'!static/app/**/__stories__/*.{js,mjs,ts,tsx}!',
5757
'!static/app/stories/**/*.{js,mjs,ts,tsx}!',
5858
// TEMPORARY!
59+
'!static/app/components/core/disclosure/index.tsx',
60+
'!static/app/components/core/disclosure/disclosure.tsx',
5961
'!static/app/utils/timeSeries/useFetchEventsTimeSeries.tsx',
6062
],
6163
compilers: {
@@ -76,6 +78,10 @@ const config: KnipConfig = {
7678
'@babel/preset-typescript', // Still used in jest
7779
'@emotion/babel-plugin', // Still used in jest
7880
'terser', // Still used in a loader
81+
82+
// TEMPORARY!
83+
'@react-stately/disclosure',
84+
'@react-aria/disclosure',
7985
],
8086
rules: {
8187
binaries: 'off',

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@r4ai/remark-callout": "^0.6.2",
2929
"@react-aria/button": "3.14.0",
3030
"@react-aria/combobox": "3.13.0",
31+
"@react-aria/disclosure": "3.0.7",
3132
"@react-aria/focus": "3.21.0",
3233
"@react-aria/gridlist": "3.13.3",
3334
"@react-aria/i18n": "3.12.11",
@@ -48,6 +49,7 @@
4849
"@react-google-maps/api": "^2.12.0",
4950
"@react-stately/collections": "3.12.6",
5051
"@react-stately/combobox": "3.11.0",
52+
"@react-stately/disclosure": "3.0.6",
5153
"@react-stately/form": "3.2.0",
5254
"@react-stately/list": "3.12.4",
5355
"@react-stately/numberfield": "3.10.0",

pnpm-lock.yaml

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
---
2+
title: Disclosure
3+
description: Disclosure components display content that can be toggled open and closed.
4+
source: 'sentry/components/core/disclosure/disclosure'
5+
resources:
6+
js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/disclosure/disclosure.tsx
7+
a11y:
8+
WCAG 1.4.3: https://www.w3.org/TR/WCAG22/#contrast-minimum
9+
WCAG 1.4.4: https://www.w3.org/TR/WCAG22/#resize-text
10+
WCAG 1.4.6: https://www.w3.org/TR/WCAG22/#contrast-enhanced
11+
WCAG 1.4.8: https://www.w3.org/TR/WCAG22/#visual-presentation
12+
WCAG 1.4.12: https://www.w3.org/TR/WCAG22/#text-spacing
13+
---
14+
15+
import {useState} from 'react';
16+
17+
import {IconEdit} from 'sentry/icons';
18+
19+
import {Button} from 'sentry/components/core/button';
20+
import {Flex, Stack} from 'sentry/components/core/layout';
21+
22+
import {Disclosure} from 'sentry/components/core/disclosure/disclosure';
23+
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
24+
25+
import * as Storybook from 'sentry/stories';
26+
27+
import types from '!!type-loader!sentry/components/core/disclosure/disclosure';
28+
29+
export {types};
30+
31+
To create a disclosure, wrap content in a `<Disclosure>` component.
32+
33+
The disclosure component supports a controlled and uncontrolled state via the `defaultExpanded` and `expanded` props.
34+
35+
<Storybook.Demo>
36+
<Flex width="100%">
37+
<Disclosure>
38+
<Disclosure.Title>This is a disclosure</Disclosure.Title>
39+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
40+
</Disclosure>
41+
</Flex>
42+
</Storybook.Demo>
43+
44+
```jsx
45+
<Disclosure>
46+
<Disclosure.Title>This is a disclosure</Disclosure.Title>
47+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
48+
</Disclosure>
49+
```
50+
51+
## Controlling Expanded State
52+
53+
The disclosure can be uncontrolled by setting the `defaultExpanded` prop, or controlled by setting the `expanded` prop.
54+
55+
<Storybook.Demo>
56+
<Flex width="100%">
57+
<Disclosure defaultExpanded>
58+
<Disclosure.Title>This is a default expanded disclosure</Disclosure.Title>
59+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
60+
</Disclosure>
61+
</Flex>
62+
</Storybook.Demo>
63+
64+
```jsx
65+
<Disclosure defaultExpanded>
66+
<Disclosure.Title>This is a default expanded disclosure</Disclosure.Title>
67+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
68+
</Disclosure>
69+
```
70+
71+
### Sizes
72+
73+
The disclosure can be sized by setting the `size` prop.
74+
75+
<Storybook.Demo>
76+
<Flex width="100%">
77+
<Stack direction="column" justify="center" gap="2xl">
78+
<Disclosure size="md" defaultExpanded>
79+
<Disclosure.Title>This is medium disclosure</Disclosure.Title>
80+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
81+
</Disclosure>
82+
<Disclosure size="sm" defaultExpanded>
83+
<Disclosure.Title>This is small disclosure</Disclosure.Title>
84+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
85+
</Disclosure>
86+
<Disclosure size="xs" defaultExpanded>
87+
<Disclosure.Title>This is extra small disclosure</Disclosure.Title>
88+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
89+
</Disclosure>
90+
</Stack>
91+
</Flex>
92+
</Storybook.Demo>
93+
94+
```jsx
95+
<Disclosure size="md" defaultExpanded>
96+
<Disclosure.Title>This is medium disclosure</Disclosure.Title>
97+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
98+
</Disclosure>
99+
<Disclosure size="sm" defaultExpanded>
100+
<Disclosure.Title>This is small disclosure</Disclosure.Title>
101+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
102+
</Disclosure>
103+
<Disclosure size="xs" defaultExpanded>
104+
<Disclosure.Title>This is extra small disclosure</Disclosure.Title>
105+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
106+
</Disclosure>
107+
```
108+
109+
### Trailing Items
110+
111+
Add interactive elements like buttons, links or badges to the right side of disclosures using the `trailingItems` prop.
112+
113+
Note: The trailing items should be sized to match the disclosure size - this is currently **not** done automatically.
114+
115+
<Storybook.Demo>
116+
<Flex width="100%">
117+
<Disclosure size="sm" defaultExpanded>
118+
<Disclosure.Title
119+
trailingItems={
120+
<Button size="xs" icon={<IconEdit />}>
121+
Trailing Item
122+
</Button>
123+
}
124+
>
125+
Title With Trailing Items
126+
</Disclosure.Title>
127+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
128+
</Disclosure>
129+
</Flex>
130+
</Storybook.Demo>
131+
132+
```jsx
133+
<Disclosure size="sm" defaultExpanded>
134+
<Disclosure.Title
135+
trailingItems={
136+
<Button size="xs" icon={<IconEdit />}>
137+
Trailing Item
138+
</Button>
139+
}
140+
>
141+
Title With Trailing Items
142+
</Disclosure.Title>
143+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
144+
</Disclosure>
145+
```
146+
147+
### Remembering Expanded State
148+
149+
The disclosure component provides a `onExpandedChange` prop that can be used to listen for changes to the expanded state - this allows you to store the state in a storage of your choice.
150+
151+
We recommend storing it inside URL State or localStorage for persistent storage.
152+
153+
export function LocalStorageDisclosure() {
154+
const [expanded, setExpanded] = useLocalStorageState('my-disclosure-key', false);
155+
156+
return (
157+
<Disclosure expanded={expanded} onExpandedChange={setExpanded}>
158+
<Disclosure.Title>The expanded state is saved to localStorage</Disclosure.Title>
159+
<Disclosure.Content>Reload the page to see the expanded state is remembered</Disclosure.Content>
160+
</Disclosure>
161+
);
162+
163+
}
164+
165+
Disclosures can maintain their expanded state across page reloads by using localStorage or URL state.
166+
167+
#### LocalStorage
168+
169+
<Storybook.Demo>
170+
<Flex width="100%">
171+
<LocalStorageDisclosure />
172+
</Flex>
173+
</Storybook.Demo>
174+
175+
```tsx
176+
function LocalStorageDisclosure() {
177+
const [expanded, setExpanded] = useLocalStorageState('my-disclosure-key', false);
178+
179+
return (
180+
<Disclosure expanded={expanded} onExpandedChange={setExpanded}>
181+
<Disclosure.Title>The expanded state is saved to localStorage</Disclosure.Title>
182+
<Disclosure.Content>
183+
Reload the page to see the expanded state is remembered
184+
</Disclosure.Content>
185+
</Disclosure>
186+
);
187+
}
188+
```
189+
190+
#### URL State
191+
192+
Disclosures can maintain their expanded state across page reloads by using URL state.
193+
194+
export function URLStateDisclosure() {
195+
const [expanded, setExpanded] = useState(new URLSearchParams(window.location.search).get('my-disclosure-key') === 'true');
196+
197+
return (
198+
<Disclosure defaultExpanded={expanded} onExpandedChange={(expanded) => {
199+
const url = new URL(window.location.href);
200+
url.searchParams.set('my-disclosure-key', expanded ? 'true' : 'false');
201+
window.history.pushState({}, '', url.toString());
202+
setExpanded(expanded);
203+
}}>
204+
<Disclosure.Title>The expanded state is saved to URL state</Disclosure.Title>
205+
<Disclosure.Content>Reload the page to see the expanded state is remembered and new params are added to the URL</Disclosure.Content>
206+
</Disclosure>
207+
);
208+
209+
}
210+
211+
<Storybook.Demo>
212+
<Flex width="100%">
213+
<URLStateDisclosure />
214+
</Flex>
215+
</Storybook.Demo>
216+
217+
```tsx
218+
function URLStateDisclosure() {
219+
const [expanded, setExpanded] = useState<boolean>(
220+
new URLSearchParams(window.location.search).get('my-disclosure-key') === 'true'
221+
);
222+
223+
return (
224+
<Disclosure
225+
defaultExpanded={expanded}
226+
onExpandedChange={expanded => {
227+
const url = new URL(window.location.href);
228+
url.searchParams.set('my-disclosure-key', expanded ? 'true' : 'false');
229+
window.history.pushState({}, '', url.toString());
230+
setExpanded(expanded);
231+
}}
232+
>
233+
<Disclosure.Title>The expanded state is saved to URL state</Disclosure.Title>
234+
<Disclosure.Content>
235+
Reload the page to see the expanded state is remembered
236+
</Disclosure.Content>
237+
</Disclosure>
238+
);
239+
}
240+
```
241+
242+
### Scrolling to a Disclosure
243+
244+
Disclosures can be scrolled to with the help of a render ref.
245+
246+
```tsx
247+
<Disclosure
248+
ref={ref => {
249+
if (ref) ref.scrollIntoView();
250+
}}
251+
>
252+
<Disclosure.Title>This is a disclosure</Disclosure.Title>
253+
<Disclosure.Content>This is the content of the disclosure</Disclosure.Content>
254+
</Disclosure>
255+
```

0 commit comments

Comments
 (0)