Skip to content

Commit 909b422

Browse files
committed
refactor: introduce createContextNamespace; drop useAssertedContext
1 parent c9d22cc commit 909b422

11 files changed

Lines changed: 98 additions & 130 deletions

src/components/PopoverMenu/v2/content/ContentContext.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import {createContext} from 'react';
21
import type {RefObject} from 'react';
32
import type {View} from 'react-native';
4-
import useAssertedContext from '@hooks/useAssertedContext';
3+
import createContextNamespace from '@hooks/createContextNamespace';
54

65
type FocusableItem = {
76
ref: RefObject<View | null>;
@@ -38,28 +37,13 @@ type ContentItemActions = {
3837
/** Closes and resets sub-navigation + focus state. */
3938
type ContentClose = () => void;
4039

41-
const PARENT = '<PopoverMenu.Content>';
40+
const createContentContext = createContextNamespace('PopoverMenu.Content');
4241

43-
const ContentNavigationContext = createContext<ContentNavigation | null>(null);
44-
ContentNavigationContext.displayName = 'PopoverMenuContentNavigationContext';
45-
46-
const ContentFocusContext = createContext<ContentFocus | null>(null);
47-
ContentFocusContext.displayName = 'PopoverMenuContentFocusContext';
48-
49-
const ContentSubActionsContext = createContext<ContentSubActions | null>(null);
50-
ContentSubActionsContext.displayName = 'PopoverMenuContentSubActionsContext';
51-
52-
const ContentItemActionsContext = createContext<ContentItemActions | null>(null);
53-
ContentItemActionsContext.displayName = 'PopoverMenuContentItemActionsContext';
54-
55-
const ContentCloseContext = createContext<ContentClose | null>(null);
56-
ContentCloseContext.displayName = 'PopoverMenuContentCloseContext';
57-
58-
const useContentNavigation = (consumerName: string) => useAssertedContext(ContentNavigationContext, consumerName, PARENT);
59-
const useContentFocus = (consumerName: string) => useAssertedContext(ContentFocusContext, consumerName, PARENT);
60-
const useContentSubActions = (consumerName: string) => useAssertedContext(ContentSubActionsContext, consumerName, PARENT);
61-
const useContentItemActions = (consumerName: string) => useAssertedContext(ContentItemActionsContext, consumerName, PARENT);
62-
const useContentClose = (consumerName: string) => useAssertedContext(ContentCloseContext, consumerName, PARENT);
42+
const [ContentNavigationContext, useContentNavigation] = createContentContext<ContentNavigation>('Navigation');
43+
const [ContentFocusContext, useContentFocus] = createContentContext<ContentFocus>('Focus');
44+
const [ContentSubActionsContext, useContentSubActions] = createContentContext<ContentSubActions>('SubActions');
45+
const [ContentItemActionsContext, useContentItemActions] = createContentContext<ContentItemActions>('ItemActions');
46+
const [ContentCloseContext, useContentClose] = createContentContext<ContentClose>('Close');
6347

6448
export {
6549
ContentNavigationContext,
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import useAssertedContext from '@hooks/useAssertedContext';
2-
import {ContentCloseContext} from './ContentContext';
1+
import {useContentClose} from './ContentContext';
32

43
/** Programmatic close escape hatch. */
54
function useClosePopover(): () => void {
6-
return useAssertedContext(ContentCloseContext, 'useClosePopover', '<PopoverMenu.Content>');
5+
return useContentClose('useClosePopover');
76
}
87

98
export default useClosePopover;

src/components/PopoverMenu/v2/root/RootContext.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import {createContext} from 'react';
21
import type {Dispatch, RefObject, SetStateAction} from 'react';
32
import type {View} from 'react-native';
4-
import useAssertedContext from '@hooks/useAssertedContext';
3+
import createContextNamespace from '@hooks/createContextNamespace';
54

65
type AnchorRef = RefObject<View | null>;
76

@@ -28,20 +27,11 @@ type RootActions = {
2827
setActiveAnchor: (anchor: ActiveAnchor) => void;
2928
};
3029

31-
const PARENT = '<PopoverMenu.Root>';
30+
const createRootContext = createContextNamespace('PopoverMenu.Root');
3231

33-
const RootVisibilityContext = createContext<RootVisibility | null>(null);
34-
RootVisibilityContext.displayName = 'PopoverMenuRootVisibilityContext';
35-
36-
const RootMetaContext = createContext<RootMeta | null>(null);
37-
RootMetaContext.displayName = 'PopoverMenuRootMetaContext';
38-
39-
const RootActionsContext = createContext<RootActions | null>(null);
40-
RootActionsContext.displayName = 'PopoverMenuRootActionsContext';
41-
42-
const useRootVisibility = (consumerName: string) => useAssertedContext(RootVisibilityContext, consumerName, PARENT);
43-
const useRootMeta = (consumerName: string) => useAssertedContext(RootMetaContext, consumerName, PARENT);
44-
const useRootActions = (consumerName: string) => useAssertedContext(RootActionsContext, consumerName, PARENT);
32+
const [RootVisibilityContext, useRootVisibility] = createRootContext<RootVisibility>('Visibility');
33+
const [RootMetaContext, useRootMeta] = createRootContext<RootMeta>('Meta');
34+
const [RootActionsContext, useRootActions] = createRootContext<RootActions>('Actions');
4535

4636
export {RootVisibilityContext, RootMetaContext, RootActionsContext, useRootVisibility, useRootMeta, useRootActions};
4737
export type {ActiveAnchor, AnchorRect, AnchorRef, RootVisibility, RootMeta, RootActions};

src/components/PopoverMenu/v2/root/useAnchorOpener.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {useRef} from 'react';
22
import type {View} from 'react-native';
3-
import useAssertedContext from '@hooks/useAssertedContext';
4-
import {RootActionsContext} from './RootContext';
3+
import {useRootActions} from './RootContext';
54
import type {AnchorRect, AnchorRef} from './RootContext';
65

76
type UseAnchorOpenerResult = {
@@ -10,7 +9,7 @@ type UseAnchorOpenerResult = {
109
};
1110

1211
function useAnchorOpener(callerName: string): UseAnchorOpenerResult {
13-
const {setIsVisible, setActiveAnchor} = useAssertedContext(RootActionsContext, callerName, '<PopoverMenu.Root>');
12+
const {setIsVisible, setActiveAnchor} = useRootActions(callerName);
1413
const ownRef: AnchorRef = useRef<View | null>(null);
1514

1615
const open = (overrideRect?: AnchorRect) => {
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import useAssertedContext from '@hooks/useAssertedContext';
2-
import {RootVisibilityContext} from './RootContext';
1+
import {useRootVisibility} from './RootContext';
32

43
function useIsPopoverVisible(): boolean {
5-
return useAssertedContext(RootVisibilityContext, 'useIsPopoverVisible', '<PopoverMenu.Root>').isVisible;
4+
return useRootVisibility('useIsPopoverVisible').isVisible;
65
}
76

87
export default useIsPopoverVisible;

src/components/PopoverMenu/v2/rows/useSelectableRow.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type {RefObject} from 'react';
22
import type {View} from 'react-native';
3-
import {ContentCloseContext} from '@components/PopoverMenu/v2/content/ContentContext';
3+
import {useContentClose} from '@components/PopoverMenu/v2/content/ContentContext';
44
import {useIsAtActiveLevel} from '@components/PopoverMenu/v2/sub/SubContext';
5-
import useAssertedContext from '@hooks/useAssertedContext';
65
import {createCustomEvent} from '@libs/CustomEventUtils';
76
import type {CustomEvent} from '@libs/CustomEventUtils';
87
import useFocusableRow from './useFocusableRow';
@@ -21,7 +20,7 @@ type UseSelectableRowResult = {
2120

2221
/** Closes after `onSelect`; call `event.preventDefault()` inside `onSelect` to keep the menu open. */
2322
function useSelectableRow({onSelect, disabled = false, text}: {onSelect?: (event: ItemSelectEvent) => void; disabled?: boolean; text?: string} = {}): UseSelectableRowResult {
24-
const close = useAssertedContext(ContentCloseContext, HOOK_NAME, '<PopoverMenu.Content>');
23+
const close = useContentClose(HOOK_NAME);
2524
const isAtActiveLevel = useIsAtActiveLevel(HOOK_NAME);
2625

2726
const row = useFocusableRow({

src/components/PopoverMenu/v2/sub/SubContext.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import {createContext, use} from 'react';
1+
import {use} from 'react';
22
import {useContentNavigation} from '@components/PopoverMenu/v2/content/ContentContext';
3-
import useAssertedContext from '@hooks/useAssertedContext';
3+
import createContextNamespace from '@hooks/createContextNamespace';
44

55
type SubContextValue = {
66
subID: string;
77
/** `null` at the outermost level. */
88
parentSubID: string | null;
99
};
1010

11-
const SubContext = createContext<SubContextValue | null>(null);
12-
SubContext.displayName = 'PopoverMenuSubContext';
13-
14-
const useSubContext = (consumerName: string) => useAssertedContext(SubContext, consumerName, '<PopoverMenu.Sub>');
11+
const [SubContext, useSubContext] = createContextNamespace('PopoverMenu.Sub')<SubContextValue>();
1512

1613
const useSubContextOptional = (): SubContextValue | null => use(SubContext);
1714

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {createContext, use} from 'react';
2+
3+
function createContextNamespace(parentName: string) {
4+
return function createStrict<T>(localName?: string) {
5+
const Ctx = createContext<T | null>(null);
6+
Ctx.displayName = localName ? `${parentName}.${localName}` : parentName;
7+
8+
function useStrictContext(consumerName: string): T {
9+
const value = use(Ctx);
10+
if (value === null) {
11+
throw new Error(`${consumerName} must be used inside <${parentName}>.`);
12+
}
13+
return value;
14+
}
15+
16+
return [Ctx, useStrictContext] as const;
17+
};
18+
}
19+
20+
export default createContextNamespace;

src/hooks/useAssertedContext.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

tests/unit/PopoverMenuV2Test.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2151,7 +2151,7 @@ describe('PopoverMenu V2', () => {
21512151
</PressableWithFeedback>
21522152
</PopoverMenu.Trigger>,
21532153
),
2154-
).toThrow(/<PopoverMenu\.Trigger> must be rendered inside <PopoverMenu\.Root>/);
2154+
).toThrow(/PopoverMenu\.Trigger must be used inside <PopoverMenu\.Root>/);
21552155
});
21562156

21572157
it('throws when SecondaryInteractionTrigger is rendered outside Root', () => {
@@ -2166,15 +2166,15 @@ describe('PopoverMenu V2', () => {
21662166
</PressableWithSecondaryInteraction>
21672167
</PopoverMenu.SecondaryInteractionTrigger>,
21682168
),
2169-
).toThrow(/<PopoverMenu\.SecondaryInteractionTrigger> must be rendered inside <PopoverMenu\.Root>/);
2169+
).toThrow(/PopoverMenu\.SecondaryInteractionTrigger must be used inside <PopoverMenu\.Root>/);
21702170
});
21712171

21722172
it('throws when useIsPopoverVisible is called outside Root', () => {
21732173
function CallVisibilityHook() {
21742174
PopoverMenu.useIsPopoverVisible();
21752175
return null;
21762176
}
2177-
expect(() => render(<CallVisibilityHook />)).toThrow(/useIsPopoverVisible\(\) must be called inside <PopoverMenu\.Root>/);
2177+
expect(() => render(<CallVisibilityHook />)).toThrow(/useIsPopoverVisible must be used inside <PopoverMenu\.Root>/);
21782178
});
21792179

21802180
it('throws when Content is rendered outside Root', () => {
@@ -2187,7 +2187,7 @@ describe('PopoverMenu V2', () => {
21872187
/>
21882188
</PopoverMenu.Content>,
21892189
),
2190-
).toThrow(/<PopoverMenu\.Content> must be rendered inside <PopoverMenu\.Root>/);
2190+
).toThrow(/PopoverMenu\.Content must be used inside <PopoverMenu\.Root>/);
21912191
});
21922192

21932193
it('throws when Item is rendered outside Content', () => {
@@ -2200,7 +2200,7 @@ describe('PopoverMenu V2', () => {
22002200
/>
22012201
</Harness>,
22022202
),
2203-
).toThrow(/<PopoverMenu\.Item> must be rendered inside <PopoverMenu\.Content>/);
2203+
).toThrow(/PopoverMenu\.Item must be used inside <PopoverMenu\.Content>/);
22042204
});
22052205

22062206
it('throws when Group is rendered outside Content', () => {
@@ -2212,7 +2212,7 @@ describe('PopoverMenu V2', () => {
22122212
</PopoverMenu.Group>
22132213
</Harness>,
22142214
),
2215-
).toThrow(/<PopoverMenu\.Group> must be rendered inside <PopoverMenu\.Content>/);
2215+
).toThrow(/PopoverMenu\.Group must be used inside <PopoverMenu\.Content>/);
22162216
});
22172217

22182218
it('throws when Sub is rendered outside Content', () => {
@@ -2224,7 +2224,7 @@ describe('PopoverMenu V2', () => {
22242224
</PopoverMenu.Sub>
22252225
</Harness>,
22262226
),
2227-
).toThrow(/<PopoverMenu\.Sub> must be rendered inside <PopoverMenu\.Content>/);
2227+
).toThrow(/PopoverMenu\.Sub must be used inside <PopoverMenu\.Content>/);
22282228
});
22292229

22302230
it('throws when Sub.Trigger is rendered outside Sub', () => {
@@ -2236,7 +2236,7 @@ describe('PopoverMenu V2', () => {
22362236
</PopoverMenu.Content>
22372237
</Harness>,
22382238
),
2239-
).toThrow(/<PopoverMenu\.Sub\.Trigger> must be rendered inside <PopoverMenu\.Sub>/);
2239+
).toThrow(/PopoverMenu\.Sub\.Trigger must be used inside <PopoverMenu\.Sub>/);
22402240
});
22412241

22422242
it('throws when useSelectableRow is called outside Content', () => {
@@ -2250,7 +2250,7 @@ describe('PopoverMenu V2', () => {
22502250
<CallSelectableRowHook />
22512251
</Harness>,
22522252
),
2253-
).toThrow(/useSelectableRow\(\) must be called inside <PopoverMenu\.Content>/);
2253+
).toThrow(/useSelectableRow must be used inside <PopoverMenu\.Content>/);
22542254
});
22552255

22562256
it('throws when useClosePopover is called outside Content', () => {
@@ -2264,7 +2264,7 @@ describe('PopoverMenu V2', () => {
22642264
<CallClosePopoverHook />
22652265
</Harness>,
22662266
),
2267-
).toThrow(/useClosePopover\(\) must be called inside <PopoverMenu\.Content>/);
2267+
).toThrow(/useClosePopover must be used inside <PopoverMenu\.Content>/);
22682268
});
22692269

22702270
it('throws when useSubBackButton is called outside Sub', () => {
@@ -2280,7 +2280,7 @@ describe('PopoverMenu V2', () => {
22802280
</PopoverMenu.Content>
22812281
</Harness>,
22822282
),
2283-
).toThrow(/useSubBackButton\(\) must be called inside <PopoverMenu\.Sub>/);
2283+
).toThrow(/useSubBackButton must be used inside <PopoverMenu\.Sub>/);
22842284
});
22852285

22862286
it('throws when useSubTrigger is called outside Sub', () => {
@@ -2296,7 +2296,7 @@ describe('PopoverMenu V2', () => {
22962296
</PopoverMenu.Content>
22972297
</Harness>,
22982298
),
2299-
).toThrow(/useSubTrigger\(\) must be called inside <PopoverMenu\.Sub>/);
2299+
).toThrow(/useSubTrigger must be used inside <PopoverMenu\.Sub>/);
23002300
});
23012301

23022302
it('throws when Sub.Content is rendered outside Sub', () => {
@@ -2313,7 +2313,7 @@ describe('PopoverMenu V2', () => {
23132313
</PopoverMenu.Content>
23142314
</Harness>,
23152315
),
2316-
).toThrow(/<PopoverMenu\.Sub\.Content> must be rendered inside <PopoverMenu\.Sub>/);
2316+
).toThrow(/PopoverMenu\.Sub\.Content must be used inside <PopoverMenu\.Sub>/);
23172317
});
23182318

23192319
it('throws when Header is rendered outside Content', () => {
@@ -2323,7 +2323,7 @@ describe('PopoverMenu V2', () => {
23232323
<PopoverMenu.Header>Title</PopoverMenu.Header>
23242324
</Harness>,
23252325
),
2326-
).toThrow(/<PopoverMenu\.Header> must be rendered inside <PopoverMenu\.Content>/);
2326+
).toThrow(/PopoverMenu\.Header must be used inside <PopoverMenu\.Content>/);
23272327
});
23282328

23292329
it('throws when Label is rendered outside Content', () => {
@@ -2333,7 +2333,7 @@ describe('PopoverMenu V2', () => {
23332333
<PopoverMenu.Label text="Label" />
23342334
</Harness>,
23352335
),
2336-
).toThrow(/<PopoverMenu\.Label> must be rendered inside <PopoverMenu\.Content>/);
2336+
).toThrow(/PopoverMenu\.Label must be used inside <PopoverMenu\.Content>/);
23372337
});
23382338

23392339
it('throws when RadioItem is rendered outside Content', () => {
@@ -2346,7 +2346,7 @@ describe('PopoverMenu V2', () => {
23462346
/>
23472347
</Harness>,
23482348
),
2349-
).toThrow(/<PopoverMenu\.RadioItem> must be rendered inside <PopoverMenu\.Content>/);
2349+
).toThrow(/PopoverMenu\.RadioItem must be used inside <PopoverMenu\.Content>/);
23502350
});
23512351

23522352
it('throws when Separator is rendered outside Content', () => {
@@ -2356,7 +2356,7 @@ describe('PopoverMenu V2', () => {
23562356
<PopoverMenu.Separator />
23572357
</Harness>,
23582358
),
2359-
).toThrow(/<PopoverMenu\.Separator> must be rendered inside <PopoverMenu\.Content>/);
2359+
).toThrow(/PopoverMenu\.Separator must be used inside <PopoverMenu\.Content>/);
23602360
});
23612361

23622362
it('throws when Sub.BackButton is rendered outside Sub', () => {
@@ -2368,7 +2368,7 @@ describe('PopoverMenu V2', () => {
23682368
</PopoverMenu.Content>
23692369
</Harness>,
23702370
),
2371-
).toThrow(/<PopoverMenu\.Sub\.BackButton> must be rendered inside <PopoverMenu\.Sub>/);
2371+
).toThrow(/PopoverMenu\.Sub\.BackButton must be used inside <PopoverMenu\.Sub>/);
23722372
});
23732373

23742374
it('throws when ScrollableContent is rendered outside Root', () => {
@@ -2381,7 +2381,7 @@ describe('PopoverMenu V2', () => {
23812381
/>
23822382
</PopoverMenu.ScrollableContent>,
23832383
),
2384-
).toThrow(/<PopoverMenu\.ScrollableContent> must be rendered inside <PopoverMenu\.Root>/);
2384+
).toThrow(/PopoverMenu\.ScrollableContent must be used inside <PopoverMenu\.Root>/);
23852385
});
23862386
});
23872387

0 commit comments

Comments
 (0)