@ankhorage/surface 4.4.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/features/bottom-sheet/BottomSheetProvider.js +8 -6
- package/dist/features/bottom-sheet/BottomSheetProvider.js.map +1 -1
- package/dist/features/bottom-sheet/public.d.ts +2 -1
- package/dist/features/bottom-sheet/public.d.ts.map +1 -1
- package/dist/features/bottom-sheet/public.js +1 -0
- package/dist/features/bottom-sheet/public.js.map +1 -1
- package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts +5 -0
- package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts.map +1 -0
- package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js +85 -0
- package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js.map +1 -0
- package/dist/features/popover-menu/composition/usePopoverMenuController.d.ts +19 -0
- package/dist/features/popover-menu/composition/usePopoverMenuController.d.ts.map +1 -0
- package/dist/features/popover-menu/composition/usePopoverMenuController.js +35 -0
- package/dist/features/popover-menu/composition/usePopoverMenuController.js.map +1 -0
- package/dist/features/popover-menu/public.d.ts +3 -0
- package/dist/features/popover-menu/public.d.ts.map +1 -0
- package/dist/features/popover-menu/public.js +2 -0
- package/dist/features/popover-menu/public.js.map +1 -0
- package/dist/features/popover-menu/utils/resolveNextMenuIndex.d.ts +5 -0
- package/dist/features/popover-menu/utils/resolveNextMenuIndex.d.ts.map +1 -0
- package/dist/features/popover-menu/utils/resolveNextMenuIndex.js +18 -0
- package/dist/features/popover-menu/utils/resolveNextMenuIndex.js.map +1 -0
- package/dist/features/tooltip/adapters/inbound/Tooltip.d.ts +5 -0
- package/dist/features/tooltip/adapters/inbound/Tooltip.d.ts.map +1 -0
- package/dist/features/tooltip/adapters/inbound/Tooltip.js +25 -0
- package/dist/features/tooltip/adapters/inbound/Tooltip.js.map +1 -0
- package/dist/features/tooltip/composition/useTooltipVisibility.d.ts +9 -0
- package/dist/features/tooltip/composition/useTooltipVisibility.d.ts.map +1 -0
- package/dist/features/tooltip/composition/useTooltipVisibility.js +25 -0
- package/dist/features/tooltip/composition/useTooltipVisibility.js.map +1 -0
- package/dist/features/tooltip/public.d.ts +3 -0
- package/dist/features/tooltip/public.d.ts.map +1 -0
- package/dist/features/tooltip/public.js +2 -0
- package/dist/features/tooltip/public.js.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/bottomSheet.d.ts +3 -0
- package/dist/types/bottomSheet.d.ts.map +1 -1
- package/dist/types/bottomSheet.js.map +1 -1
- package/dist/types/popoverMenu.d.ts +24 -0
- package/dist/types/popoverMenu.d.ts.map +1 -0
- package/dist/types/popoverMenu.js +2 -0
- package/dist/types/popoverMenu.js.map +1 -0
- package/dist/{components/tooltip/types.d.ts → types/tooltip.d.ts} +2 -2
- package/dist/types/tooltip.d.ts.map +1 -0
- package/dist/types/tooltip.js +2 -0
- package/dist/types/tooltip.js.map +1 -0
- package/package.json +1 -1
- package/src/features/bottom-sheet/BottomSheetProvider.tsx +13 -8
- package/src/features/bottom-sheet/public.test.ts +23 -0
- package/src/features/bottom-sheet/public.ts +12 -1
- package/src/features/popover-menu/adapters/inbound/PopoverMenu.tsx +201 -0
- package/src/features/popover-menu/composition/usePopoverMenuController.ts +55 -0
- package/src/features/popover-menu/public.test.ts +13 -0
- package/src/features/popover-menu/public.ts +6 -0
- package/src/{components/menu/navigation.test.ts → features/popover-menu/utils/resolveNextMenuIndex.test.ts} +4 -4
- package/src/features/popover-menu/utils/resolveNextMenuIndex.ts +21 -0
- package/src/features/tooltip/adapters/inbound/Tooltip.tsx +56 -0
- package/src/features/tooltip/composition/useTooltipVisibility.ts +29 -0
- package/src/features/tooltip/public.test.ts +11 -0
- package/src/features/tooltip/public.ts +2 -0
- package/src/index.test.ts +8 -3
- package/src/index.ts +8 -4
- package/src/types/bottomSheet.ts +4 -0
- package/src/types/popoverMenu.ts +27 -0
- package/src/{components/tooltip/types.ts → types/tooltip.ts} +1 -1
- package/dist/components/menu/Menu.d.ts +0 -4
- package/dist/components/menu/Menu.d.ts.map +0 -1
- package/dist/components/menu/Menu.js +0 -152
- package/dist/components/menu/Menu.js.map +0 -1
- package/dist/components/menu/index.d.ts +0 -3
- package/dist/components/menu/index.d.ts.map +0 -1
- package/dist/components/menu/index.js +0 -2
- package/dist/components/menu/index.js.map +0 -1
- package/dist/components/menu/navigation.d.ts +0 -4
- package/dist/components/menu/navigation.d.ts.map +0 -1
- package/dist/components/menu/navigation.js +0 -25
- package/dist/components/menu/navigation.js.map +0 -1
- package/dist/components/menu/types.d.ts +0 -23
- package/dist/components/menu/types.d.ts.map +0 -1
- package/dist/components/menu/types.js +0 -2
- package/dist/components/menu/types.js.map +0 -1
- package/dist/components/tooltip/Tooltip.d.ts +0 -4
- package/dist/components/tooltip/Tooltip.d.ts.map +0 -1
- package/dist/components/tooltip/Tooltip.js +0 -75
- package/dist/components/tooltip/Tooltip.js.map +0 -1
- package/dist/components/tooltip/index.d.ts +0 -3
- package/dist/components/tooltip/index.d.ts.map +0 -1
- package/dist/components/tooltip/index.js +0 -2
- package/dist/components/tooltip/index.js.map +0 -1
- package/dist/components/tooltip/types.d.ts.map +0 -1
- package/dist/components/tooltip/types.js +0 -2
- package/dist/components/tooltip/types.js.map +0 -1
- package/src/components/menu/Menu.tsx +0 -228
- package/src/components/menu/index.ts +0 -2
- package/src/components/menu/navigation.ts +0 -34
- package/src/components/menu/types.ts +0 -26
- package/src/components/tooltip/Tooltip.tsx +0 -117
- package/src/components/tooltip/index.ts +0 -2
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
+
export type BottomSheetContentMode = 'view' | 'direct';
|
|
2
3
|
export interface BottomSheetPresentOptions {
|
|
3
4
|
content: React.ReactNode;
|
|
5
|
+
contentMode?: BottomSheetContentMode;
|
|
4
6
|
snapPoints?: readonly (number | string)[];
|
|
5
7
|
initialIndex?: number;
|
|
6
8
|
enableDynamicSizing?: boolean;
|
|
9
|
+
maxDynamicContentSize?: number;
|
|
7
10
|
enablePanDownToClose?: boolean;
|
|
8
11
|
dismissOnBackdropPress?: boolean;
|
|
9
12
|
keyboardBehavior?: 'extend' | 'fillParent' | 'interactive';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bottomSheet.d.ts","sourceRoot":"","sources":["../../src/types/bottomSheet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,UAAU,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,aAAa,CAAC;IAC3D,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAC;IACtD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB"}
|
|
1
|
+
{"version":3,"file":"bottomSheet.d.ts","sourceRoot":"","sources":["../../src/types/bottomSheet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,UAAU,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,aAAa,CAAC;IAC3D,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,CAAC;IACtD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bottomSheet.js","sourceRoot":"","sources":["../../src/types/bottomSheet.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\nexport interface BottomSheetPresentOptions {\n content: React.ReactNode;\n snapPoints?: readonly (number | string)[];\n initialIndex?: number;\n enableDynamicSizing?: boolean;\n enablePanDownToClose?: boolean;\n dismissOnBackdropPress?: boolean;\n keyboardBehavior?: 'extend' | 'fillParent' | 'interactive';\n keyboardBlurBehavior?: 'none' | 'restore';\n onIndexChange?: (index: number) => void;\n onDismiss?: () => void;\n}\n\nexport interface BottomSheetController {\n present: (options: BottomSheetPresentOptions) => void;\n dismiss: () => void;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bottomSheet.js","sourceRoot":"","sources":["../../src/types/bottomSheet.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\nexport type BottomSheetContentMode = 'view' | 'direct';\n\nexport interface BottomSheetPresentOptions {\n content: React.ReactNode;\n contentMode?: BottomSheetContentMode;\n snapPoints?: readonly (number | string)[];\n initialIndex?: number;\n enableDynamicSizing?: boolean;\n maxDynamicContentSize?: number;\n enablePanDownToClose?: boolean;\n dismissOnBackdropPress?: boolean;\n keyboardBehavior?: 'extend' | 'fillParent' | 'interactive';\n keyboardBlurBehavior?: 'none' | 'restore';\n onIndexChange?: (index: number) => void;\n onDismiss?: () => void;\n}\n\nexport interface BottomSheetController {\n present: (options: BottomSheetPresentOptions) => void;\n dismiss: () => void;\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { InteractionPolicy } from '../interactionPolicy';
|
|
3
|
+
import type { PopoverAnchorRenderProps } from './popover';
|
|
4
|
+
export type PopoverMenuActionIntent = 'default' | 'danger';
|
|
5
|
+
export interface PopoverMenuAction {
|
|
6
|
+
id: string;
|
|
7
|
+
title: React.ReactNode;
|
|
8
|
+
description?: React.ReactNode;
|
|
9
|
+
leading?: React.ReactNode;
|
|
10
|
+
trailing?: React.ReactNode;
|
|
11
|
+
intent?: PopoverMenuActionIntent;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
activate?: () => void;
|
|
15
|
+
}
|
|
16
|
+
export interface PopoverMenuProps {
|
|
17
|
+
trigger: (controls: PopoverAnchorRenderProps) => React.ReactNode;
|
|
18
|
+
actions: readonly PopoverMenuAction[];
|
|
19
|
+
dismiss?: () => void;
|
|
20
|
+
closeOnSelect?: boolean;
|
|
21
|
+
interactionPolicy?: InteractionPolicy;
|
|
22
|
+
testID?: string;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=popoverMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popoverMenu.d.ts","sourceRoot":"","sources":["../../src/types/popoverMenu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAE1D,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,KAAK,CAAC,SAAS,CAAC;IACjE,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"popoverMenu.js","sourceRoot":"","sources":["../../src/types/popoverMenu.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\nimport type { InteractionPolicy } from '../interactionPolicy';\nimport type { PopoverAnchorRenderProps } from './popover';\n\nexport type PopoverMenuActionIntent = 'default' | 'danger';\n\nexport interface PopoverMenuAction {\n id: string;\n title: React.ReactNode;\n description?: React.ReactNode;\n leading?: React.ReactNode;\n trailing?: React.ReactNode;\n intent?: PopoverMenuActionIntent;\n disabled?: boolean;\n selected?: boolean;\n activate?: () => void;\n}\n\nexport interface PopoverMenuProps {\n trigger: (controls: PopoverAnchorRenderProps) => React.ReactNode;\n actions: readonly PopoverMenuAction[];\n dismiss?: () => void;\n closeOnSelect?: boolean;\n interactionPolicy?: InteractionPolicy;\n testID?: string;\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import type { InteractionPolicy } from '
|
|
2
|
+
import type { InteractionPolicy } from '../interactionPolicy';
|
|
3
3
|
export interface TooltipProps {
|
|
4
4
|
children?: React.ReactNode;
|
|
5
5
|
content?: React.ReactNode;
|
|
@@ -8,4 +8,4 @@ export interface TooltipProps {
|
|
|
8
8
|
placement?: 'top' | 'bottom';
|
|
9
9
|
testID?: string;
|
|
10
10
|
}
|
|
11
|
-
//# sourceMappingURL=
|
|
11
|
+
//# sourceMappingURL=tooltip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip.d.ts","sourceRoot":"","sources":["../../src/types/tooltip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip.js","sourceRoot":"","sources":["../../src/types/tooltip.ts"],"names":[],"mappings":"","sourcesContent":["import type React from 'react';\n\nimport type { InteractionPolicy } from '../interactionPolicy';\n\nexport interface TooltipProps {\n children?: React.ReactNode;\n content?: React.ReactNode;\n delay?: number;\n interactionPolicy?: InteractionPolicy;\n placement?: 'top' | 'bottom';\n testID?: string;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -77,10 +77,7 @@ function BottomSheetHost({
|
|
|
77
77
|
presentedRef,
|
|
78
78
|
}: BottomSheetHostProps) {
|
|
79
79
|
React.useEffect(() => {
|
|
80
|
-
if (!activeRequest || presentedRef.current)
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
80
|
+
if (!activeRequest || presentedRef.current) return;
|
|
84
81
|
presentedRef.current = true;
|
|
85
82
|
modalRef.current?.present();
|
|
86
83
|
}, [activeRequest, modalRef, presentedRef]);
|
|
@@ -91,9 +88,7 @@ function BottomSheetHost({
|
|
|
91
88
|
[activeRequest?.snapPoints],
|
|
92
89
|
);
|
|
93
90
|
|
|
94
|
-
if (!activeRequest)
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
91
|
+
if (!activeRequest) return null;
|
|
97
92
|
|
|
98
93
|
return (
|
|
99
94
|
<BottomSheetModal
|
|
@@ -104,15 +99,25 @@ function BottomSheetHost({
|
|
|
104
99
|
index={activeRequest.initialIndex ?? 0}
|
|
105
100
|
keyboardBehavior={activeRequest.keyboardBehavior ?? 'interactive'}
|
|
106
101
|
keyboardBlurBehavior={activeRequest.keyboardBlurBehavior ?? 'restore'}
|
|
102
|
+
maxDynamicContentSize={activeRequest.maxDynamicContentSize}
|
|
107
103
|
onChange={activeRequest.onIndexChange}
|
|
108
104
|
onDismiss={handleDismiss}
|
|
109
105
|
snapPoints={snapPoints}
|
|
110
106
|
>
|
|
111
|
-
|
|
107
|
+
{renderBottomSheetContent(activeRequest)}
|
|
112
108
|
</BottomSheetModal>
|
|
113
109
|
);
|
|
114
110
|
}
|
|
115
111
|
|
|
112
|
+
/*** Renders default content in BottomSheetView and direct content without an extra wrapper. */
|
|
113
|
+
function renderBottomSheetContent(activeRequest: BottomSheetPresentOptions): React.ReactNode {
|
|
114
|
+
return activeRequest.contentMode === 'direct' ? (
|
|
115
|
+
activeRequest.content
|
|
116
|
+
) : (
|
|
117
|
+
<BottomSheetView>{activeRequest.content}</BottomSheetView>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
116
121
|
/*** Creates the backdrop renderer for the active bottom-sheet request. */
|
|
117
122
|
function useBottomSheetBackdrop(dismissOnBackdropPress: boolean | undefined) {
|
|
118
123
|
return React.useCallback(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('bottom-sheet boundary exposes Gorhom integrated scrollables and direct content mode', async () => {
|
|
4
|
+
const [publicSource, providerSource, typeSource] = await Promise.all([
|
|
5
|
+
Bun.file('src/features/bottom-sheet/public.ts').text(),
|
|
6
|
+
Bun.file('src/features/bottom-sheet/BottomSheetProvider.tsx').text(),
|
|
7
|
+
Bun.file('src/types/bottomSheet.ts').text(),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
for (const name of [
|
|
11
|
+
'BottomSheetFlatList',
|
|
12
|
+
'BottomSheetScrollView',
|
|
13
|
+
'BottomSheetSectionList',
|
|
14
|
+
'BottomSheetView',
|
|
15
|
+
'BottomSheetVirtualizedList',
|
|
16
|
+
]) {
|
|
17
|
+
expect(publicSource).toContain(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
expect(typeSource).toContain("export type BottomSheetContentMode = 'view' | 'direct';");
|
|
21
|
+
expect(providerSource).toContain("activeRequest.contentMode === 'direct'");
|
|
22
|
+
expect(providerSource).toContain('maxDynamicContentSize={activeRequest.maxDynamicContentSize}');
|
|
23
|
+
});
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type {
|
|
2
|
+
BottomSheetContentMode,
|
|
3
|
+
BottomSheetController,
|
|
4
|
+
BottomSheetPresentOptions,
|
|
5
|
+
} from '../../types/bottomSheet';
|
|
2
6
|
export { BottomSheetProvider } from './BottomSheetProvider';
|
|
3
7
|
export { useBottomSheet } from './useBottomSheet';
|
|
8
|
+
export {
|
|
9
|
+
BottomSheetFlatList,
|
|
10
|
+
BottomSheetScrollView,
|
|
11
|
+
BottomSheetSectionList,
|
|
12
|
+
BottomSheetView,
|
|
13
|
+
BottomSheetVirtualizedList,
|
|
14
|
+
} from '@gorhom/bottom-sheet';
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { FocusScope } from '../../../../internal/focus/FocusScope';
|
|
5
|
+
import { useFocusManager } from '../../../../internal/focus/useFocusManager';
|
|
6
|
+
import { Inline } from '../../../../layout';
|
|
7
|
+
import { useTheme } from '../../../../theme/ThemeContext';
|
|
8
|
+
import type { PopoverMenuAction, PopoverMenuProps } from '../../../../types/popoverMenu';
|
|
9
|
+
import { Box, Stack } from '../../../layout/public';
|
|
10
|
+
import { Popover } from '../../../popover/public';
|
|
11
|
+
import { Surface } from '../../../surface/public';
|
|
12
|
+
import { Text } from '../../../typography/public';
|
|
13
|
+
import { usePopoverMenuController } from '../../composition/usePopoverMenuController';
|
|
14
|
+
import { resolveNextMenuIndex } from '../../utils/resolveNextMenuIndex';
|
|
15
|
+
|
|
16
|
+
/*** Presents an anchored action menu using the shared Popover capability. */
|
|
17
|
+
export function PopoverMenu({
|
|
18
|
+
trigger,
|
|
19
|
+
actions,
|
|
20
|
+
dismiss,
|
|
21
|
+
closeOnSelect = true,
|
|
22
|
+
interactionPolicy = 'enabled',
|
|
23
|
+
testID,
|
|
24
|
+
}: PopoverMenuProps) {
|
|
25
|
+
const passive = interactionPolicy === 'passive';
|
|
26
|
+
const controller = usePopoverMenuController({ actions, closeOnSelect, dismiss, passive });
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Popover
|
|
30
|
+
anchor={trigger}
|
|
31
|
+
closeOnOutsidePress
|
|
32
|
+
interactionPolicy={interactionPolicy}
|
|
33
|
+
onOpenChange={controller.handleOpenChange}
|
|
34
|
+
open={controller.open}
|
|
35
|
+
placement="bottom-start"
|
|
36
|
+
testID={testID}
|
|
37
|
+
>
|
|
38
|
+
<PopoverMenuContent
|
|
39
|
+
actions={actions}
|
|
40
|
+
activeIndex={controller.activeIndex}
|
|
41
|
+
activateAction={controller.activateAction}
|
|
42
|
+
close={controller.close}
|
|
43
|
+
passive={passive}
|
|
44
|
+
setActiveIndex={controller.setActiveIndex}
|
|
45
|
+
testID={testID}
|
|
46
|
+
/>
|
|
47
|
+
</Popover>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface PopoverMenuContentProps {
|
|
52
|
+
actions: readonly PopoverMenuAction[];
|
|
53
|
+
activeIndex: number;
|
|
54
|
+
activateAction: (action: PopoverMenuAction) => void;
|
|
55
|
+
close: () => void;
|
|
56
|
+
passive: boolean;
|
|
57
|
+
setActiveIndex: React.Dispatch<React.SetStateAction<number>>;
|
|
58
|
+
testID?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*** Renders menu semantics and keyboard navigation inside an open Popover. */
|
|
62
|
+
function PopoverMenuContent({
|
|
63
|
+
actions,
|
|
64
|
+
activeIndex,
|
|
65
|
+
activateAction,
|
|
66
|
+
close,
|
|
67
|
+
passive,
|
|
68
|
+
setActiveIndex,
|
|
69
|
+
testID,
|
|
70
|
+
}: PopoverMenuContentProps) {
|
|
71
|
+
usePopoverMenuKeyboard({ actions, activeIndex, activateAction, close, passive, setActiveIndex });
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<FocusScope active onEscape={passive ? undefined : close}>
|
|
75
|
+
<Surface accessibilityRole="menu" p="xs" testID={testID} variant="raised">
|
|
76
|
+
{actions.map((action, index) => (
|
|
77
|
+
<PopoverMenuItem
|
|
78
|
+
action={action}
|
|
79
|
+
active={index === activeIndex}
|
|
80
|
+
activateAction={activateAction}
|
|
81
|
+
key={action.id}
|
|
82
|
+
passive={passive}
|
|
83
|
+
testID={testID}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</Surface>
|
|
87
|
+
</FocusScope>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*** Subscribes an open PopoverMenu to standard menu keyboard controls. */
|
|
92
|
+
function usePopoverMenuKeyboard({
|
|
93
|
+
actions,
|
|
94
|
+
activeIndex,
|
|
95
|
+
activateAction,
|
|
96
|
+
close,
|
|
97
|
+
passive,
|
|
98
|
+
setActiveIndex,
|
|
99
|
+
}: Omit<PopoverMenuContentProps, 'testID'>) {
|
|
100
|
+
const { bindKeydown } = useFocusManager();
|
|
101
|
+
|
|
102
|
+
React.useEffect(
|
|
103
|
+
() =>
|
|
104
|
+
bindKeydown((event) => {
|
|
105
|
+
if (passive) return;
|
|
106
|
+
|
|
107
|
+
const { key } = event;
|
|
108
|
+
if (key === undefined) return;
|
|
109
|
+
|
|
110
|
+
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key)) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
setActiveIndex((current) => resolveNextMenuIndex(actions, current, key));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (key === 'Enter') {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
const action = actions.at(activeIndex);
|
|
118
|
+
if (action) activateAction(action);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key === 'Escape') {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
close();
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
[actions, activeIndex, activateAction, bindKeydown, close, passive, setActiveIndex],
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/*** Renders one accessible PopoverMenu action row. */
|
|
131
|
+
function PopoverMenuItem({
|
|
132
|
+
action,
|
|
133
|
+
active,
|
|
134
|
+
activateAction,
|
|
135
|
+
passive,
|
|
136
|
+
testID,
|
|
137
|
+
}: {
|
|
138
|
+
action: PopoverMenuAction;
|
|
139
|
+
active: boolean;
|
|
140
|
+
activateAction: (action: PopoverMenuAction) => void;
|
|
141
|
+
passive: boolean;
|
|
142
|
+
testID?: string;
|
|
143
|
+
}) {
|
|
144
|
+
const { theme } = useTheme();
|
|
145
|
+
const selected = action.selected ?? active;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Pressable
|
|
149
|
+
accessibilityRole="menuitem"
|
|
150
|
+
accessibilityState={{ disabled: action.disabled, selected }}
|
|
151
|
+
disabled={action.disabled}
|
|
152
|
+
onPress={passive ? undefined : () => activateAction(action)}
|
|
153
|
+
testID={testID ? `${testID}-item-${action.id}` : undefined}
|
|
154
|
+
>
|
|
155
|
+
<Box
|
|
156
|
+
bg={selected ? theme.semantics.action.neutral.softBg : 'transparent'}
|
|
157
|
+
opacity={action.disabled ? 0.56 : 1}
|
|
158
|
+
px="m"
|
|
159
|
+
py="s"
|
|
160
|
+
radius="s"
|
|
161
|
+
>
|
|
162
|
+
<PopoverMenuItemContent action={action} active={active} />
|
|
163
|
+
</Box>
|
|
164
|
+
</Pressable>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*** Renders the leading, textual, and trailing content of one menu action. */
|
|
169
|
+
function PopoverMenuItemContent({
|
|
170
|
+
action,
|
|
171
|
+
active,
|
|
172
|
+
}: {
|
|
173
|
+
action: PopoverMenuAction;
|
|
174
|
+
active: boolean;
|
|
175
|
+
}) {
|
|
176
|
+
const titleColor =
|
|
177
|
+
action.intent === 'danger' ? 'danger' : active || action.selected ? 'neutral' : undefined;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Inline align="center" gap="s" justify="space-between" wrap="nowrap">
|
|
181
|
+
{action.leading ? <Box>{action.leading}</Box> : null}
|
|
182
|
+
<Box flex={1}>
|
|
183
|
+
<Stack gap="xxs">
|
|
184
|
+
<Text
|
|
185
|
+
color={titleColor}
|
|
186
|
+
variant="bodySmall"
|
|
187
|
+
weight={action.selected ? 'semiBold' : 'medium'}
|
|
188
|
+
>
|
|
189
|
+
{action.title}
|
|
190
|
+
</Text>
|
|
191
|
+
{action.description ? (
|
|
192
|
+
<Text emphasis="muted" variant="caption">
|
|
193
|
+
{action.description}
|
|
194
|
+
</Text>
|
|
195
|
+
) : null}
|
|
196
|
+
</Stack>
|
|
197
|
+
</Box>
|
|
198
|
+
{action.trailing ? <Box>{action.trailing}</Box> : null}
|
|
199
|
+
</Inline>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { PopoverMenuAction } from '../../../types/popoverMenu';
|
|
4
|
+
|
|
5
|
+
interface PopoverMenuControllerOptions {
|
|
6
|
+
actions: readonly PopoverMenuAction[];
|
|
7
|
+
closeOnSelect: boolean;
|
|
8
|
+
dismiss?: () => void;
|
|
9
|
+
passive: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*** Owns PopoverMenu visibility, active action state, and selection behavior. */
|
|
13
|
+
export function usePopoverMenuController({
|
|
14
|
+
actions,
|
|
15
|
+
closeOnSelect,
|
|
16
|
+
dismiss,
|
|
17
|
+
passive,
|
|
18
|
+
}: PopoverMenuControllerOptions) {
|
|
19
|
+
const [open, setOpen] = React.useState(false);
|
|
20
|
+
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
21
|
+
|
|
22
|
+
const handleOpenChange = React.useCallback(
|
|
23
|
+
(nextOpen: boolean) => {
|
|
24
|
+
if (nextOpen) setActiveIndex(resolveInitialIndex(actions));
|
|
25
|
+
if (open && !nextOpen) dismiss?.();
|
|
26
|
+
setOpen(nextOpen);
|
|
27
|
+
},
|
|
28
|
+
[actions, dismiss, open],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const close = React.useCallback(() => handleOpenChange(false), [handleOpenChange]);
|
|
32
|
+
const activateAction = React.useCallback(
|
|
33
|
+
(action: PopoverMenuAction) => {
|
|
34
|
+
if (action.disabled || passive) return;
|
|
35
|
+
action.activate?.();
|
|
36
|
+
if (closeOnSelect) close();
|
|
37
|
+
},
|
|
38
|
+
[close, closeOnSelect, passive],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
activeIndex,
|
|
43
|
+
activateAction,
|
|
44
|
+
close,
|
|
45
|
+
handleOpenChange,
|
|
46
|
+
open,
|
|
47
|
+
setActiveIndex,
|
|
48
|
+
} as const;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*** Returns the first enabled action index for a newly opened menu. */
|
|
52
|
+
function resolveInitialIndex(actions: readonly PopoverMenuAction[]): number {
|
|
53
|
+
const index = actions.findIndex((action) => !action.disabled);
|
|
54
|
+
return index === -1 ? 0 : index;
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('PopoverMenu composes Popover instead of owning anchor and portal mechanics', async () => {
|
|
4
|
+
const source = await Bun.file(
|
|
5
|
+
'src/features/popover-menu/adapters/inbound/PopoverMenu.tsx',
|
|
6
|
+
).text();
|
|
7
|
+
|
|
8
|
+
expect(source).toContain("import { Popover } from '../../../popover/public';");
|
|
9
|
+
expect(source).toContain('<Popover');
|
|
10
|
+
expect(source).not.toContain('measureInWindow');
|
|
11
|
+
expect(source).not.toContain('internal/overlay');
|
|
12
|
+
expect(source).not.toContain('<Portal');
|
|
13
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { resolveNextMenuIndex } from './
|
|
3
|
+
import { resolveNextMenuIndex } from './resolveNextMenuIndex';
|
|
4
4
|
|
|
5
5
|
describe('resolveNextMenuIndex', () => {
|
|
6
|
-
const items = [{}, { disabled: true }, {}, {}];
|
|
6
|
+
const items = [{}, { disabled: true }, {}, {}] as const;
|
|
7
7
|
|
|
8
|
-
it('cycles down and up through
|
|
8
|
+
it('cycles down and up through enabled actions', () => {
|
|
9
9
|
expect(resolveNextMenuIndex(items, 0, 'ArrowDown')).toBe(2);
|
|
10
10
|
expect(resolveNextMenuIndex(items, 0, 'ArrowUp')).toBe(3);
|
|
11
11
|
});
|
|
@@ -15,7 +15,7 @@ describe('resolveNextMenuIndex', () => {
|
|
|
15
15
|
expect(resolveNextMenuIndex(items, 0, 'End')).toBe(3);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
it('returns -1 when all
|
|
18
|
+
it('returns -1 when all actions are disabled', () => {
|
|
19
19
|
expect(resolveNextMenuIndex([{ disabled: true }], 0, 'ArrowDown')).toBe(-1);
|
|
20
20
|
});
|
|
21
21
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*** Resolves the next enabled action index for standard menu keyboard navigation. */
|
|
2
|
+
export function resolveNextMenuIndex(
|
|
3
|
+
items: readonly { disabled?: boolean }[],
|
|
4
|
+
currentIndex: number,
|
|
5
|
+
key: string,
|
|
6
|
+
): number {
|
|
7
|
+
const enabledIndexes = items.flatMap((item, index) => (item.disabled ? [] : [index]));
|
|
8
|
+
if (enabledIndexes.length === 0) return -1;
|
|
9
|
+
if (key === 'Home') return enabledIndexes.at(0) ?? -1;
|
|
10
|
+
if (key === 'End') return enabledIndexes.at(-1) ?? -1;
|
|
11
|
+
|
|
12
|
+
const activeEnabledIndex = enabledIndexes.indexOf(currentIndex);
|
|
13
|
+
const nextEnabledIndex =
|
|
14
|
+
activeEnabledIndex === -1
|
|
15
|
+
? 0
|
|
16
|
+
: key === 'ArrowUp'
|
|
17
|
+
? (activeEnabledIndex - 1 + enabledIndexes.length) % enabledIndexes.length
|
|
18
|
+
: (activeEnabledIndex + 1) % enabledIndexes.length;
|
|
19
|
+
|
|
20
|
+
return enabledIndexes.at(nextEnabledIndex) ?? -1;
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../../../../theme/ThemeContext';
|
|
5
|
+
import type { TooltipProps } from '../../../../types/tooltip';
|
|
6
|
+
import { Popover } from '../../../popover/public';
|
|
7
|
+
import { Surface } from '../../../surface/public';
|
|
8
|
+
import { Text } from '../../../typography/public';
|
|
9
|
+
import { useTooltipVisibility } from '../../composition/useTooltipVisibility';
|
|
10
|
+
|
|
11
|
+
/*** Presents delayed hover/focus help through the shared Popover foundation. */
|
|
12
|
+
export function Tooltip({
|
|
13
|
+
children,
|
|
14
|
+
content,
|
|
15
|
+
delay = 150,
|
|
16
|
+
interactionPolicy = 'enabled',
|
|
17
|
+
placement = 'top',
|
|
18
|
+
testID,
|
|
19
|
+
}: TooltipProps) {
|
|
20
|
+
const { theme } = useTheme();
|
|
21
|
+
const passive = interactionPolicy === 'passive';
|
|
22
|
+
const { hide, setVisible, show, visible } = useTooltipVisibility(delay, passive);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Popover
|
|
26
|
+
closeOnOutsidePress={false}
|
|
27
|
+
interactionPolicy={interactionPolicy}
|
|
28
|
+
onOpenChange={setVisible}
|
|
29
|
+
open={visible}
|
|
30
|
+
placement={placement}
|
|
31
|
+
testID={testID}
|
|
32
|
+
anchor={() => (
|
|
33
|
+
<Pressable
|
|
34
|
+
onBlur={passive ? undefined : hide}
|
|
35
|
+
onFocus={passive ? undefined : show}
|
|
36
|
+
onHoverIn={Platform.OS === 'web' && !passive ? show : undefined}
|
|
37
|
+
onHoverOut={Platform.OS === 'web' && !passive ? hide : undefined}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</Pressable>
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<View pointerEvents="none">
|
|
44
|
+
<Surface
|
|
45
|
+
p="s"
|
|
46
|
+
style={{ backgroundColor: theme.semantics.surface.inverse }}
|
|
47
|
+
variant="raised"
|
|
48
|
+
>
|
|
49
|
+
<Text emphasis="inverse" variant="caption">
|
|
50
|
+
{content}
|
|
51
|
+
</Text>
|
|
52
|
+
</Surface>
|
|
53
|
+
</View>
|
|
54
|
+
</Popover>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/*** Manages delayed tooltip visibility and timer cleanup. */
|
|
4
|
+
export function useTooltipVisibility(delay: number, passive: boolean) {
|
|
5
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
6
|
+
const [visible, setVisible] = React.useState(false);
|
|
7
|
+
|
|
8
|
+
const clearTimer = React.useCallback(() => {
|
|
9
|
+
if (timeoutRef.current) {
|
|
10
|
+
clearTimeout(timeoutRef.current);
|
|
11
|
+
timeoutRef.current = null;
|
|
12
|
+
}
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const show = React.useCallback(() => {
|
|
16
|
+
if (passive) return;
|
|
17
|
+
clearTimer();
|
|
18
|
+
timeoutRef.current = setTimeout(() => setVisible(true), delay);
|
|
19
|
+
}, [clearTimer, delay, passive]);
|
|
20
|
+
|
|
21
|
+
const hide = React.useCallback(() => {
|
|
22
|
+
clearTimer();
|
|
23
|
+
setVisible(false);
|
|
24
|
+
}, [clearTimer]);
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => clearTimer, [clearTimer]);
|
|
27
|
+
|
|
28
|
+
return { hide, setVisible, show, visible } as const;
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
test('Tooltip delegates anchored positioning and portal ownership to Popover', async () => {
|
|
4
|
+
const source = await Bun.file('src/features/tooltip/adapters/inbound/Tooltip.tsx').text();
|
|
5
|
+
|
|
6
|
+
expect(source).toContain("import { Popover } from '../../../popover/public';");
|
|
7
|
+
expect(source).toContain('<Popover');
|
|
8
|
+
expect(source).not.toContain('measureInWindow');
|
|
9
|
+
expect(source).not.toContain('internal/overlay');
|
|
10
|
+
expect(source).not.toContain('<Portal');
|
|
11
|
+
});
|