fix(ios): stabilize keyboard toolbar, image picker, and scrolling (#15182)

## Summary
- keep the editor active when iOS keyboard toolbar interactions move
focus into range-sync excluded widgets
- use the native iOS image picker/source sheet and sync native
presentation with the app theme
- pin `ListViewKit` to `1.1.6` so the iOS workspace resolves with Xcode
16.3
- restore vertical scrolling in the iOS `WKWebView` by removing the
global `contentOffset` reset while preserving zoom prevention

## Test plan
- [x] `yarn vitest --run --config \"vitest.config.ts\"
--browser.enabled=false \"src/__tests__/inline/active.unit.spec.ts\"`
- [x] `LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 xcodebuild
-resolvePackageDependencies -workspace
\"packages/frontend/apps/ios/App/App.xcworkspace\" -scheme \"App\"`
- [x] `xcodebuild -workspace \"App.xcworkspace\" -scheme \"App\"
-destination \"generic/platform=iOS Simulator\" build
CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES ARCHS=arm64`
- [x] Xcode build validation for the updated PR branch
This commit is contained in:
keepClamDown
2026-07-09 11:11:55 +08:00
committed by GitHub
parent 9f8e5c0eb3
commit a868f54eeb
19 changed files with 1095 additions and 28 deletions
@@ -0,0 +1,79 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
getImageFilesFromLocal,
registerNativeImageFilesPicker,
} from '../../utils';
describe('getImageFilesFromLocal', () => {
let originalShowPicker: ((this: HTMLInputElement) => void) | undefined;
beforeEach(() => {
originalShowPicker = (
HTMLInputElement.prototype as HTMLInputElement & {
showPicker?: (this: HTMLInputElement) => void;
}
).showPicker;
(
HTMLInputElement.prototype as HTMLInputElement & {
showPicker?: (this: HTMLInputElement) => void;
}
).showPicker = vi.fn(function (this: HTMLInputElement) {
this.dispatchEvent(new Event('cancel'));
});
});
afterEach(() => {
if (originalShowPicker) {
(
HTMLInputElement.prototype as HTMLInputElement & {
showPicker?: (this: HTMLInputElement) => void;
}
).showPicker = originalShowPicker;
} else {
delete (
HTMLInputElement.prototype as unknown as {
showPicker?: unknown;
}
).showPicker;
}
registerNativeImageFilesPicker(null);
vi.restoreAllMocks();
});
it('prefers the registered native image picker when available', async () => {
const file = new File(['picked-image'], 'picked.png', {
type: 'image/png',
lastModified: 123,
});
const nativePicker = vi.fn(async () => [file]);
registerNativeImageFilesPicker(nativePicker);
const files = await getImageFilesFromLocal();
expect(nativePicker).toHaveBeenCalledTimes(1);
expect(files).toEqual([file]);
});
it('falls back to the standard picker when the native picker rejects', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {});
const nativePicker = vi.fn(async () => {
throw new Error('Native picker failed');
});
registerNativeImageFilesPicker(nativePicker);
const files = await getImageFilesFromLocal();
expect(nativePicker).toHaveBeenCalledTimes(1);
expect(files).toEqual([]);
expect(
(
HTMLInputElement.prototype as HTMLInputElement & {
showPicker?: (this: HTMLInputElement) => void;
}
).showPicker
).toHaveBeenCalledTimes(1);
});
});