Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-callouts-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fluentui-react-native/callout": patch
---

Add macOS and Windows Fabric component views and interactive Callout stories
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"**/*.generated.*",
"**/lib-commonjs/**",
"**/dist/**",
"packages/components/Callout/windows/FRNCallout/codegen/**",
"**/CHANGELOG.*",
"**/CODE_OF_CONDUCT.md",
"**/SECURITY.md",
Expand Down
2 changes: 1 addition & 1 deletion apps/fluent-tester/macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2640,7 +2640,7 @@ SPEC CHECKSUMS:
FBLazyVector: 3cde5aa3abeda29fd70560e5a3e3199e6add163f
fmt: 4cf0c5ec5864511c96d8d4bd7b03c3c69040af06
FRNAvatar: 7c377a859feea236a20eb0413c2e73afcc21e0d2
FRNCallout: f2acc41ab57051facfadcd7e3a6c68c98ddab0e4
FRNCallout: bb597344d0051c47d12f37192910a2e145e40543
FRNCheckbox: 48dcf237f30dff0cb66ab1befa1401b72d2393df
FRNMenuButton: f7d6ef3c81a6308fd00dc5f4241dcc6d5e5e63dd
FRNRadioButton: c9421d2736512aa64bbf46517fe702e43013586a
Expand Down
1 change: 0 additions & 1 deletion docs/pages/Components/Callout.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ type DirectionalHint =
| 'rightCenter'
| 'rightBottomEdge'
| 'bottomLeftEdge'
| 'bottonLeftEdge' // Deprecated misspelling retained for compatibility
| 'bottomAutoEdge'
| 'bottomCenter'
| 'bottomRightEdge';
Expand Down
19 changes: 10 additions & 9 deletions packages/agentic-components/storybook/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Agentic Components Storybook

On-device [Storybook](https://storybook.js.org/) app (Storybook for React Native v10) for
`@fluentui-react-native/components`. It loads every `*.stories.(ts|tsx)` file from the
library source (`../src`) so new component stories appear automatically.
`@fluentui-react-native/components` and linked standalone native packages. It loads every
`*.stories.(ts|tsx)` file from the agentic library source (`../src`) plus the standalone
Callout package so its native stories run in the Fabric host.

It runs in Storybook **liteMode**, which mocks out the heavy default on-device UI
(`@storybook/react-native-ui`). This avoids the `react-native-reanimated` /
Expand All @@ -19,8 +20,7 @@ selected while navigating between stories.

```
storybook/
.rnstorybook/ Storybook config (main.ts, preview.tsx, index.tsx)
StorybookApp.tsx Root component -> renders the Storybook UI
src/ Storybook config, generated requires, and root component
index.js AppRegistry entry
app.json react-native-test-app manifest
metro.config.js rnx-kit metro config wrapped with withStorybook (liteMode)
Expand All @@ -31,11 +31,11 @@ storybook/
> `StorybookApp.tsx` is intentionally not named `App.tsx`: on a case-insensitive macOS
> filesystem `App` collides with `app.json` during Metro resolution.

The `.rnstorybook/storybook.requires.ts` file is **generated** (git-ignored) from the
The `src/storybook.requires.ts` file is **generated** (git-ignored) from the
`main.ts` stories glob by the `withStorybook` metro wrapper when Metro starts, or on demand via:

```sh
yarn workspace @fluentui-react-native/agentic-components-storybook storybook-generate
yarn workspace @fluentui-react-native/agentic-components-storybook prebuild
```

## Running on macOS
Expand Down Expand Up @@ -76,7 +76,8 @@ yarn pods:macos:update
## Running on Windows

The Windows app also uses `react-native-test-app`. Its generated Win32 project uses React Native
Windows' New Architecture and Fabric renderer.
Windows 0.81's New Architecture and Fabric renderer. The Callout package is autolinked as a
Windows Fabric native library; its Paper implementation remains built into the platform.

```powershell
# from this directory
Expand Down Expand Up @@ -131,7 +132,7 @@ yarn storybook-server # WebSocket: ws://127.0.0.1:7007/ MCP: http://127.0.0.
```

Run it alongside `yarn start` + `yarn macos` or `yarn windows`. The on-device app connects to it automatically
(`.rnstorybook/index.tsx` calls `getStorybookUI({ enableWebsockets: true, host, port })`).
(`src/StorybookApp.tsx` calls `getStorybookUI({ enableWebsockets: true, host, port })`).

- **WebSocket channel** (`ws://127.0.0.1:7007/`): agents connect and emit Storybook channel events
to drive the app — e.g. `setCurrentStory` (`{ storyId }`) to switch story, and arg-update events
Expand All @@ -153,5 +154,5 @@ Run it alongside `yarn start` + `yarn macos` or `yarn windows`. The on-device ap
## Writing stories

Follow the package-level story authoring instructions in `../AGENTS.md`. Add a `*.stories.tsx` file next to its component
under `../src`; the `src/main.ts` glob discovers it automatically. See
under `../src`; standalone native package story globs are listed explicitly in `src/main.ts`. See
`../src/components/button/button.stories.tsx` for the canonical higher-order component example.
2 changes: 1 addition & 1 deletion packages/agentic-components/storybook/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const config = makeMetroConfig({
});

module.exports = withStorybook(config, {
configPath: path.resolve(__dirname, '.rnstorybook'),
configPath: path.resolve(__dirname, 'src'),
// Lite mode mocks out the heavy default Storybook UI so we don't need react-native-reanimated,
// react-native-gesture-handler, @gorhom/bottom-sheet or react-native-svg.
liteMode: true,
Expand Down
4 changes: 2 additions & 2 deletions packages/agentic-components/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"format": "fluentui-scripts format"
},
"dependencies": {
"@fluentui-react-native/callout": "workspace:*",
"@fluentui-react-native/components": "workspace:*",
"@fluentui-react-native/default-theme": "workspace:*",
"@fluentui-react-native/design": "workspace:*",
Expand All @@ -44,8 +45,6 @@
"@react-native-community/cli": "^20.0.0",
"@react-native-community/cli-platform-android": "^20.0.0",
"@react-native-community/cli-platform-ios": "^20.0.0",
"@react-native-community/datetimepicker": "^9.1.0",
"@react-native-community/slider": "^5.0.0",
"@react-native-windows/cli": "^0.81.0",
"@react-native/babel-preset": "^0.81.0",
"@react-native/metro-babel-transformer": "^0.81.0",
Expand All @@ -71,6 +70,7 @@
"furn": {
"knip": {
"ignoreDependencies": [
"@fluentui-react-native/callout",
"@fluentui-react-native/components",
"react-native-svg"
]
Expand Down
9 changes: 1 addition & 8 deletions packages/agentic-components/storybook/react-native.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,7 @@ const project = (() => {

module.exports = {
dependencies: {
// These optional Storybook control dependencies only provide legacy UWP projects. Linking
// them into the WinUI 3 Fabric app fails, while liteMode does not require their native views.
'@react-native-community/datetimepicker': {
platforms: {
windows: null,
},
},
'@react-native-community/slider': {
'react-native-svg': {
platforms: {
windows: null,
},
Expand Down
6 changes: 3 additions & 3 deletions packages/agentic-components/storybook/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import type { StorybookConfig } from '@storybook/react-native';
/**
* Storybook configuration for the agentic-components on-device app.
*
* Stories are loaded directly from the sibling library source (`../../src`) so any
* `*.stories.(ts|tsx)` added to a component automatically shows up here.
* Stories are loaded from the sibling agentic library and standalone native packages
* that are linked into this application.
*/
const main: StorybookConfig = {
stories: ['../../src/**/*.stories.?(ts|tsx)'],
stories: ['../../src/**/*.stories.?(ts|tsx)', '../../../components/Callout/src/**/*.stories.?(ts|tsx)'],
addons: [],
deviceAddons: ['@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-actions'],
};
Expand Down
4 changes: 2 additions & 2 deletions packages/agentic-components/storybook/storybook-server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This lets external agents drive the running on-device Storybook app:
// - WebSocket channel (ws://<host>:<port>/): select the current story, read/update control
// args, and receive Storybook channel events. The on-device app connects to this server
// (configured via getStorybookUI({ enableWebsockets: true, host, port }) in .rnstorybook/index.tsx).
// (configured via getStorybookUI({ enableWebsockets: true, host, port }) in src/StorybookApp.tsx).
// - MCP endpoint (http://<host>:<port>/mcp): an MCP server for AI agents to query story /
// component documentation and metadata (enabled via experimental_mcp).
//
Expand All @@ -21,7 +21,7 @@ const port = Number(process.env.STORYBOOK_WS_PORT) || 7007;
createChannelServer({
host,
port,
configPath: path.resolve(__dirname, '.rnstorybook'),
configPath: path.resolve(__dirname, 'src'),
websockets: true,
experimental_mcp: true,
keepNodeProcessAlive: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/agentic-components/storybook/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"include": ["index.js", "src", "app.json"],
"exclude": ["node_modules", "dist", ".cache"],
"references": [
{
"path": "../../components/Callout/tsconfig.json"
},
{
"path": "../../agentic-design/tsconfig.json"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/components/Callout/FRNCallout.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Pod::Spec.new do |s|
s.swift_version = "5"

s.osx.deployment_target = "10.15"
s.osx.source_files = "macos/*.{swift,h,m}"
s.osx.source_files = "macos/*.{swift,h,m,mm}"

s.dependency 'React'

Expand Down
11 changes: 11 additions & 0 deletions packages/components/Callout/NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="react-native" value="https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json" />
<add key="Nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<disabledPackageSources>
<clear />
</disabledPackageSources>
</configuration>
4 changes: 3 additions & 1 deletion packages/components/Callout/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ CocoaPods integration.
## Platform behavior

- macOS presents children in a native popup window and supports window focus
commands.
commands through both Paper and Fabric component registrations.
- Windows uses the platform's built-in Paper `RCTCallout` and supplies a
package-owned Fabric registration for React Native Windows 0.81 and newer.
- Win32 supplies the platform `RCTCallout` implementation and supports native
dismissal, pointer capture, beak, and focus-restoration behavior.
- Unsupported native behavior remains platform-defined rather than being
Expand Down
66 changes: 49 additions & 17 deletions packages/components/Callout/macos/CalloutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,11 @@ open class CalloutView: RCTView, CalloutWindowLifeCycleDelegate {

public weak var bridge: RCTBridge?

private init() {
super.init(frame: .zero)
}
@objc public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)

public required init?(coder: NSCoder) {
preconditionFailure()
}

convenience init(bridge: RCTBridge) {
self.init()
self.bridge = bridge
backgroundColor = .clear
borderColor = .clear

// Listens for mouse clicks in the main menu bar while callout is shown
NotificationCenter.default.addObserver(
Expand All @@ -66,6 +60,15 @@ open class CalloutView: RCTView, CalloutWindowLifeCycleDelegate {
object: nil)
}

public required init?(coder: NSCoder) {
preconditionFailure()
}

convenience init(bridge: RCTBridge) {
self.init(frame: .zero)
self.bridge = bridge
}

public override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
if (window != nil) {
Expand All @@ -84,6 +87,36 @@ open class CalloutView: RCTView, CalloutWindowLifeCycleDelegate {
}
}

// MARK: Fabric interface

@objc public var contentProxyView: NSView {
return proxyView
}

@objc public func setAnchorView(_ view: NSView?) {
anchorView = view
updateCalloutFrameToAnchor()
}

@objc public func mountContentSubview(_ subview: NSView, at index: Int) {
if index >= proxyView.subviews.count {
proxyView.addSubview(subview)
} else {
proxyView.addSubview(subview, positioned: .below, relativeTo: proxyView.subviews[index])
}
}

@objc public func unmountContentSubview(_ subview: NSView) {
if subview.superview == proxyView {
subview.removeFromSuperview()
}
}

@objc public func updateContentSize(_ size: NSSize) {
proxyView.frame = NSRect(origin: .zero, size: size)
updateCalloutFrameToAnchor()
}

// MARK: RCTComponent Overrides

public override func insertReactSubview(_ subview: NSView!, at atIndex: Int) {
Expand Down Expand Up @@ -362,10 +395,7 @@ open class CalloutView: RCTView, CalloutWindowLifeCycleDelegate {

private func onDismissCallout() {
if let onDismiss = onDismiss {
guard let reactTag = reactTag else {
preconditionFailure("React Tag missing")
}
let event: [AnyHashable: Any] = ["target": reactTag]
let event: [AnyHashable: Any] = ["target": reactTag ?? NSNumber(value: 0)]
onDismiss(event)
}
}
Expand Down Expand Up @@ -393,10 +423,12 @@ open class CalloutView: RCTView, CalloutWindowLifeCycleDelegate {
* We can't directly call touchHandler.attach(to:) because `visualEffectView` is not an RCTUIView.
* We get around this limitation by just replicating what `attach` did internally: add a gestureRecognizer.
*/
guard let touchHandler = RCTTouchHandler(bridge: bridge) else {
preconditionFailure("Callout could not create RCTTouchHandler")
if let bridge = bridge {
guard let touchHandler = RCTTouchHandler(bridge: bridge) else {
preconditionFailure("Callout could not create RCTTouchHandler")
}
visualEffectView.addGestureRecognizer(touchHandler)
}
visualEffectView.addGestureRecognizer(touchHandler)

return visualEffectView
}()
Expand Down
1 change: 0 additions & 1 deletion packages/components/Callout/macos/FRNCalloutManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ + (NSRect)screenRect:(id)json
@"rightCenter": @(NSRectEdgeMaxX),
@"rightBottomEdge": @(NSRectEdgeMaxX),
@"bottomLeftEdge": @(NSRectEdgeMinY),
@"bottonLeftEdge": @(NSRectEdgeMinY), // Add both the correct and incorrect spellings during the transition period
@"bottomAutoEdge": @(NSRectEdgeMinY),
@"bottomCenter": @(NSRectEdgeMinY),
@"bottomRightEdge": @(NSRectEdgeMinY),
Expand Down
12 changes: 12 additions & 0 deletions packages/components/Callout/macos/RCTCalloutComponentView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#ifdef RCT_NEW_ARCH_ENABLED

#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTCalloutComponentView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END

#endif
Loading
Loading