@fluentui-react-native/callout 0.28.5 → 0.29.2

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 (69) hide show
  1. package/.cache/tsconfig.tsbuildinfo +1 -1
  2. package/CHANGELOG.md +65 -0
  3. package/FRNCallout.podspec +1 -1
  4. package/NuGet.config +10 -0
  5. package/SPEC.md +46 -0
  6. package/lib/Callout.d.ts +2 -5
  7. package/lib/Callout.d.ts.map +1 -1
  8. package/lib/Callout.js +57 -58
  9. package/lib/Callout.js.map +1 -1
  10. package/lib/Callout.types.d.ts +51 -152
  11. package/lib/Callout.types.d.ts.map +1 -1
  12. package/lib/Callout.types.js.map +1 -1
  13. package/lib/CalloutNativeComponent.d.ts +7 -3
  14. package/lib/CalloutNativeComponent.d.ts.map +1 -1
  15. package/lib/CalloutNativeComponent.js +3 -1
  16. package/lib/CalloutNativeComponent.js.map +1 -1
  17. package/lib/__tests__/Callout.test.js +94 -10
  18. package/lib/__tests__/Callout.test.js.map +1 -1
  19. package/lib/__tests__/Callout.types.test.d.ts +2 -0
  20. package/lib/__tests__/Callout.types.test.d.ts.map +1 -0
  21. package/lib/__tests__/Callout.types.test.js +21 -0
  22. package/lib/__tests__/Callout.types.test.js.map +1 -0
  23. package/lib/index.d.ts +1 -2
  24. package/lib/index.d.ts.map +1 -1
  25. package/macos/CalloutManager.swift +1 -1
  26. package/macos/CalloutView.swift +49 -17
  27. package/macos/FRNCalloutManager.m +1 -2
  28. package/macos/RCTCalloutComponentView.h +12 -0
  29. package/macos/RCTCalloutComponentView.mm +208 -0
  30. package/package.json +27 -12
  31. package/react-native.config.cjs +15 -0
  32. package/src/Callout.stories.tsx +271 -0
  33. package/src/Callout.tsx +98 -64
  34. package/src/Callout.types.ts +50 -172
  35. package/src/CalloutNativeComponent.ts +10 -5
  36. package/src/__tests__/Callout.test.tsx +152 -8
  37. package/src/__tests__/Callout.types.test.ts +30 -0
  38. package/src/__tests__/__snapshots__/Callout.test.tsx.snap +8 -10
  39. package/src/index.ts +4 -4
  40. package/tsconfig.json +2 -13
  41. package/windows/Callout/Callout.cpp +398 -0
  42. package/windows/Callout/Callout.def +3 -0
  43. package/windows/Callout/Callout.h +25 -0
  44. package/windows/Callout/Callout.vcxproj +152 -0
  45. package/windows/Callout/ReactPackageProvider.cpp +25 -0
  46. package/windows/Callout/ReactPackageProvider.h +25 -0
  47. package/windows/Callout/ReactPackageProvider.idl +12 -0
  48. package/windows/Callout/codegen/.clang-format +2 -0
  49. package/windows/Callout/codegen/react/components/FRNCalloutSpec/Callout.g.h +375 -0
  50. package/windows/Callout/packages.lock.json +151 -0
  51. package/windows/Callout/pch.cpp +1 -0
  52. package/windows/Callout/pch.h +28 -0
  53. package/windows/ExperimentalFeatures.props +33 -0
  54. package/windows/NuGet.Config +13 -0
  55. package/lib/Callout.settings.d.ts +0 -4
  56. package/lib/Callout.settings.d.ts.map +0 -1
  57. package/lib/Callout.settings.js +0 -16
  58. package/lib/Callout.settings.js.map +0 -1
  59. package/lib/Callout.settings.macos.d.ts +0 -4
  60. package/lib/Callout.settings.macos.d.ts.map +0 -1
  61. package/lib/Callout.settings.macos.js +0 -19
  62. package/lib/Callout.settings.macos.js.map +0 -1
  63. package/lib/CalloutNativeComponent.macos.d.ts +0 -45
  64. package/lib/CalloutNativeComponent.macos.d.ts.map +0 -1
  65. package/lib/CalloutNativeComponent.macos.js +0 -7
  66. package/lib/CalloutNativeComponent.macos.js.map +0 -1
  67. package/src/Callout.settings.macos.ts +0 -22
  68. package/src/Callout.settings.ts +0 -19
  69. package/src/CalloutNativeComponent.macos.ts +0 -69
@@ -0,0 +1,271 @@
1
+ /** @jsxImportSource @fluentui-react-native/framework-base */
2
+ import * as React from 'react';
3
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
4
+
5
+ import type { Meta, StoryObj } from '@storybook/react-native';
6
+
7
+ import { Callout } from './Callout';
8
+ import type { CalloutHandle, CalloutProps, DirectionalHint } from './Callout.types';
9
+
10
+ const directionalHints: readonly DirectionalHint[] = [
11
+ 'leftTopEdge',
12
+ 'leftCenter',
13
+ 'leftBottomEdge',
14
+ 'topLeftEdge',
15
+ 'topAutoEdge',
16
+ 'topCenter',
17
+ 'topRightEdge',
18
+ 'rightTopEdge',
19
+ 'rightCenter',
20
+ 'rightBottomEdge',
21
+ 'bottomLeftEdge',
22
+ 'bottomAutoEdge',
23
+ 'bottomCenter',
24
+ 'bottomRightEdge',
25
+ ];
26
+
27
+ type CalloutExampleProps = CalloutProps & {
28
+ defaultVisible?: boolean;
29
+ showWindowCommands?: boolean;
30
+ };
31
+
32
+ const CalloutExample = ({ defaultVisible = false, onDismiss, onShow, showWindowCommands = false, ...props }: CalloutExampleProps) => {
33
+ const anchorRef = React.useRef<View>(null);
34
+ const calloutRef = React.useRef<CalloutHandle>(null);
35
+ const [visible, setVisible] = React.useState(defaultVisible);
36
+ const [status, setStatus] = React.useState(defaultVisible ? 'Opening' : 'Closed');
37
+
38
+ const dismiss = React.useCallback(() => {
39
+ setVisible(false);
40
+ setStatus('Dismissed');
41
+ onDismiss?.();
42
+ }, [onDismiss]);
43
+
44
+ const show = React.useCallback(() => {
45
+ setStatus('Shown');
46
+ onShow?.();
47
+ }, [onShow]);
48
+
49
+ return (
50
+ <View style={styles.story}>
51
+ <View>
52
+ <Pressable
53
+ accessibilityRole="button"
54
+ onPress={() => {
55
+ setVisible((current) => !current);
56
+ setStatus(visible ? 'Closed' : 'Opening');
57
+ }}
58
+ ref={anchorRef}
59
+ style={({ pressed }) => [styles.trigger, pressed && styles.triggerPressed]}
60
+ testID="agentic-storybook-callout-trigger"
61
+ >
62
+ <Text style={styles.triggerText}>{visible ? 'Close callout' : 'Open callout'}</Text>
63
+ </Pressable>
64
+ </View>
65
+ <Text accessibilityLiveRegion="polite" style={styles.status} testID="agentic-storybook-callout-status">
66
+ Native window: {status}
67
+ </Text>
68
+ {visible && (
69
+ <Callout {...props} componentRef={calloutRef} onDismiss={dismiss} onShow={show} target={anchorRef}>
70
+ <View style={styles.calloutContent} collapsable={false}>
71
+ <Text style={styles.heading}>Fabric Callout</Text>
72
+ <Text style={styles.body}>This content is hosted in a separate native window.</Text>
73
+ {showWindowCommands && (
74
+ <View style={styles.commandRow}>
75
+ <Pressable accessibilityRole="button" onPress={() => calloutRef.current?.focusWindow()} style={styles.command}>
76
+ <Text>Focus window</Text>
77
+ </Pressable>
78
+ <Pressable accessibilityRole="button" onPress={() => calloutRef.current?.blurWindow()} style={styles.command}>
79
+ <Text>Blur window</Text>
80
+ </Pressable>
81
+ </View>
82
+ )}
83
+ </View>
84
+ </Callout>
85
+ )}
86
+ </View>
87
+ );
88
+ };
89
+
90
+ type Placement = {
91
+ hint: DirectionalHint;
92
+ label: string;
93
+ };
94
+
95
+ const placements: readonly Placement[] = [
96
+ { hint: 'topCenter', label: 'Above' },
97
+ { hint: 'rightCenter', label: 'Right' },
98
+ { hint: 'bottomCenter', label: 'Below' },
99
+ { hint: 'leftCenter', label: 'Left' },
100
+ ];
101
+
102
+ const PlacementExample = (props: CalloutProps) => {
103
+ const aboveRef = React.useRef<View>(null);
104
+ const rightRef = React.useRef<View>(null);
105
+ const belowRef = React.useRef<View>(null);
106
+ const leftRef = React.useRef<View>(null);
107
+ const targetRefs = [aboveRef, rightRef, belowRef, leftRef] as const;
108
+ const [selectedIndex, setSelectedIndex] = React.useState<number>();
109
+
110
+ return (
111
+ <View style={styles.placementStory}>
112
+ {placements.map(({ hint, label }, index) => (
113
+ <View key={hint} style={styles.placementAnchor}>
114
+ <Pressable
115
+ accessibilityRole="button"
116
+ onPress={() => setSelectedIndex(index)}
117
+ ref={targetRefs[index]}
118
+ style={({ pressed }) => [styles.trigger, pressed && styles.triggerPressed]}
119
+ >
120
+ <Text style={styles.triggerText}>{label}</Text>
121
+ </Pressable>
122
+ </View>
123
+ ))}
124
+ {selectedIndex !== undefined && (
125
+ <Callout
126
+ {...props}
127
+ directionalHint={placements[selectedIndex].hint}
128
+ onDismiss={() => setSelectedIndex(undefined)}
129
+ target={targetRefs[selectedIndex]}
130
+ >
131
+ <View style={styles.calloutContent} collapsable={false}>
132
+ <Text style={styles.heading}>{placements[selectedIndex].label}</Text>
133
+ <Text style={styles.body}>Click outside the native window to dismiss it.</Text>
134
+ </View>
135
+ </Callout>
136
+ )}
137
+ </View>
138
+ );
139
+ };
140
+
141
+ const meta: Meta<typeof Callout> = {
142
+ title: 'Primitives/Callout',
143
+ component: Callout,
144
+ args: {
145
+ accessibilityLabel: 'Callout example',
146
+ accessibilityRole: 'dialog',
147
+ backgroundColor: '#ffffff',
148
+ borderColor: '#d1d1d1',
149
+ borderRadius: 8,
150
+ borderWidth: 1,
151
+ directionalHint: 'bottomCenter',
152
+ maxWidth: 320,
153
+ setInitialFocus: false,
154
+ testID: 'agentic-storybook-callout',
155
+ },
156
+ argTypes: {
157
+ backgroundColor: { control: 'color' },
158
+ borderColor: { control: 'color' },
159
+ borderRadius: { control: { type: 'number', min: 0, max: 24, step: 1 } },
160
+ borderWidth: { control: { type: 'number', min: 0, max: 8, step: 1 } },
161
+ directionalHint: { control: 'select', options: directionalHints },
162
+ maxHeight: { control: { type: 'number', min: 80, max: 600, step: 20 } },
163
+ maxWidth: { control: { type: 'number', min: 120, max: 600, step: 20 } },
164
+ minWidth: { control: { type: 'number', min: 80, max: 400, step: 20 } },
165
+ setInitialFocus: { control: 'boolean' },
166
+ },
167
+ parameters: {
168
+ docs: {
169
+ description: {
170
+ component:
171
+ 'Callout is an unstyled native primitive that presents React content in a transient window positioned relative to a target.',
172
+ },
173
+ },
174
+ },
175
+ };
176
+
177
+ export default meta;
178
+
179
+ type Story = StoryObj<typeof Callout>;
180
+
181
+ export const Default: Story = {
182
+ render: (args) => <CalloutExample {...args} defaultVisible />,
183
+ };
184
+
185
+ export const Placement: Story = {
186
+ render: (args) => <PlacementExample {...args} />,
187
+ parameters: {
188
+ docs: {
189
+ description: {
190
+ story: 'Each trigger requests one of four placement edges. The native Callout remains within the available work area.',
191
+ },
192
+ },
193
+ },
194
+ };
195
+
196
+ export const WindowCommands: Story = {
197
+ args: {
198
+ setInitialFocus: true,
199
+ },
200
+ render: (args) => <CalloutExample {...args} defaultVisible showWindowCommands />,
201
+ parameters: {
202
+ docs: {
203
+ description: {
204
+ story: 'The content buttons exercise Fabric touch handling and the imperative focusWindow and blurWindow native commands.',
205
+ },
206
+ },
207
+ },
208
+ };
209
+
210
+ const styles = StyleSheet.create({
211
+ body: {
212
+ color: '#424242',
213
+ marginTop: 6,
214
+ },
215
+ calloutContent: {
216
+ padding: 16,
217
+ width: 280,
218
+ },
219
+ command: {
220
+ backgroundColor: '#f5f5f5',
221
+ borderColor: '#d1d1d1',
222
+ borderRadius: 4,
223
+ borderWidth: 1,
224
+ paddingHorizontal: 12,
225
+ paddingVertical: 8,
226
+ },
227
+ commandRow: {
228
+ flexDirection: 'row',
229
+ gap: 8,
230
+ marginTop: 16,
231
+ },
232
+ heading: {
233
+ color: '#242424',
234
+ fontSize: 16,
235
+ fontWeight: '600',
236
+ },
237
+ placementAnchor: {
238
+ margin: 32,
239
+ },
240
+ placementStory: {
241
+ alignItems: 'center',
242
+ flexDirection: 'row',
243
+ flexWrap: 'wrap',
244
+ justifyContent: 'center',
245
+ minHeight: 300,
246
+ width: 520,
247
+ },
248
+ status: {
249
+ color: '#616161',
250
+ marginTop: 12,
251
+ },
252
+ story: {
253
+ alignItems: 'center',
254
+ justifyContent: 'center',
255
+ minHeight: 300,
256
+ width: 520,
257
+ },
258
+ trigger: {
259
+ backgroundColor: '#0f6cbd',
260
+ borderRadius: 4,
261
+ paddingHorizontal: 16,
262
+ paddingVertical: 10,
263
+ },
264
+ triggerPressed: {
265
+ backgroundColor: '#115ea3',
266
+ },
267
+ triggerText: {
268
+ color: '#ffffff',
269
+ fontWeight: '600',
270
+ },
271
+ });
package/src/Callout.tsx CHANGED
@@ -1,77 +1,111 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- * Licensed under the MIT License.
4
- * @format
5
- */
6
-
1
+ /** @jsxImportSource @fluentui-react-native/framework-base */
7
2
  import * as React from 'react';
8
- import { findNodeHandle } from 'react-native';
3
+ import { findNodeHandle, StyleSheet } from 'react-native';
9
4
 
10
- import { backgroundColorTokens, borderTokens } from '@fluentui-react-native/tokens';
11
- import type { IUseComposeStyling } from '@uifabricshared/foundation-compose';
12
- import { compose } from '@uifabricshared/foundation-compose';
13
- import { mergeSettings } from '@uifabricshared/foundation-settings';
5
+ import { directComponent, mergeProps, phasedComponent } from '@fluentui-react-native/framework-base';
14
6
 
15
- import { settings } from './Callout.settings';
16
- import type { ICalloutProps, ICalloutSlotProps, ICalloutType } from './Callout.types';
7
+ import type { CalloutProps } from './Callout.types';
17
8
  import { calloutName } from './Callout.types';
9
+ import NativeCalloutView, { Commands } from './CalloutNativeComponent';
18
10
 
19
- import NativeCalloutView from './CalloutNativeComponent';
20
- import { Commands } from './CalloutNativeComponent';
11
+ const colorTransparent = '#00000000';
21
12
 
22
- export const Callout = compose<ICalloutType>({
23
- displayName: calloutName,
24
- usePrepareProps: (props: ICalloutProps, useStyling: IUseComposeStyling<ICalloutType>) => {
25
- const { componentRef, target, ...rest } = props;
26
- React.useImperativeHandle(
27
- componentRef,
28
- () => ({
29
- blurWindow() {
30
- if (componentRef.current != null) {
31
- Commands.blurWindow(nativeComponentRef.current);
32
- }
33
- },
34
- focusWindow() {
35
- if (componentRef.current != null) {
36
- Commands.focusWindow(nativeComponentRef.current);
37
- }
38
- },
39
- }),
40
- [componentRef],
41
- );
42
- const nativeComponentRef = React.useRef<React.ElementRef<typeof NativeCalloutView> | null>(null);
43
- const [nativeTarget, setNativeTarget] = React.useState<number | string | null>(null);
13
+ const styles = StyleSheet.create({
14
+ root: {
15
+ position: 'absolute',
16
+ },
17
+ });
44
18
 
45
- React.useLayoutEffect(() => {
46
- if (typeof target === 'string') {
47
- // Pass string type `target` directly
48
- setNativeTarget(target);
49
- } else if (target?.current) {
50
- // Pass the tagID for a valid ref `target`
51
- setNativeTarget(findNodeHandle(target.current));
52
- } else {
53
- // Clear `target` so we may fall back on `anchorRect` if provided
54
- setNativeTarget(null);
55
- }
56
- }, [target]);
19
+ /**
20
+ * Renders the native Callout without applying theme or appearance defaults.
21
+ */
22
+ export const Callout = phasedComponent<CalloutProps>((props) => {
23
+ const { componentRef, target } = props;
24
+ const nativeComponentRef = React.useRef<React.ElementRef<typeof NativeCalloutView> | null>(null);
25
+ const [nativeTarget, setNativeTarget] = React.useState<number | string | undefined>(undefined);
57
26
 
58
- const slotProps = mergeSettings<ICalloutSlotProps>(useStyling(props), {
59
- root: {
60
- ref: nativeComponentRef,
61
- ...(nativeTarget && { target: nativeTarget }),
62
- ...rest,
27
+ React.useImperativeHandle(
28
+ componentRef,
29
+ () => ({
30
+ blurWindow() {
31
+ if (nativeComponentRef.current !== null) {
32
+ Commands.blurWindow(nativeComponentRef.current);
33
+ }
34
+ },
35
+ focusWindow() {
36
+ if (nativeComponentRef.current !== null) {
37
+ Commands.focusWindow(nativeComponentRef.current);
38
+ }
63
39
  },
64
- });
40
+ }),
41
+ [],
42
+ );
65
43
 
66
- return { slotProps };
67
- },
68
- settings: settings,
69
- slots: {
70
- root: NativeCalloutView,
71
- },
72
- styles: {
73
- root: [backgroundColorTokens, borderTokens],
74
- },
44
+ React.useLayoutEffect(() => {
45
+ if (typeof target === 'string') {
46
+ setNativeTarget(target);
47
+ } else if (target?.current) {
48
+ setNativeTarget(findNodeHandle(target.current) ?? undefined);
49
+ } else {
50
+ setNativeTarget(undefined);
51
+ }
52
+ }, [target]);
53
+
54
+ return directComponent<CalloutProps>((renderProps) => {
55
+ const {
56
+ anchorRect,
57
+ backgroundColor,
58
+ beakWidth,
59
+ borderColor,
60
+ borderRadius,
61
+ borderWidth,
62
+ componentRef: _componentRef,
63
+ directionalHint,
64
+ dismissBehaviors,
65
+ gapSpace,
66
+ maxHeight,
67
+ maxWidth,
68
+ minPadding,
69
+ minWidth,
70
+ style,
71
+ target: _target,
72
+ ...nativeProps
73
+ } = mergeProps(props, renderProps);
74
+
75
+ /**
76
+ * Build the style for the callout based on the props. For `borderColor`, `borderWidth`, `backgroundColor`, and
77
+ * `borderRadius` defaults values are required otherwise crashes in CalloutView.swift:updateLayer() will occur.
78
+ */
79
+ const calloutStyle = {
80
+ backgroundColor: backgroundColor ?? colorTransparent,
81
+ borderColor: borderColor ?? colorTransparent,
82
+ borderWidth: borderWidth ?? 0,
83
+ borderRadius: borderRadius ?? 0,
84
+ ...(maxHeight != null && { maxHeight }),
85
+ ...(maxWidth != null && { maxWidth }),
86
+ ...(minWidth != null && { minWidth }),
87
+ };
88
+
89
+ return (
90
+ <NativeCalloutView
91
+ {...nativeProps}
92
+ anchorRect={anchorRect}
93
+ beakWidth={beakWidth}
94
+ directionalHint={directionalHint}
95
+ dismissBehaviors={dismissBehaviors}
96
+ gapSpace={gapSpace}
97
+ maxHeight={typeof maxHeight === 'number' ? maxHeight : undefined}
98
+ maxWidth={typeof maxWidth === 'number' ? maxWidth : undefined}
99
+ minPadding={minPadding}
100
+ minWidth={typeof minWidth === 'number' ? minWidth : undefined}
101
+ ref={nativeComponentRef}
102
+ style={[styles.root, calloutStyle, style]}
103
+ {...(nativeTarget !== undefined && { target: nativeTarget })}
104
+ />
105
+ );
106
+ });
75
107
  });
76
108
 
109
+ Callout.displayName = calloutName;
110
+
77
111
  export default Callout;