@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/features/bottom-sheet/BottomSheetProvider.js +8 -6
  3. package/dist/features/bottom-sheet/BottomSheetProvider.js.map +1 -1
  4. package/dist/features/bottom-sheet/public.d.ts +2 -1
  5. package/dist/features/bottom-sheet/public.d.ts.map +1 -1
  6. package/dist/features/bottom-sheet/public.js +1 -0
  7. package/dist/features/bottom-sheet/public.js.map +1 -1
  8. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts +5 -0
  9. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.d.ts.map +1 -0
  10. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js +85 -0
  11. package/dist/features/popover-menu/adapters/inbound/PopoverMenu.js.map +1 -0
  12. package/dist/features/popover-menu/composition/usePopoverMenuController.d.ts +19 -0
  13. package/dist/features/popover-menu/composition/usePopoverMenuController.d.ts.map +1 -0
  14. package/dist/features/popover-menu/composition/usePopoverMenuController.js +35 -0
  15. package/dist/features/popover-menu/composition/usePopoverMenuController.js.map +1 -0
  16. package/dist/features/popover-menu/public.d.ts +3 -0
  17. package/dist/features/popover-menu/public.d.ts.map +1 -0
  18. package/dist/features/popover-menu/public.js +2 -0
  19. package/dist/features/popover-menu/public.js.map +1 -0
  20. package/dist/features/popover-menu/utils/resolveNextMenuIndex.d.ts +5 -0
  21. package/dist/features/popover-menu/utils/resolveNextMenuIndex.d.ts.map +1 -0
  22. package/dist/features/popover-menu/utils/resolveNextMenuIndex.js +18 -0
  23. package/dist/features/popover-menu/utils/resolveNextMenuIndex.js.map +1 -0
  24. package/dist/features/tooltip/adapters/inbound/Tooltip.d.ts +5 -0
  25. package/dist/features/tooltip/adapters/inbound/Tooltip.d.ts.map +1 -0
  26. package/dist/features/tooltip/adapters/inbound/Tooltip.js +25 -0
  27. package/dist/features/tooltip/adapters/inbound/Tooltip.js.map +1 -0
  28. package/dist/features/tooltip/composition/useTooltipVisibility.d.ts +9 -0
  29. package/dist/features/tooltip/composition/useTooltipVisibility.d.ts.map +1 -0
  30. package/dist/features/tooltip/composition/useTooltipVisibility.js +25 -0
  31. package/dist/features/tooltip/composition/useTooltipVisibility.js.map +1 -0
  32. package/dist/features/tooltip/public.d.ts +3 -0
  33. package/dist/features/tooltip/public.d.ts.map +1 -0
  34. package/dist/features/tooltip/public.js +2 -0
  35. package/dist/features/tooltip/public.js.map +1 -0
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -2
  39. package/dist/index.js.map +1 -1
  40. package/dist/types/bottomSheet.d.ts +3 -0
  41. package/dist/types/bottomSheet.d.ts.map +1 -1
  42. package/dist/types/bottomSheet.js.map +1 -1
  43. package/dist/types/popoverMenu.d.ts +24 -0
  44. package/dist/types/popoverMenu.d.ts.map +1 -0
  45. package/dist/types/popoverMenu.js +2 -0
  46. package/dist/types/popoverMenu.js.map +1 -0
  47. package/dist/{components/tooltip/types.d.ts → types/tooltip.d.ts} +2 -2
  48. package/dist/types/tooltip.d.ts.map +1 -0
  49. package/dist/types/tooltip.js +2 -0
  50. package/dist/types/tooltip.js.map +1 -0
  51. package/package.json +1 -1
  52. package/src/features/bottom-sheet/BottomSheetProvider.tsx +13 -8
  53. package/src/features/bottom-sheet/public.test.ts +23 -0
  54. package/src/features/bottom-sheet/public.ts +12 -1
  55. package/src/features/popover-menu/adapters/inbound/PopoverMenu.tsx +201 -0
  56. package/src/features/popover-menu/composition/usePopoverMenuController.ts +55 -0
  57. package/src/features/popover-menu/public.test.ts +13 -0
  58. package/src/features/popover-menu/public.ts +6 -0
  59. package/src/{components/menu/navigation.test.ts → features/popover-menu/utils/resolveNextMenuIndex.test.ts} +4 -4
  60. package/src/features/popover-menu/utils/resolveNextMenuIndex.ts +21 -0
  61. package/src/features/tooltip/adapters/inbound/Tooltip.tsx +56 -0
  62. package/src/features/tooltip/composition/useTooltipVisibility.ts +29 -0
  63. package/src/features/tooltip/public.test.ts +11 -0
  64. package/src/features/tooltip/public.ts +2 -0
  65. package/src/index.test.ts +8 -3
  66. package/src/index.ts +8 -4
  67. package/src/types/bottomSheet.ts +4 -0
  68. package/src/types/popoverMenu.ts +27 -0
  69. package/src/{components/tooltip/types.ts → types/tooltip.ts} +1 -1
  70. package/dist/components/menu/Menu.d.ts +0 -4
  71. package/dist/components/menu/Menu.d.ts.map +0 -1
  72. package/dist/components/menu/Menu.js +0 -152
  73. package/dist/components/menu/Menu.js.map +0 -1
  74. package/dist/components/menu/index.d.ts +0 -3
  75. package/dist/components/menu/index.d.ts.map +0 -1
  76. package/dist/components/menu/index.js +0 -2
  77. package/dist/components/menu/index.js.map +0 -1
  78. package/dist/components/menu/navigation.d.ts +0 -4
  79. package/dist/components/menu/navigation.d.ts.map +0 -1
  80. package/dist/components/menu/navigation.js +0 -25
  81. package/dist/components/menu/navigation.js.map +0 -1
  82. package/dist/components/menu/types.d.ts +0 -23
  83. package/dist/components/menu/types.d.ts.map +0 -1
  84. package/dist/components/menu/types.js +0 -2
  85. package/dist/components/menu/types.js.map +0 -1
  86. package/dist/components/tooltip/Tooltip.d.ts +0 -4
  87. package/dist/components/tooltip/Tooltip.d.ts.map +0 -1
  88. package/dist/components/tooltip/Tooltip.js +0 -75
  89. package/dist/components/tooltip/Tooltip.js.map +0 -1
  90. package/dist/components/tooltip/index.d.ts +0 -3
  91. package/dist/components/tooltip/index.d.ts.map +0 -1
  92. package/dist/components/tooltip/index.js +0 -2
  93. package/dist/components/tooltip/index.js.map +0 -1
  94. package/dist/components/tooltip/types.d.ts.map +0 -1
  95. package/dist/components/tooltip/types.js +0 -2
  96. package/dist/components/tooltip/types.js.map +0 -1
  97. package/src/components/menu/Menu.tsx +0 -228
  98. package/src/components/menu/index.ts +0 -2
  99. package/src/components/menu/navigation.ts +0 -34
  100. package/src/components/menu/types.ts +0 -26
  101. package/src/components/tooltip/Tooltip.tsx +0 -117
  102. package/src/components/tooltip/index.ts +0 -2
@@ -1,228 +0,0 @@
1
- import React from 'react';
2
- import { type LayoutRectangle, Pressable, View } from 'react-native';
3
-
4
- import { Box, Stack } from '../../features/layout/public';
5
- import { Surface } from '../../features/surface/public';
6
- import { Text } from '../../features/typography/public';
7
- import { FocusScope } from '../../internal/focus/FocusScope';
8
- import { useFocusManager } from '../../internal/focus/useFocusManager';
9
- import { Portal } from '../../internal/overlay/Portal';
10
- import { resolvePointerEvents } from '../../internal/resolvePointerEvents';
11
- import { resolveOverlayAnimation } from '../../internal/resolvers';
12
- import { Inline } from '../../layout';
13
- import { ButtonBase } from '../../primitives/button-base';
14
- import { useTheme } from '../../theme/ThemeContext';
15
- import { resolveNextMenuIndex } from './navigation';
16
- import type { MenuAction, MenuProps } from './types';
17
-
18
- const boxNonePointerEvents = resolvePointerEvents('box-none');
19
-
20
- interface MeasurableNode {
21
- measureInWindow?: (
22
- callback: (x: number, y: number, width: number, height: number) => void,
23
- ) => void;
24
- }
25
-
26
- function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void) {
27
- const measurableNode = node as MeasurableNode | null;
28
- measurableNode?.measureInWindow?.((x, y, width, height) => {
29
- callback({ height, width, x, y });
30
- });
31
- }
32
-
33
- function renderActionContent(action: MenuAction, active: boolean) {
34
- const titleColor =
35
- action.intent === 'danger' ? 'danger' : active || action.selected ? 'neutral' : undefined;
36
-
37
- return (
38
- <Inline align="center" gap="s" justify="space-between" wrap="nowrap">
39
- {action.leading ? <Box>{action.leading}</Box> : null}
40
- <Box flex={1}>
41
- <Stack gap="xxs">
42
- <Text
43
- color={titleColor}
44
- variant="bodySmall"
45
- weight={action.selected ? 'semiBold' : 'medium'}
46
- >
47
- {action.title}
48
- </Text>
49
- {action.description ? (
50
- <Text emphasis="muted" variant="caption">
51
- {action.description}
52
- </Text>
53
- ) : null}
54
- </Stack>
55
- </Box>
56
- {action.trailing ? <Box>{action.trailing}</Box> : null}
57
- </Inline>
58
- );
59
- }
60
-
61
- export function Menu({
62
- trigger,
63
- actions,
64
- dismiss,
65
- closeOnSelect = true,
66
- interactionPolicy = 'enabled',
67
- testID,
68
- }: MenuProps) {
69
- const { theme } = useTheme();
70
- const { bindKeydown } = useFocusManager();
71
- const animation = resolveOverlayAnimation('menu');
72
- const anchorRef = React.useRef<View | null>(null);
73
- const [open, setOpen] = React.useState(false);
74
- const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
75
- const [activeIndex, setActiveIndex] = React.useState(0);
76
- const passive = interactionPolicy === 'passive';
77
-
78
- const closeMenu = React.useCallback(() => {
79
- setOpen(false);
80
- if (dismiss) {
81
- dismiss();
82
- }
83
- }, [dismiss]);
84
-
85
- const activateAction = React.useCallback(
86
- (action: MenuAction) => {
87
- if (action.disabled) {
88
- return;
89
- }
90
-
91
- if (action.activate) {
92
- action.activate();
93
- }
94
-
95
- if (closeOnSelect) {
96
- closeMenu();
97
- }
98
- },
99
- [closeMenu, closeOnSelect],
100
- );
101
-
102
- const openMenu = React.useCallback(() => {
103
- measureNode(anchorRef.current, setLayout);
104
- const firstEnabledIndex = actions.findIndex((action) => !action.disabled);
105
- setActiveIndex(firstEnabledIndex === -1 ? 0 : firstEnabledIndex);
106
- setOpen(true);
107
- }, [actions]);
108
-
109
- React.useEffect(() => {
110
- if (!open) {
111
- return undefined;
112
- }
113
-
114
- return bindKeydown((event) => {
115
- if (passive) {
116
- return;
117
- }
118
-
119
- const { key } = event;
120
- if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End') {
121
- event.preventDefault();
122
- setActiveIndex((current) => resolveNextMenuIndex(actions, current, key));
123
- }
124
-
125
- if (event.key === 'Enter') {
126
- event.preventDefault();
127
- const activeAction = actions[activeIndex];
128
- if (activeAction) {
129
- activateAction(activeAction);
130
- }
131
- }
132
-
133
- if (event.key === 'Escape') {
134
- event.preventDefault();
135
- closeMenu();
136
- }
137
- });
138
- }, [activateAction, actions, activeIndex, bindKeydown, closeMenu, open, passive]);
139
-
140
- return (
141
- <View collapsable={false} ref={anchorRef}>
142
- <ButtonBase
143
- onPress={passive ? undefined : open ? closeMenu : openMenu}
144
- testID={testID ? `${testID}-trigger` : undefined}
145
- >
146
- {trigger}
147
- </ButtonBase>
148
- <Portal layer="menu" visible={open && Boolean(layout)}>
149
- <View
150
- {...boxNonePointerEvents.props}
151
- style={[
152
- boxNonePointerEvents.style,
153
- {
154
- bottom: 0,
155
- left: 0,
156
- position: 'absolute',
157
- right: 0,
158
- top: 0,
159
- },
160
- ]}
161
- >
162
- <Pressable
163
- onPress={passive ? undefined : closeMenu}
164
- style={{
165
- bottom: 0,
166
- left: 0,
167
- position: 'absolute',
168
- right: 0,
169
- top: 0,
170
- }}
171
- />
172
- <FocusScope active={open} onEscape={passive ? undefined : closeMenu}>
173
- <View
174
- style={{
175
- left: layout?.x ?? 0,
176
- position: 'absolute',
177
- top: (layout?.y ?? 0) + (layout?.height ?? 0) + animation.offset,
178
- }}
179
- >
180
- <Surface
181
- accessibilityRole="menu"
182
- p="xs"
183
- style={{
184
- minWidth: Math.max(layout?.width ?? 0, 220),
185
- shadowOpacity: 0.12,
186
- shadowRadius: 12,
187
- shadowOffset: { width: 0, height: 6 },
188
- }}
189
- testID={testID}
190
- variant="raised"
191
- >
192
- {actions.map((action, index) => {
193
- const active = index === activeIndex;
194
- const selected = action.selected ?? active;
195
-
196
- return (
197
- <Pressable
198
- accessibilityRole="menuitem"
199
- accessibilityState={{ disabled: action.disabled, selected }}
200
- disabled={action.disabled}
201
- key={action.id}
202
- onPress={passive ? undefined : () => activateAction(action)}
203
- >
204
- <Box
205
- px="m"
206
- py="s"
207
- radius="s"
208
- style={{
209
- backgroundColor: selected
210
- ? theme.semantics.action.neutral.softBg
211
- : 'transparent',
212
- opacity: action.disabled ? 0.56 : 1,
213
- }}
214
- testID={testID ? `${testID}-item-${action.id}` : undefined}
215
- >
216
- {renderActionContent(action, active)}
217
- </Box>
218
- </Pressable>
219
- );
220
- })}
221
- </Surface>
222
- </View>
223
- </FocusScope>
224
- </View>
225
- </Portal>
226
- </View>
227
- );
228
- }
@@ -1,2 +0,0 @@
1
- export { Menu } from './Menu';
2
- export type { MenuAction, MenuActionIntent, MenuAction as MenuItem, MenuProps } from './types';
@@ -1,34 +0,0 @@
1
- export function resolveNextMenuIndex(
2
- items: readonly { disabled?: boolean }[],
3
- currentIndex: number,
4
- key: 'ArrowDown' | 'ArrowUp' | 'Home' | 'End',
5
- ): number {
6
- const enabledIndexes = items.reduce<number[]>((indexes, item, index) => {
7
- if (!item.disabled) {
8
- indexes.push(index);
9
- }
10
- return indexes;
11
- }, []);
12
-
13
- if (enabledIndexes.length === 0) {
14
- return -1;
15
- }
16
-
17
- if (key === 'Home') {
18
- return enabledIndexes[0] ?? -1;
19
- }
20
-
21
- if (key === 'End') {
22
- return enabledIndexes[enabledIndexes.length - 1] ?? -1;
23
- }
24
-
25
- const activeEnabledIndex = enabledIndexes.indexOf(currentIndex);
26
- const nextEnabledIndex =
27
- activeEnabledIndex === -1
28
- ? 0
29
- : key === 'ArrowUp'
30
- ? (activeEnabledIndex - 1 + enabledIndexes.length) % enabledIndexes.length
31
- : (activeEnabledIndex + 1) % enabledIndexes.length;
32
-
33
- return enabledIndexes[nextEnabledIndex] ?? -1;
34
- }
@@ -1,26 +0,0 @@
1
- import type React from 'react';
2
-
3
- import type { InteractionPolicy } from '../../interactionPolicy';
4
-
5
- export type MenuActionIntent = 'default' | 'danger';
6
-
7
- export interface MenuAction {
8
- id: string;
9
- title: React.ReactNode;
10
- description?: React.ReactNode;
11
- leading?: React.ReactNode;
12
- trailing?: React.ReactNode;
13
- intent?: MenuActionIntent;
14
- disabled?: boolean;
15
- selected?: boolean;
16
- activate?: () => void;
17
- }
18
-
19
- export interface MenuProps {
20
- trigger?: React.ReactNode;
21
- actions: readonly MenuAction[];
22
- dismiss?: () => void;
23
- closeOnSelect?: boolean;
24
- interactionPolicy?: InteractionPolicy;
25
- testID?: string;
26
- }
@@ -1,117 +0,0 @@
1
- import React from 'react';
2
- import { type LayoutRectangle, Platform, Pressable, View } from 'react-native';
3
-
4
- import { Surface } from '../../features/surface/public';
5
- import { Text } from '../../features/typography/public';
6
- import { Portal } from '../../internal/overlay/Portal';
7
- import { resolvePointerEvents } from '../../internal/resolvePointerEvents';
8
- import { resolveOverlayAnimation } from '../../internal/resolvers';
9
- import { useTheme } from '../../theme/ThemeContext';
10
- import type { TooltipProps } from './types';
11
-
12
- const noPointerEvents = resolvePointerEvents('none');
13
-
14
- interface MeasurableNode {
15
- measureInWindow?: (
16
- callback: (x: number, y: number, width: number, height: number) => void,
17
- ) => void;
18
- }
19
-
20
- function measureNode(node: unknown, callback: (layout: LayoutRectangle) => void) {
21
- const measurableNode = node as MeasurableNode | null;
22
- measurableNode?.measureInWindow?.((x, y, width, height) => {
23
- callback({ height, width, x, y });
24
- });
25
- }
26
-
27
- export function Tooltip({
28
- children,
29
- content,
30
- delay = 150,
31
- interactionPolicy = 'enabled',
32
- placement = 'top',
33
- testID,
34
- }: TooltipProps) {
35
- const { theme } = useTheme();
36
- const animation = resolveOverlayAnimation('tooltip');
37
- const anchorRef = React.useRef<View | null>(null);
38
- const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
39
- const [visible, setVisible] = React.useState(false);
40
- const [layout, setLayout] = React.useState<LayoutRectangle | null>(null);
41
- const passive = interactionPolicy === 'passive';
42
-
43
- const show = React.useCallback(() => {
44
- if (timeoutRef.current) {
45
- clearTimeout(timeoutRef.current);
46
- }
47
-
48
- timeoutRef.current = setTimeout(() => {
49
- measureNode(anchorRef.current, setLayout);
50
- setVisible(true);
51
- }, delay);
52
- }, [delay]);
53
-
54
- const hide = React.useCallback(() => {
55
- if (timeoutRef.current) {
56
- clearTimeout(timeoutRef.current);
57
- timeoutRef.current = null;
58
- }
59
- setVisible(false);
60
- }, []);
61
-
62
- React.useEffect(
63
- () => () => {
64
- if (timeoutRef.current) {
65
- clearTimeout(timeoutRef.current);
66
- }
67
- },
68
- [],
69
- );
70
-
71
- const tooltipLeft = layout ? layout.x : 0;
72
- const tooltipTop =
73
- layout && placement === 'top'
74
- ? layout.y - animation.offset
75
- : layout
76
- ? layout.y + layout.height + animation.offset
77
- : 0;
78
-
79
- return (
80
- <View collapsable={false} ref={anchorRef} testID={testID ? `${testID}-anchor` : undefined}>
81
- <Pressable
82
- onBlur={passive ? undefined : hide}
83
- onFocus={passive ? undefined : show}
84
- onHoverIn={Platform.OS === 'web' && !passive ? show : undefined}
85
- onHoverOut={Platform.OS === 'web' && !passive ? hide : undefined}
86
- >
87
- {children}
88
- </Pressable>
89
- <Portal layer="tooltip" visible={visible && Boolean(layout)}>
90
- <View
91
- {...noPointerEvents.props}
92
- style={[
93
- noPointerEvents.style,
94
- {
95
- left: tooltipLeft,
96
- position: 'absolute',
97
- top: tooltipTop,
98
- },
99
- ]}
100
- >
101
- <Surface
102
- p="s"
103
- style={{
104
- backgroundColor: theme.semantics.surface.inverse,
105
- }}
106
- testID={testID}
107
- variant="raised"
108
- >
109
- <Text emphasis="inverse" variant="caption">
110
- {content}
111
- </Text>
112
- </Surface>
113
- </View>
114
- </Portal>
115
- </View>
116
- );
117
- }
@@ -1,2 +0,0 @@
1
- export { Tooltip } from './Tooltip';
2
- export type { TooltipProps } from './types';