@erkamyaman/capacitor-foldable 8.3.1 → 8.3.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.
- package/README.md +10 -4
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +3 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ionic-tabs.css +2 -2
- package/ios/Sources/FoldablePlugin/Foldable.swift +21 -2
- package/ios/Sources/FoldablePlugin/FoldablePlugin.swift +4 -1
- package/package.json +1 -1
- package/skills/capacitor-foldable/SKILL.md +1 -1
- package/skills/capacitor-foldable/references/api.md +1 -1
- package/skills/capacitor-foldable/references/testing.md +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ More in [Examples](docs/examples.md).
|
|
|
80
80
|
|
|
81
81
|
## Web standard APIs
|
|
82
82
|
|
|
83
|
-
Chrome ships the [Device Posture API](https://developer.mozilla.org/docs/Web/API/Device_Posture_API) and the [Viewport Segments API](https://developer.mozilla.org/docs/Web/API/Viewport_Segments_API), but Android's WebView turns both off
|
|
83
|
+
Chrome ships the [Device Posture API](https://developer.mozilla.org/docs/Web/API/Device_Posture_API) and the [Viewport Segments API](https://developer.mozilla.org/docs/Web/API/Viewport_Segments_API), but Android's WebView turns both off. Safari 27.1 has them behind a feature flag that is off by default and belongs to Safari, so a `WKWebView` inside your app cannot switch it on. Neither works in a Capacitor app today. `installFoldablePolyfill()` fills them in from the native fold state, and mirrors the CSS features as classes and variables on `<html>`:
|
|
84
84
|
|
|
85
85
|
```typescript
|
|
86
86
|
import { installFoldablePolyfill } from '@erkamyaman/capacitor-foldable';
|
|
@@ -572,9 +572,9 @@ Only fires on iOS 27.1 or later.
|
|
|
572
572
|
|
|
573
573
|
#### BarPlacement
|
|
574
574
|
|
|
575
|
-
| Prop | Type | Description
|
|
576
|
-
| --------------------- | -------------------------------------------- |
|
|
577
|
-
| **`verticalBarEdge`** | <code>'leading' \| 'trailing' \| null</code> | The edge iPhone Duo moves tab bars and toolbars to when it lays them out vertically: `'leading'` or `'trailing'` in the reading direction, so `'leading'` is the left edge in left-to-right languages. `null` when bars stay horizontal, and always on Android, web and iOS before 27.1. | 7.0.0 |
|
|
575
|
+
| Prop | Type | Description | Since |
|
|
576
|
+
| --------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
|
577
|
+
| **`verticalBarEdge`** | <code>'leading' \| 'trailing' \| null</code> | The edge iPhone Duo moves tab bars and toolbars to when it lays them out vertically: `'leading'` or `'trailing'` in the reading direction, so `'leading'` is the left edge in left-to-right languages. `null` when bars stay horizontal, and always on Android, web and iOS before 27.1. An app built with an older Xcode still gets the edge: it is inferred from the safe-area inset the bar leaves, since the trait that names it is missing. | 7.0.0 |
|
|
578
578
|
|
|
579
579
|
|
|
580
580
|
#### PluginListenerHandle
|
|
@@ -600,3 +600,9 @@ angle: iOS keeps reporting `'closed'` for a moment after the phone opens.
|
|
|
600
600
|
<code>'unsupported' | 'unavailable' | 'available' | 'active'</code>
|
|
601
601
|
|
|
602
602
|
</docgen-api>
|
|
603
|
+
|
|
604
|
+
## Trademarks and licence
|
|
605
|
+
|
|
606
|
+
This plugin is MIT licensed, and that covers the work in this repository. Quoted passages from Apple's and Google's documentation belong to them and are used here to explain their guidance.
|
|
607
|
+
|
|
608
|
+
iPhone and iPhone Duo are trademarks of Apple Inc., registered in the U.S. and other countries. Android and Pixel are trademarks of Google LLC. Galaxy Z Fold and Galaxy Z Flip are trademarks of Samsung Electronics Co., Ltd. This project is not affiliated with, endorsed by, or sponsored by any of them; the names appear only to say which devices the plugin works with.
|
package/dist/docs.json
CHANGED
|
@@ -684,7 +684,7 @@
|
|
|
684
684
|
"name": "since"
|
|
685
685
|
}
|
|
686
686
|
],
|
|
687
|
-
"docs": "The edge iPhone Duo moves tab bars and toolbars to when it lays them out\nvertically: `'leading'` or `'trailing'` in the reading direction, so\n`'leading'` is the left edge in left-to-right languages. `null` when bars\nstay horizontal, and always on Android, web and iOS before 27.1.",
|
|
687
|
+
"docs": "The edge iPhone Duo moves tab bars and toolbars to when it lays them out\nvertically: `'leading'` or `'trailing'` in the reading direction, so\n`'leading'` is the left edge in left-to-right languages. `null` when bars\nstay horizontal, and always on Android, web and iOS before 27.1. An app\nbuilt with an older Xcode still gets the edge: it is inferred from the\nsafe-area inset the bar leaves, since the trait that names it is missing.",
|
|
688
688
|
"complexTypes": [],
|
|
689
689
|
"type": "'leading' | 'trailing' | null"
|
|
690
690
|
}
|
|
@@ -183,7 +183,9 @@ export interface BarPlacement {
|
|
|
183
183
|
* The edge iPhone Duo moves tab bars and toolbars to when it lays them out
|
|
184
184
|
* vertically: `'leading'` or `'trailing'` in the reading direction, so
|
|
185
185
|
* `'leading'` is the left edge in left-to-right languages. `null` when bars
|
|
186
|
-
* stay horizontal, and always on Android, web and iOS before 27.1.
|
|
186
|
+
* stay horizontal, and always on Android, web and iOS before 27.1. An app
|
|
187
|
+
* built with an older Xcode still gets the edge: it is inferred from the
|
|
188
|
+
* safe-area inset the bar leaves, since the trait that names it is missing.
|
|
187
189
|
*
|
|
188
190
|
* @since 7.0.0
|
|
189
191
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface FoldState {\n /**\n * Posture of the fold. `'closed'` is never reported today: a device shut on\n * its cover display reports `'flat'`.\n *\n * @since 7.0.0\n */\n state: 'flat' | 'half-opened' | 'closed';\n\n /**\n * Whether the fold splits the web view into two areas: `true` when\n * half-opened, or when the hinge has a physical gap.\n *\n * @since 7.0.0\n */\n isSeparating: boolean;\n\n /**\n * How the device is held: `'tabletop'` when half-opened with a horizontal\n * hinge, like a laptop, `'book'` when half-opened with a vertical hinge, and\n * `'flat'` otherwise.\n *\n * @since 7.0.0\n */\n posture: 'flat' | 'tabletop' | 'book';\n\n /**\n * Direction of the hinge relative to the window, so it flips when the device\n * rotates. Omitted when there is no fold.\n *\n * @since 7.0.0\n */\n hingeOrientation?: 'horizontal' | 'vertical';\n\n /**\n * Position of the fold in CSS pixels, relative to the web view. Zero wide (or\n * zero tall) on a seamless fold. Omitted when there is no fold.\n *\n * @since 7.0.0\n */\n hingeBounds?: { x: number; y: number; width: number; height: number };\n\n /**\n * Area of the web view the hinge covers, in CSS pixels. Only present on\n * devices with a physical gap.\n *\n * @since 7.0.0\n */\n occludedBounds?: { x: number; y: number; width: number; height: number };\n\n /**\n * Which display is showing the app, on a device with an inner and an outer\n * display such as iPhone Duo. Reported on iPhone Duo (iOS 27.1 or later) and\n * omitted elsewhere.\n *\n * @since 7.0.0\n */\n activeDisplay?: 'inner' | 'outer';\n\n /**\n * Areas of the web view something covers, in CSS pixels. On Android these are\n * the display cutouts. On iPhone Duo these are the active occlusion regions:\n * the camera in use, and the strip the system keeps for the vertical status\n * bar. Use `getReservedRegions()` when you need to tell them apart. Omitted\n * when there are none.\n *\n * @since 7.0.0\n */\n cameraBounds?: { x: number; y: number; width: number; height: number }[];\n\n /**\n * Space the system asks you to keep clear around the fold, in CSS pixels.\n * `hingeBounds` covers the crease plus these margins, so the crease itself is\n * `hingeBounds` shrunk by them. iPhone Duo reports 20 on each side of a\n * vertical fold. Omitted on Android and where there is no fold.\n *\n * @since 8.2.0\n */\n hingeMargins?: { top: number; right: number; bottom: number; left: number };\n}\n\nexport interface ReservedRegion {\n /**\n * `'division'` is the fold itself. `'occlusion'` is something covering the\n * display, such as a camera or the vertical status bar area.\n *\n * @since 8.2.0\n */\n kind: 'division' | 'occlusion';\n\n /**\n * Whether the region applies right now. An inactive division is a flat fold,\n * and an inactive occlusion is a camera that is not in use.\n *\n * @since 8.2.0\n */\n isActive: boolean;\n\n x: number;\n y: number;\n width: number;\n height: number;\n\n /**\n * Space to keep clear around the region, in CSS pixels.\n *\n * @since 8.2.0\n */\n margins: { top: number; right: number; bottom: number; left: number };\n}\n\n/**\n * How far open the hinge is, as the system sees it. Note that it can lag the\n * angle: iOS keeps reporting `'closed'` for a moment after the phone opens.\n *\n * @since 8.2.0\n */\nexport type HingeStatus = 'closed' | 'partiallyOpen' | 'fullyOpen';\n\nexport interface SizeClass {\n /**\n * Width size class of the window: `'compact'` on a phone and on the outer\n * display of a foldable, `'regular'` on the inner display, tablets and wide\n * windows. On Android and web `'regular'` starts at 600 CSS pixels.\n *\n * @since 7.0.0\n */\n horizontal: 'compact' | 'regular';\n\n /**\n * Height size class of the window: `'compact'` on a phone in landscape. On\n * Android and web `'regular'` starts at 480 CSS pixels.\n *\n * @since 7.0.0\n */\n vertical: 'compact' | 'regular';\n\n /**\n * Material window width class, from the window width in CSS pixels:\n * `'compact'` below 600, `'medium'` below 840, `'expanded'` below 1200,\n * `'large'` below 1600 and `'extraLarge'` from 1600.\n *\n * @since 7.0.0\n */\n widthClass: 'compact' | 'medium' | 'expanded' | 'large' | 'extraLarge';\n\n /**\n * Material window height class, from the window height in CSS pixels:\n * `'compact'` below 480, `'medium'` below 900 and `'expanded'` from 900.\n *\n * @since 7.0.0\n */\n heightClass: 'compact' | 'medium' | 'expanded';\n}\n\nexport type DisplayModeStatus = 'unsupported' | 'unavailable' | 'available' | 'active';\n\nexport interface DisplayModes {\n /**\n * Rear display mode moves the app to the outer display, so people can frame a\n * photo with the rear cameras. Only on Android foldables that offer it.\n *\n * @since 7.0.0\n */\n rearDisplay: DisplayModeStatus;\n\n /**\n * Dual-screen mode shows a second page on the outer display while the app\n * stays on the inner one. Only on Android foldables that offer it.\n *\n * @since 7.0.0\n */\n dualScreen: DisplayModeStatus;\n}\n\nexport interface BarPlacement {\n /**\n * The edge iPhone Duo moves tab bars and toolbars to when it lays them out\n * vertically: `'leading'` or `'trailing'` in the reading direction, so\n * `'leading'` is the left edge in left-to-right languages. `null` when bars\n * stay horizontal, and always on Android, web and iOS before 27.1.\n *\n * @since 7.0.0\n */\n verticalBarEdge: 'leading' | 'trailing' | null;\n}\n\nexport interface FoldablePolyfillOptions {\n /**\n * Take over hiding Ionic's tab bar while the keyboard is open. iPhone Duo\n * reports a keyboard on every fold, and Ionic waits for the window to return\n * to the height it had when the keyboard first opened, so its tab bar can\n * disappear until the device is folded back. With this on, the keyboard events\n * never reach Ionic, and a `foldable-keyboard-open` class on `<html>` marks\n * when a text field really has the keyboard. `ionic-tabs.css` hides the tab\n * bar with it.\n *\n * @default false\n * @since 8.0.2\n */\n ionicKeyboard?: boolean;\n}\n\nexport interface FoldablePlugin {\n /**\n * Whether the device has a fold at all, and whether it can be propped half\n * open like a laptop. Both `false` on web, and on iOS except on iPhone Duo\n * (iOS 27.1 or later).\n *\n * @since 7.0.0\n */\n isDeviceFoldable(): Promise<{ foldable: boolean; supportsTabletop: boolean }>;\n\n /**\n * Read the current fold state. Resolves to\n * `{ state: 'flat', isSeparating: false, posture: 'flat' }` when there is no\n * fold information.\n *\n * @since 7.0.0\n */\n getFoldState(): Promise<FoldState>;\n\n /**\n * Read the angle between the two halves of the device, in degrees: `0` when\n * closed, `180` when flat. Resolves to `{ angle: null }` on devices without a\n * hinge angle sensor, on web, and on iOS before 27.1.\n *\n * @since 7.0.0\n */\n getHingeAngle(): Promise<{ angle: number | null; status: HingeStatus | null }>;\n\n /**\n * Read every region the system reserves on this display: the fold and\n * anything covering the screen, active or not. iPhone Duo reports the fold,\n * the vertical status bar area and the under-display camera. Resolves to an\n * empty list on Android, on web and on iOS before 27.1.\n *\n * @since 8.2.0\n */\n getReservedRegions(): Promise<{ regions: ReservedRegion[] }>;\n\n /**\n * Choose whether iPhone Duo may move this app's bars to the side of the\n * display. `'disabled'` keeps everything horizontal, including the status\n * bar. Resolves to `{ applied: false }` when the app does not use\n * `FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.\n * See the iPhone Duo guide for the one-line storyboard change.\n *\n * @since 8.2.0\n */\n setVerticalBarBehavior(options: { behavior: 'automatic' | 'disabled' }): Promise<{ applied: boolean }>;\n\n /**\n * Read the window's size classes: Apple's compact and regular, the signal its\n * iPhone Duo guidelines recommend for telling the outer display from the inner\n * one, and Material's width and height classes. On iOS `horizontal` and\n * `vertical` are UIKit's size classes; everything else comes from the window\n * size.\n *\n * @since 7.0.0\n */\n getSizeClass(): Promise<SizeClass>;\n\n /**\n * Read which of the foldable display modes the device offers right now. Both\n * are `'unsupported'` on iOS and web.\n *\n * @since 7.0.0\n */\n getDisplayModes(): Promise<DisplayModes>;\n\n /**\n * Read where native bars go. Native tab bars and toolbars move to the side of\n * the display on iPhone Duo, but HTML ones stay put, so use this to move your\n * own tab bar too.\n *\n * @since 7.0.0\n */\n getBarPlacement(): Promise<BarPlacement>;\n\n /**\n * Move the app to the outer display. Android asks the user to confirm first,\n * and the promise resolves once the app has moved. Rejects when rear display\n * mode is not `'available'`. Only on Android.\n *\n * @since 7.0.0\n */\n startRearDisplay(): Promise<void>;\n\n /**\n * Move the app back to the inner display.\n *\n * @since 7.0.0\n */\n stopRearDisplay(): Promise<void>;\n\n /**\n * Show a page on the outer display while the app stays on the inner one. A\n * relative `url` resolves against the app's own URL, so `'cover.html'` loads a\n * page bundled with the app. The page runs in its own web view without access\n * to Capacitor plugins. Calling this again while dual-screen mode is active\n * replaces the page. Only on Android.\n *\n * @since 7.0.0\n */\n startDualScreen(options: { url: string }): Promise<void>;\n\n /**\n * Close the page on the outer display.\n *\n * @since 7.0.0\n */\n stopDualScreen(): Promise<void>;\n\n /**\n * Listen for fold state changes. On a foldable this also fires when the\n * device rotates, because `hingeOrientation` and `hingeBounds` rotate with\n * the window.\n *\n * @since 7.0.0\n */\n addListener(eventName: 'foldStateChange', listenerFunc: (state: FoldState) => void): Promise<PluginListenerHandle>;\n\n /**\n * Listen for hinge angle changes. On Android the hinge sensor only runs while\n * at least one of these listeners is registered. On iOS it fires on iPhone\n * Duo (iOS 27.1 or later). Never fires on web.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'hingeAngleChange',\n listenerFunc: (event: { angle: number }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for size class changes, such as unfolding the device, rotating it or\n * resizing the window. On iOS this fires when UIKit's size classes change\n * (iOS 17 or later) or the device rotates, so a resize that keeps the same\n * size classes may not update `widthClass` and `heightClass` until then.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'sizeClassChange',\n listenerFunc: (sizeClass: SizeClass) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for changes to the display modes, including a mode ending because\n * the user folded or unfolded the device. Never fires on iOS and web.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'displayModeChange',\n listenerFunc: (modes: DisplayModes) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for the device being folded or unfolded. `folding` turns `true` as\n * soon as the hinge starts moving and `false` about half a second after it\n * stops, so an app can pause animations or heavy work while the screen is in\n * motion. Only fires where a hinge angle is available: Android foldables with\n * a hinge sensor, and iPhone Duo on iOS 27.1 or later.\n *\n * @since 8.2.0\n */\n addListener(\n eventName: 'foldingChange',\n listenerFunc: (event: { folding: boolean }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for bar placement changes, such as opening or rotating iPhone Duo.\n * Only fires on iOS 27.1 or later.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'barPlacementChange',\n listenerFunc: (placement: BarPlacement) => void,\n ): Promise<PluginListenerHandle>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface FoldState {\n /**\n * Posture of the fold. `'closed'` is never reported today: a device shut on\n * its cover display reports `'flat'`.\n *\n * @since 7.0.0\n */\n state: 'flat' | 'half-opened' | 'closed';\n\n /**\n * Whether the fold splits the web view into two areas: `true` when\n * half-opened, or when the hinge has a physical gap.\n *\n * @since 7.0.0\n */\n isSeparating: boolean;\n\n /**\n * How the device is held: `'tabletop'` when half-opened with a horizontal\n * hinge, like a laptop, `'book'` when half-opened with a vertical hinge, and\n * `'flat'` otherwise.\n *\n * @since 7.0.0\n */\n posture: 'flat' | 'tabletop' | 'book';\n\n /**\n * Direction of the hinge relative to the window, so it flips when the device\n * rotates. Omitted when there is no fold.\n *\n * @since 7.0.0\n */\n hingeOrientation?: 'horizontal' | 'vertical';\n\n /**\n * Position of the fold in CSS pixels, relative to the web view. Zero wide (or\n * zero tall) on a seamless fold. Omitted when there is no fold.\n *\n * @since 7.0.0\n */\n hingeBounds?: { x: number; y: number; width: number; height: number };\n\n /**\n * Area of the web view the hinge covers, in CSS pixels. Only present on\n * devices with a physical gap.\n *\n * @since 7.0.0\n */\n occludedBounds?: { x: number; y: number; width: number; height: number };\n\n /**\n * Which display is showing the app, on a device with an inner and an outer\n * display such as iPhone Duo. Reported on iPhone Duo (iOS 27.1 or later) and\n * omitted elsewhere.\n *\n * @since 7.0.0\n */\n activeDisplay?: 'inner' | 'outer';\n\n /**\n * Areas of the web view something covers, in CSS pixels. On Android these are\n * the display cutouts. On iPhone Duo these are the active occlusion regions:\n * the camera in use, and the strip the system keeps for the vertical status\n * bar. Use `getReservedRegions()` when you need to tell them apart. Omitted\n * when there are none.\n *\n * @since 7.0.0\n */\n cameraBounds?: { x: number; y: number; width: number; height: number }[];\n\n /**\n * Space the system asks you to keep clear around the fold, in CSS pixels.\n * `hingeBounds` covers the crease plus these margins, so the crease itself is\n * `hingeBounds` shrunk by them. iPhone Duo reports 20 on each side of a\n * vertical fold. Omitted on Android and where there is no fold.\n *\n * @since 8.2.0\n */\n hingeMargins?: { top: number; right: number; bottom: number; left: number };\n}\n\nexport interface ReservedRegion {\n /**\n * `'division'` is the fold itself. `'occlusion'` is something covering the\n * display, such as a camera or the vertical status bar area.\n *\n * @since 8.2.0\n */\n kind: 'division' | 'occlusion';\n\n /**\n * Whether the region applies right now. An inactive division is a flat fold,\n * and an inactive occlusion is a camera that is not in use.\n *\n * @since 8.2.0\n */\n isActive: boolean;\n\n x: number;\n y: number;\n width: number;\n height: number;\n\n /**\n * Space to keep clear around the region, in CSS pixels.\n *\n * @since 8.2.0\n */\n margins: { top: number; right: number; bottom: number; left: number };\n}\n\n/**\n * How far open the hinge is, as the system sees it. Note that it can lag the\n * angle: iOS keeps reporting `'closed'` for a moment after the phone opens.\n *\n * @since 8.2.0\n */\nexport type HingeStatus = 'closed' | 'partiallyOpen' | 'fullyOpen';\n\nexport interface SizeClass {\n /**\n * Width size class of the window: `'compact'` on a phone and on the outer\n * display of a foldable, `'regular'` on the inner display, tablets and wide\n * windows. On Android and web `'regular'` starts at 600 CSS pixels.\n *\n * @since 7.0.0\n */\n horizontal: 'compact' | 'regular';\n\n /**\n * Height size class of the window: `'compact'` on a phone in landscape. On\n * Android and web `'regular'` starts at 480 CSS pixels.\n *\n * @since 7.0.0\n */\n vertical: 'compact' | 'regular';\n\n /**\n * Material window width class, from the window width in CSS pixels:\n * `'compact'` below 600, `'medium'` below 840, `'expanded'` below 1200,\n * `'large'` below 1600 and `'extraLarge'` from 1600.\n *\n * @since 7.0.0\n */\n widthClass: 'compact' | 'medium' | 'expanded' | 'large' | 'extraLarge';\n\n /**\n * Material window height class, from the window height in CSS pixels:\n * `'compact'` below 480, `'medium'` below 900 and `'expanded'` from 900.\n *\n * @since 7.0.0\n */\n heightClass: 'compact' | 'medium' | 'expanded';\n}\n\nexport type DisplayModeStatus = 'unsupported' | 'unavailable' | 'available' | 'active';\n\nexport interface DisplayModes {\n /**\n * Rear display mode moves the app to the outer display, so people can frame a\n * photo with the rear cameras. Only on Android foldables that offer it.\n *\n * @since 7.0.0\n */\n rearDisplay: DisplayModeStatus;\n\n /**\n * Dual-screen mode shows a second page on the outer display while the app\n * stays on the inner one. Only on Android foldables that offer it.\n *\n * @since 7.0.0\n */\n dualScreen: DisplayModeStatus;\n}\n\nexport interface BarPlacement {\n /**\n * The edge iPhone Duo moves tab bars and toolbars to when it lays them out\n * vertically: `'leading'` or `'trailing'` in the reading direction, so\n * `'leading'` is the left edge in left-to-right languages. `null` when bars\n * stay horizontal, and always on Android, web and iOS before 27.1. An app\n * built with an older Xcode still gets the edge: it is inferred from the\n * safe-area inset the bar leaves, since the trait that names it is missing.\n *\n * @since 7.0.0\n */\n verticalBarEdge: 'leading' | 'trailing' | null;\n}\n\nexport interface FoldablePolyfillOptions {\n /**\n * Take over hiding Ionic's tab bar while the keyboard is open. iPhone Duo\n * reports a keyboard on every fold, and Ionic waits for the window to return\n * to the height it had when the keyboard first opened, so its tab bar can\n * disappear until the device is folded back. With this on, the keyboard events\n * never reach Ionic, and a `foldable-keyboard-open` class on `<html>` marks\n * when a text field really has the keyboard. `ionic-tabs.css` hides the tab\n * bar with it.\n *\n * @default false\n * @since 8.0.2\n */\n ionicKeyboard?: boolean;\n}\n\nexport interface FoldablePlugin {\n /**\n * Whether the device has a fold at all, and whether it can be propped half\n * open like a laptop. Both `false` on web, and on iOS except on iPhone Duo\n * (iOS 27.1 or later).\n *\n * @since 7.0.0\n */\n isDeviceFoldable(): Promise<{ foldable: boolean; supportsTabletop: boolean }>;\n\n /**\n * Read the current fold state. Resolves to\n * `{ state: 'flat', isSeparating: false, posture: 'flat' }` when there is no\n * fold information.\n *\n * @since 7.0.0\n */\n getFoldState(): Promise<FoldState>;\n\n /**\n * Read the angle between the two halves of the device, in degrees: `0` when\n * closed, `180` when flat. Resolves to `{ angle: null }` on devices without a\n * hinge angle sensor, on web, and on iOS before 27.1.\n *\n * @since 7.0.0\n */\n getHingeAngle(): Promise<{ angle: number | null; status: HingeStatus | null }>;\n\n /**\n * Read every region the system reserves on this display: the fold and\n * anything covering the screen, active or not. iPhone Duo reports the fold,\n * the vertical status bar area and the under-display camera. Resolves to an\n * empty list on Android, on web and on iOS before 27.1.\n *\n * @since 8.2.0\n */\n getReservedRegions(): Promise<{ regions: ReservedRegion[] }>;\n\n /**\n * Choose whether iPhone Duo may move this app's bars to the side of the\n * display. `'disabled'` keeps everything horizontal, including the status\n * bar. Resolves to `{ applied: false }` when the app does not use\n * `FoldableBridgeViewController`, on Android, on web and on iOS before 27.1.\n * See the iPhone Duo guide for the one-line storyboard change.\n *\n * @since 8.2.0\n */\n setVerticalBarBehavior(options: { behavior: 'automatic' | 'disabled' }): Promise<{ applied: boolean }>;\n\n /**\n * Read the window's size classes: Apple's compact and regular, the signal its\n * iPhone Duo guidelines recommend for telling the outer display from the inner\n * one, and Material's width and height classes. On iOS `horizontal` and\n * `vertical` are UIKit's size classes; everything else comes from the window\n * size.\n *\n * @since 7.0.0\n */\n getSizeClass(): Promise<SizeClass>;\n\n /**\n * Read which of the foldable display modes the device offers right now. Both\n * are `'unsupported'` on iOS and web.\n *\n * @since 7.0.0\n */\n getDisplayModes(): Promise<DisplayModes>;\n\n /**\n * Read where native bars go. Native tab bars and toolbars move to the side of\n * the display on iPhone Duo, but HTML ones stay put, so use this to move your\n * own tab bar too.\n *\n * @since 7.0.0\n */\n getBarPlacement(): Promise<BarPlacement>;\n\n /**\n * Move the app to the outer display. Android asks the user to confirm first,\n * and the promise resolves once the app has moved. Rejects when rear display\n * mode is not `'available'`. Only on Android.\n *\n * @since 7.0.0\n */\n startRearDisplay(): Promise<void>;\n\n /**\n * Move the app back to the inner display.\n *\n * @since 7.0.0\n */\n stopRearDisplay(): Promise<void>;\n\n /**\n * Show a page on the outer display while the app stays on the inner one. A\n * relative `url` resolves against the app's own URL, so `'cover.html'` loads a\n * page bundled with the app. The page runs in its own web view without access\n * to Capacitor plugins. Calling this again while dual-screen mode is active\n * replaces the page. Only on Android.\n *\n * @since 7.0.0\n */\n startDualScreen(options: { url: string }): Promise<void>;\n\n /**\n * Close the page on the outer display.\n *\n * @since 7.0.0\n */\n stopDualScreen(): Promise<void>;\n\n /**\n * Listen for fold state changes. On a foldable this also fires when the\n * device rotates, because `hingeOrientation` and `hingeBounds` rotate with\n * the window.\n *\n * @since 7.0.0\n */\n addListener(eventName: 'foldStateChange', listenerFunc: (state: FoldState) => void): Promise<PluginListenerHandle>;\n\n /**\n * Listen for hinge angle changes. On Android the hinge sensor only runs while\n * at least one of these listeners is registered. On iOS it fires on iPhone\n * Duo (iOS 27.1 or later). Never fires on web.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'hingeAngleChange',\n listenerFunc: (event: { angle: number }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for size class changes, such as unfolding the device, rotating it or\n * resizing the window. On iOS this fires when UIKit's size classes change\n * (iOS 17 or later) or the device rotates, so a resize that keeps the same\n * size classes may not update `widthClass` and `heightClass` until then.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'sizeClassChange',\n listenerFunc: (sizeClass: SizeClass) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for changes to the display modes, including a mode ending because\n * the user folded or unfolded the device. Never fires on iOS and web.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'displayModeChange',\n listenerFunc: (modes: DisplayModes) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for the device being folded or unfolded. `folding` turns `true` as\n * soon as the hinge starts moving and `false` about half a second after it\n * stops, so an app can pause animations or heavy work while the screen is in\n * motion. Only fires where a hinge angle is available: Android foldables with\n * a hinge sensor, and iPhone Duo on iOS 27.1 or later.\n *\n * @since 8.2.0\n */\n addListener(\n eventName: 'foldingChange',\n listenerFunc: (event: { folding: boolean }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for bar placement changes, such as opening or rotating iPhone Duo.\n * Only fires on iOS 27.1 or later.\n *\n * @since 7.0.0\n */\n addListener(\n eventName: 'barPlacementChange',\n listenerFunc: (placement: BarPlacement) => void,\n ): Promise<PluginListenerHandle>;\n}\n"]}
|
package/ionic-tabs.css
CHANGED
|
@@ -127,8 +127,9 @@ struct UnsupportedFoldProvider: FoldProvider {
|
|
|
127
127
|
]
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
@objc public func barPlacement(in traits: UITraitCollection) -> [String: Any] {
|
|
131
|
-
|
|
130
|
+
@objc public func barPlacement(in traits: UITraitCollection, view: UIView? = nil) -> [String: Any] {
|
|
131
|
+
let edge = verticalBarEdge(of: traits) ?? view.flatMap(verticalBarEdge(inferredFrom:))
|
|
132
|
+
return ["verticalBarEdge": edge.map { $0 as Any } ?? NSNull()]
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
private func verticalBarEdge(of traits: UITraitCollection) -> String? {
|
|
@@ -144,6 +145,24 @@ struct UnsupportedFoldProvider: FoldProvider {
|
|
|
144
145
|
return nil
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
private func verticalBarEdge(inferredFrom view: UIView) -> String? {
|
|
149
|
+
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
150
|
+
return nil
|
|
151
|
+
#else
|
|
152
|
+
guard #available(iOS 27.1, *) else { return nil }
|
|
153
|
+
|
|
154
|
+
view.layoutIfNeeded()
|
|
155
|
+
let insets = view.safeAreaInsets
|
|
156
|
+
let rightToLeft = view.effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
157
|
+
|
|
158
|
+
if insets.right >= barInsetThreshold { return rightToLeft ? "leading" : "trailing" }
|
|
159
|
+
if insets.left >= barInsetThreshold { return rightToLeft ? "trailing" : "leading" }
|
|
160
|
+
return nil
|
|
161
|
+
#endif
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private let barInsetThreshold: CGFloat = 70
|
|
165
|
+
|
|
147
166
|
public func observeChanges(in view: UIView, onChange: @escaping () -> Void) {
|
|
148
167
|
provider.observe(view, onChange: onChange)
|
|
149
168
|
}
|
|
@@ -128,7 +128,10 @@ public class FoldablePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
private func currentBarPlacement() -> [String: Any] {
|
|
131
|
-
return implementation.barPlacement(
|
|
131
|
+
return implementation.barPlacement(
|
|
132
|
+
in: bridge?.viewController?.traitCollection ?? UITraitCollection.current,
|
|
133
|
+
view: bridge?.webView
|
|
134
|
+
)
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
private func currentSizeClass() -> [String: String] {
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ import { Foldable, installFoldablePolyfill } from '@erkamyaman/capacitor-foldabl
|
|
|
34
34
|
await installFoldablePolyfill();
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Whenever the device reports a fold, that sets `--fold-left`, `--fold-top`, `--fold-width`, `--fold-height` and `--fold-margin-*` on `<html>`, and adds `fold-vertical` or `fold-horizontal`. It also sets the `vertical-bars-*` classes the Ionic stylesheet needs, and fills in the standard Viewport Segments and Device Posture APIs,
|
|
37
|
+
Whenever the device reports a fold, that sets `--fold-left`, `--fold-top`, `--fold-width`, `--fold-height` and `--fold-margin-*` on `<html>`, and adds `fold-vertical` or `fold-horizontal`. It also sets the `vertical-bars-*` classes the Ionic stylesheet needs, and fills in the standard Viewport Segments and Device Posture APIs. Android's WebView turns both off, and Safari 27.1 has them behind an off-by-default flag that only Safari can set, so a Capacitor web view gets neither. The polyfill stands aside if it ever finds them already there.
|
|
38
38
|
|
|
39
39
|
Prefer the `--fold-*` variables over the polyfilled segment variables for layout: the segments only split when the fold actually separates the window, which on iPhone Duo means half open, while the fold variables are there when it is open flat too.
|
|
40
40
|
|
|
@@ -75,7 +75,7 @@ Apple's compact and regular plus Material's window classes. On iOS the first pai
|
|
|
75
75
|
const { verticalBarEdge } = await Foldable.getBarPlacement(); // 'leading' | 'trailing' | null
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
`null` means no vertical bar edge is reported: bars are horizontal, or the platform cannot say, which is always the case on Android, web and iOS before 27.1. On iPhone Duo the bar also arrives as a safe-area inset, so simple layouts need nothing beyond `env(safe-area-inset-*)`. Use this when positioning your own chrome to match. `barPlacementChange` fires when it moves.
|
|
78
|
+
`null` means no vertical bar edge is reported: bars are horizontal, or the platform cannot say, which is always the case on Android, web and iOS before 27.1. An app built with an Xcode older than 27.1 cannot read the trait that names the edge, so the plugin infers it from the safe-area inset the bar leaves. On iPhone Duo the bar also arrives as a safe-area inset, so simple layouts need nothing beyond `env(safe-area-inset-*)`. Use this when positioning your own chrome to match. `barPlacementChange` fires when it moves.
|
|
79
79
|
|
|
80
80
|
```typescript
|
|
81
81
|
await Foldable.setVerticalBarBehavior({ behavior: 'disabled' }); // everything horizontal
|
|
@@ -28,7 +28,7 @@ hinge -d <udid> get
|
|
|
28
28
|
|
|
29
29
|
Sweep rather than jumping: an instant change can confuse the simulator's display switching.
|
|
30
30
|
|
|
31
|
-
Known limitation:
|
|
31
|
+
Known limitation: agent and automation tooling does not handle the Duo's two panels yet, and several projects have open bugs about it ([XcodeBuildMCP #537](https://github.com/getsentry/XcodeBuildMCP/issues/537), [claude-code #96941](https://github.com/anthropics/claude-code/issues/96941)). Taps and key input do not land, and some tools keep reporting the closed panel's coordinate space while the phone is open, so scripted UI tests cannot drive it. Work from the terminal instead. Screenshots work from the terminal with an explicit `--display`; through tooling that cannot pass one, a folded screenshot comes back black because it always targets the inner panel.
|
|
32
32
|
|
|
33
33
|
## Android foldable emulator
|
|
34
34
|
|