Skip to content

Commit b958085

Browse files
feat(s2): update accordion api to allow sibling elements in disclosure header (#7179)
* update s2 accordion api * fix lint * rename to disclosure title, context for action button * fix lint * remove console log * fix typscript * allow disclosure title to wrap header * add chromatic stories * make code more concise * fix lint * fix chromatic stories --------- Co-authored-by: Robert Snow <rsnow@adobe.com>
1 parent e1b72a7 commit b958085

File tree

6 files changed

+221
-49
lines changed

6 files changed

+221
-49
lines changed

packages/@react-spectrum/s2/chromatic/Accordion.stories.tsx

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Accordion, Disclosure, DisclosureHeader, DisclosurePanel, TextField} from '../src';
13+
import {Accordion, ActionButton, Disclosure, DisclosureHeader, DisclosurePanel, DisclosureTitle, TextField} from '../src';
1414
import type {Meta, StoryObj} from '@storybook/react';
15+
import NewIcon from '../s2wf-icons/S2_Icon_New_20_N.svg';
1516
import React from 'react';
1617
import {style} from '../style/spectrum-theme' with { type: 'macro' };
1718

@@ -32,17 +33,17 @@ export const Example: Story = {
3233
<div className={style({minHeight: 240})}>
3334
<Accordion {...args}>
3435
<Disclosure id="files">
35-
<DisclosureHeader>
36+
<DisclosureTitle>
3637
Files
37-
</DisclosureHeader>
38+
</DisclosureTitle>
3839
<DisclosurePanel>
3940
Files content
4041
</DisclosurePanel>
4142
</Disclosure>
4243
<Disclosure id="people">
43-
<DisclosureHeader>
44+
<DisclosureTitle>
4445
People
45-
</DisclosureHeader>
46+
</DisclosureTitle>
4647
<DisclosurePanel>
4748
<TextField label="Name" styles={style({maxWidth: 176})} />
4849
</DisclosurePanel>
@@ -59,25 +60,25 @@ export const WithLongTitle: Story = {
5960
<div className={style({minHeight: 224})}>
6061
<Accordion styles={style({maxWidth: 224})} {...args}>
6162
<Disclosure>
62-
<DisclosureHeader>
63+
<DisclosureTitle>
6364
Files
64-
</DisclosureHeader>
65+
</DisclosureTitle>
6566
<DisclosurePanel>
6667
Files content
6768
</DisclosurePanel>
6869
</Disclosure>
6970
<Disclosure>
70-
<DisclosureHeader>
71+
<DisclosureTitle>
7172
People
72-
</DisclosureHeader>
73+
</DisclosureTitle>
7374
<DisclosurePanel>
7475
People content
7576
</DisclosurePanel>
7677
</Disclosure>
7778
<Disclosure>
78-
<DisclosureHeader>
79+
<DisclosureTitle>
7980
Very very very very very long title that wraps
80-
</DisclosureHeader>
81+
</DisclosureTitle>
8182
<DisclosurePanel>
8283
Accordion content
8384
</DisclosurePanel>
@@ -94,17 +95,17 @@ export const WithDisabledDisclosure: Story = {
9495
<div className={style({minHeight: 240})}>
9596
<Accordion {...args}>
9697
<Disclosure>
97-
<DisclosureHeader>
98+
<DisclosureTitle>
9899
Files
99-
</DisclosureHeader>
100+
</DisclosureTitle>
100101
<DisclosurePanel>
101102
Files content
102103
</DisclosurePanel>
103104
</Disclosure>
104105
<Disclosure isDisabled>
105-
<DisclosureHeader>
106+
<DisclosureTitle>
106107
People
107-
</DisclosureHeader>
108+
</DisclosureTitle>
108109
<DisclosurePanel>
109110
<TextField label="Name" />
110111
</DisclosurePanel>
@@ -127,3 +128,35 @@ WithDisabledDisclosure.parameters = {
127128
}
128129
};
129130

131+
export const WithActionButton: Story = {
132+
render: (args) => {
133+
return (
134+
<div className={style({minHeight: 240})}>
135+
<Accordion {...args}>
136+
<Disclosure id="files">
137+
<DisclosureHeader>
138+
<DisclosureTitle>
139+
Files
140+
</DisclosureTitle>
141+
<ActionButton><NewIcon aria-label="new icon" /></ActionButton>
142+
</DisclosureHeader>
143+
<DisclosurePanel>
144+
Files content
145+
</DisclosurePanel>
146+
</Disclosure>
147+
<Disclosure id="people">
148+
<DisclosureHeader>
149+
<DisclosureTitle>
150+
People
151+
</DisclosureTitle>
152+
<ActionButton><NewIcon aria-label="new icon" /></ActionButton>
153+
</DisclosureHeader>
154+
<DisclosurePanel>
155+
<TextField label="Name" styles={style({maxWidth: 176})} />
156+
</DisclosurePanel>
157+
</Disclosure>
158+
</Accordion>
159+
</div>
160+
);
161+
}
162+
};

packages/@react-spectrum/s2/chromatic/Disclosure.stories.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Disclosure, DisclosureHeader, DisclosurePanel} from '../src';
13+
import {ActionButton, Disclosure, DisclosureHeader, DisclosurePanel, DisclosureTitle} from '../src';
1414
import type {Meta, StoryObj} from '@storybook/react';
15+
import NewIcon from '../s2wf-icons/S2_Icon_New_20_N.svg';
1516
import React from 'react';
1617
import {style} from '../style/spectrum-theme' with { type: 'macro' };
1718

@@ -31,9 +32,9 @@ export const Example: Story = {
3132
return (
3233
<div className={style({minHeight: 240})}>
3334
<Disclosure {...args}>
34-
<DisclosureHeader>
35+
<DisclosureTitle>
3536
Files
36-
</DisclosureHeader>
37+
</DisclosureTitle>
3738
<DisclosurePanel>
3839
Files content
3940
</DisclosurePanel>
@@ -48,9 +49,9 @@ export const WithLongTitle: Story = {
4849
return (
4950
<div className={style({minHeight: 240})}>
5051
<Disclosure styles={style({maxWidth: 224})} {...args}>
51-
<DisclosureHeader>
52+
<DisclosureTitle>
5253
Very very very very very long title that wraps
53-
</DisclosureHeader>
54+
</DisclosureTitle>
5455
<DisclosurePanel>
5556
Content
5657
</DisclosurePanel>
@@ -66,3 +67,23 @@ WithLongTitle.parameters = {
6667
disable: true
6768
}
6869
};
70+
71+
export const WithActionButton: Story = {
72+
render: (args) => {
73+
return (
74+
<div className={style({minHeight: 240})}>
75+
<Disclosure {...args}>
76+
<DisclosureHeader>
77+
<DisclosureTitle>
78+
Files
79+
</DisclosureTitle>
80+
<ActionButton><NewIcon aria-label="new icon " /></ActionButton>
81+
</DisclosureHeader>
82+
<DisclosurePanel>
83+
Files content
84+
</DisclosurePanel>
85+
</Disclosure>
86+
</div>
87+
);
88+
}
89+
};

packages/@react-spectrum/s2/src/Disclosure.tsx

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {AriaLabelingProps, DOMProps, DOMRef, DOMRefValue} from '@react-types/shared';
13+
import {ActionButtonContext} from './ActionButton';
14+
import {AriaLabelingProps, DOMProps, DOMRef, DOMRefValue, forwardRefType} from '@react-types/shared';
1415
import {Button, ContextValue, DisclosureStateContext, Heading, Provider, UNSTABLE_Disclosure as RACDisclosure, UNSTABLE_DisclosurePanel as RACDisclosurePanel, DisclosurePanelProps as RACDisclosurePanelProps, DisclosureProps as RACDisclosureProps, useLocale, useSlottedContext} from 'react-aria-components';
1516
import {CenterBaseline} from './CenterBaseline';
1617
import {centerPadding, getAllowedOverrides, StyleProps, UnsafeStyles} from './style-utils' with { type: 'macro' };
@@ -93,7 +94,7 @@ function Disclosure(props: DisclosureProps, ref: DOMRef<HTMLDivElement>) {
9394
let _Disclosure = forwardRef(Disclosure);
9495
export {_Disclosure as Disclosure};
9596

96-
export interface DisclosureHeaderProps extends UnsafeStyles, DOMProps {
97+
export interface DisclosureTitleProps extends UnsafeStyles, DOMProps {
9798
/** The heading level of the disclosure header.
9899
*
99100
* @default 3
@@ -103,8 +104,13 @@ export interface DisclosureHeaderProps extends UnsafeStyles, DOMProps {
103104
children: React.ReactNode
104105
}
105106

107+
interface DisclosureHeaderProps extends UnsafeStyles, DOMProps {
108+
children: React.ReactNode
109+
}
110+
106111
const headingStyle = style({
107-
margin: 0
112+
margin: 0,
113+
flexGrow: 1
108114
});
109115

110116
const buttonStyles = style({
@@ -195,7 +201,52 @@ const chevronStyles = style({
195201
flexShrink: 0
196202
});
197203

198-
function DisclosureHeader(props: DisclosureHeaderProps, ref: DOMRef<HTMLDivElement>) {
204+
const InternalDisclosureHeader = createContext<{} | null>(null);
205+
206+
function DisclosureHeaderWithForwardRef(props: DisclosureHeaderProps, ref: DOMRef<HTMLDivElement>) {
207+
let {
208+
UNSAFE_className,
209+
UNSAFE_style,
210+
children
211+
} = props;
212+
let domRef = useDOMRef(ref);
213+
let {size, isQuiet, density} = useSlottedContext(DisclosureContext)!;
214+
215+
let mapSize = {
216+
S: 'XS',
217+
M: 'S',
218+
L: 'M',
219+
XL: 'L'
220+
};
221+
222+
// maps to one size smaller in the compact density to ensure there is space between the top and bottom of the action button and container
223+
let newSize : 'XS' | 'S' | 'M' | 'L' | 'XL' | undefined = size;
224+
if (density === 'compact') {
225+
newSize = mapSize[size ?? 'M'] as 'XS' | 'S' | 'M' | 'L';
226+
}
227+
228+
return (
229+
<Provider
230+
values={[
231+
[ActionButtonContext, {size: newSize, isQuiet}],
232+
[InternalDisclosureHeader, {}]
233+
]}>
234+
<div
235+
style={UNSAFE_style}
236+
className={(UNSAFE_className ?? '') + style({display: 'flex', alignItems: 'center', gap: 4})}
237+
ref={domRef}>
238+
{children}
239+
</div>
240+
</Provider>
241+
);
242+
}
243+
244+
/**
245+
* A wrapper element for the disclosure title that can contain other elements not part of the trigger.
246+
*/
247+
export const DisclosureHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(DisclosureHeaderWithForwardRef);
248+
249+
function DisclosureTitle(props: DisclosureTitleProps, ref: DOMRef<HTMLDivElement>) {
199250
let {
200251
level = 3,
201252
UNSAFE_style,
@@ -208,7 +259,8 @@ function DisclosureHeader(props: DisclosureHeaderProps, ref: DOMRef<HTMLDivEleme
208259
let {isExpanded} = useContext(DisclosureStateContext)!;
209260
let {size, density, isQuiet} = useSlottedContext(DisclosureContext)!;
210261
let isRTL = direction === 'rtl';
211-
return (
262+
263+
let buttonTrigger = (
212264
<Heading
213265
{...domProps}
214266
level={level}
@@ -223,13 +275,23 @@ function DisclosureHeader(props: DisclosureHeaderProps, ref: DOMRef<HTMLDivEleme
223275
</Button>
224276
</Heading>
225277
);
278+
let ctx = useContext(InternalDisclosureHeader);
279+
if (ctx) {
280+
return buttonTrigger;
281+
}
282+
283+
return (
284+
<DisclosureHeader>
285+
{buttonTrigger}
286+
</DisclosureHeader>
287+
);
226288
}
227289

228290
/**
229-
* A header for a disclosure. Contains a heading and a trigger button to expand/collapse the panel.
291+
* A disclosure title consisting of a heading and a trigger button to expand/collapse the panel.
230292
*/
231-
let _DisclosureHeader = forwardRef(DisclosureHeader);
232-
export {_DisclosureHeader as DisclosureHeader};
293+
let _DisclosureTitle = forwardRef(DisclosureTitle);
294+
export {_DisclosureTitle as DisclosureTitle};
233295

234296
export interface DisclosurePanelProps extends Omit<RACDisclosurePanelProps, 'className' | 'style' | 'children'>, UnsafeStyles, DOMProps, AriaLabelingProps {
235297
children: React.ReactNode

packages/@react-spectrum/s2/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export {ColorSwatchPicker, ColorSwatchPickerContext} from './ColorSwatchPicker';
3232
export {ColorWheel, ColorWheelContext} from './ColorWheel';
3333
export {ComboBox, ComboBoxItem, ComboBoxSection, ComboBoxContext} from './ComboBox';
3434
export {ContextualHelp, ContextualHelpContext} from './ContextualHelp';
35-
export {DisclosureHeader, Disclosure, DisclosurePanel, DisclosureContext} from './Disclosure';
35+
export {DisclosureHeader, Disclosure, DisclosurePanel, DisclosureContext, DisclosureTitle} from './Disclosure';
3636
export {Heading, HeadingContext, Header, HeaderContext, Content, ContentContext, Footer, FooterContext, Text, TextContext, Keyboard, KeyboardContext} from './Content';
3737
export {Dialog} from './Dialog';
3838
export {DialogTrigger} from './DialogTrigger';

0 commit comments

Comments
 (0)