Skip to content

Commit d1cb21e

Browse files
committed
Switch to DisclosureGroup
1 parent 99a0133 commit d1cb21e

File tree

4 files changed

+138
-24
lines changed

4 files changed

+138
-24
lines changed

packages/gitbook/src/components/DocumentView/OpenAPI/style.css

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@
334334
}
335335

336336
.openapi-securities-scopes ul {
337-
@apply !my-0 !list-none !pl-0;
337+
@apply !my-0 ml-4 pl-0;
338338
}
339339

340340
.openapi-securities-url {
@@ -1016,11 +1016,7 @@ body:has(.openapi-select-popover) {
10161016
}
10171017

10181018
.openapi-required-scopes {
1019-
@apply ring-1 ring-tint-subtle text-base font-medium mx-0;
1020-
}
1021-
1022-
.openapi-required-scopes .openapi-disclosure-trigger-label {
1023-
@apply top-1/2 -translate-y-1/2;
1019+
@apply border text-base rounded-md straight-corners:rounded-none circular-corners:rounded-md font-medium mx-0;
10241020
}
10251021

10261022
.openapi-required-scopes .openapi-required-scopes-header {
@@ -1029,4 +1025,15 @@ body:has(.openapi-select-popover) {
10291025

10301026
.openapi-required-scopes .openapi-required-scopes-header svg {
10311027
@apply size-3.5 text-tint-subtle rotate-none;
1028+
}
1029+
1030+
.openapi-required-scopes .openapi-disclosure-group-panel {
1031+
@apply px-3 pb-3;
1032+
}
1033+
.openapi-required-scopes .openapi-securities-scopes {
1034+
@apply ml-6;
1035+
}
1036+
1037+
.openapi-required-scopes .openapi-required-scopes-description {
1038+
@apply text-xs text-tint-subtle font-normal mb-2;
10321039
}

packages/react-openapi/src/OpenAPIDisclosureGroup.tsx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
'use client';
22

3+
import clsx from 'classnames';
34
import { createContext, useContext, useRef } from 'react';
45
import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
6+
import type { Key } from 'react-aria';
57
import {
68
type DisclosureGroupProps,
79
type DisclosureGroupState,
810
useDisclosureGroupState,
911
useDisclosureState,
1012
} from 'react-stately';
13+
import { useStore } from 'zustand';
1114
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
15+
import { getOrCreateDisclosureStoreByKey } from './getOrCreateDisclosureStoreByKey';
1216

1317
interface Props {
1418
groups: TDisclosureGroup[];
1519
icon?: React.ReactNode;
1620
/** State key to use with a store */
1721
selectStateKey?: string;
22+
/** State key to synchronize disclosure groups across the page */
23+
stateKey?: string;
1824
/** Icon to display for the select */
1925
selectIcon?: React.ReactNode;
26+
className?: string;
2027
}
2128

2229
type TDisclosureGroup = {
@@ -31,18 +38,50 @@ type TDisclosureGroup = {
3138

3239
const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);
3340

41+
function useDisclosureGroupStore(stateKey = 'disclosure-group', initialKeys?: Iterable<Key>) {
42+
const store = useStore(getOrCreateDisclosureStoreByKey(stateKey, initialKeys));
43+
return store;
44+
}
45+
3446
/**
3547
* Display an interactive OpenAPI disclosure group.
3648
*/
3749
export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
38-
const { icon, groups, selectStateKey, selectIcon } = props;
50+
const {
51+
icon,
52+
groups,
53+
selectStateKey,
54+
stateKey,
55+
selectIcon,
56+
className,
57+
expandedKeys,
58+
defaultExpandedKeys,
59+
onExpandedChange,
60+
} = props;
61+
62+
const initialKeys =
63+
expandedKeys || defaultExpandedKeys
64+
? new Set(expandedKeys || defaultExpandedKeys)
65+
: undefined;
66+
const { expandedKeys: storeExpandedKeys, setExpandedKeys } = useDisclosureGroupStore(
67+
stateKey,
68+
initialKeys
69+
);
3970

40-
const state = useDisclosureGroupState(props);
71+
const state = useDisclosureGroupState({
72+
...props,
73+
expandedKeys: storeExpandedKeys,
74+
onExpandedChange: (keys) => {
75+
setExpandedKeys(keys);
76+
onExpandedChange?.(keys);
77+
},
78+
});
4179

4280
return (
4381
<DisclosureGroupStateContext.Provider value={state}>
4482
{groups.map((group) => (
4583
<DisclosureItem
84+
className={className}
4685
selectStateKey={selectStateKey}
4786
selectIcon={selectIcon}
4887
icon={icon}
@@ -59,8 +98,9 @@ function DisclosureItem(props: {
5998
icon?: React.ReactNode;
6099
selectStateKey?: string;
61100
selectIcon?: React.ReactNode;
101+
className?: string;
62102
}) {
63-
const { icon, group, selectStateKey, selectIcon } = props;
103+
const { icon, group, selectStateKey, selectIcon, className } = props;
64104

65105
const defaultId = useId();
66106
const id = group.key || defaultId;
@@ -95,7 +135,10 @@ function DisclosureItem(props: {
95135
const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0];
96136

97137
return (
98-
<div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
138+
<div
139+
className={clsx('openapi-disclosure-group', className)}
140+
aria-expanded={state.isExpanded}
141+
>
99142
<div
100143
slot="trigger"
101144
ref={triggerRef}

packages/react-openapi/src/OpenAPIRequiredScopes.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { OpenAPICopyButton } from './OpenAPICopyButton';
4-
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
4+
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
55
import { useSelectState } from './OpenAPISelect';
66
import type { OpenAPIClientContext } from './context';
77
import { t } from './translate';
@@ -39,20 +39,30 @@ export function OpenAPIRequiredScopes(props: {
3939
}
4040

4141
return (
42-
<OpenAPIDisclosure
43-
defaultExpanded
42+
<OpenAPIDisclosureGroup
4443
className="openapi-required-scopes"
45-
header={
46-
<div className="openapi-required-scopes-header">
47-
{context.icons.lock}
48-
<span>{t(context.translation, 'required_scopes')}</span>
49-
</div>
50-
}
51-
icon={context.icons.plus}
52-
label=""
53-
>
54-
<OpenAPISchemaScopes scopes={scopes} context={context} />
55-
</OpenAPIDisclosure>
44+
icon={context.icons.chevronRight}
45+
stateKey="required-scopes"
46+
defaultExpandedKeys={['required-scopes']}
47+
groups={[
48+
{
49+
key: 'required-scopes',
50+
label: (
51+
<div className="openapi-required-scopes-header">
52+
{context.icons.lock}
53+
<span>{t(context.translation, 'required_scopes')}</span>
54+
</div>
55+
),
56+
tabs: [
57+
{
58+
key: 'scopes',
59+
label: '',
60+
body: <OpenAPISchemaScopes scopes={scopes} context={context} />,
61+
},
62+
],
63+
},
64+
]}
65+
/>
5666
);
5767
}
5868

@@ -64,6 +74,9 @@ function OpenAPISchemaScopes(props: {
6474

6575
return (
6676
<div className="openapi-securities-scopes openapi-markdown">
77+
<div className="openapi-required-scopes-description">
78+
This endpoint requires the following scopes:
79+
</div>
6780
<ul>
6881
{scopes.map((scope) => (
6982
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Key } from 'react-stately';
2+
import { createStore } from 'zustand';
3+
4+
type DisclosureState = {
5+
expandedKeys: Set<Key>;
6+
};
7+
8+
type DisclosureActions = {
9+
setExpandedKeys: (keys: Set<Key>) => void;
10+
toggleKey: (key: Key) => void;
11+
};
12+
13+
export type DisclosureStore = DisclosureState & DisclosureActions;
14+
15+
const createDisclosureStore = (initialKeys?: Iterable<Key>) => {
16+
return createStore<DisclosureStore>()((set) => ({
17+
expandedKeys: initialKeys ? new Set(initialKeys) : new Set(),
18+
setExpandedKeys: (keys) => {
19+
set(() => ({ expandedKeys: keys }));
20+
},
21+
toggleKey: (key) => {
22+
set((state) => {
23+
const newKeys = new Set(state.expandedKeys);
24+
if (newKeys.has(key)) {
25+
newKeys.delete(key);
26+
} else {
27+
newKeys.add(key);
28+
}
29+
return { expandedKeys: newKeys };
30+
});
31+
},
32+
}));
33+
};
34+
35+
const defaultDisclosureStores = new Map<string, ReturnType<typeof createDisclosureStore>>();
36+
37+
const createDisclosureStoreFactory = (stores: typeof defaultDisclosureStores) => {
38+
return (storeKey: string, initialKeys?: Iterable<Key>) => {
39+
if (!stores.has(storeKey)) {
40+
stores.set(storeKey, createDisclosureStore(initialKeys));
41+
}
42+
const store = stores.get(storeKey);
43+
if (!store) {
44+
throw new Error(`Failed to get or create store for key: ${storeKey}`);
45+
}
46+
return store;
47+
};
48+
};
49+
50+
export const getOrCreateDisclosureStoreByKey =
51+
createDisclosureStoreFactory(defaultDisclosureStores);

0 commit comments

Comments
 (0)