文件
mynode/readme/dev/spec/modal_focus_management.md
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

72 行
4.6 KiB
Markdown

# Modal focus management
Most Joplin dialogs should follow the [modal dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/). On desktop, this is usually done with the native [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) element. On mobile, it's a bit more complicated.
## Mobile
### Managing focus
On mobile, the `<AccessibleView>` component allows moving focus to a component or preventing a component from being accessibility focused. For example,
```jsx
<AccessibleView inert={true}>{children}</AccessibleView>
```
prevents `children` from being focused using accessibility tools in a cross-platform way. The `inert` prop is named after the [HTML `inert` attribute](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert).
Similarly, the following logic auto-focuses `children` when the view first renders:
```jsx
// Danger: This implicitly sets `accessible={true}`, which prevents
// VoiceOver from focusing individual children in `children`.
<AccessibleView refocusCounter={1}>{children}</AccessibleView>
```
Changing the `refocusCounter` prop causes the `AccessibleView` to be focused again.
### Native `Modal`s
React Native has a built-in `Modal` component.
The `components/Modal` component wraps this built-in `Modal` component. Among other things, this wrapper tracks whether `Modal`s are open, closing, or closed. This allows greater customization over where focus moves after modals are dismissed.
When a `Modal` is visible, it prevents content behind it from being focused. With the React Native built-in `Modal`, setting focus to items behind a visible `Modal` does nothing. On Android, this is also the case briefly after the `Modal` is dismissed.
The custom `Modal` works with `AccessibleView` to improve focus behavior. The `Modal` keeps track of the last `AccessibleView` that was focused while the `Modal` was open. When the `Modal` is dismissed, it auto-focuses this `AccessibleView`. This is useful, for example, if an button in a `Modal` shows UI that needs to be auto-focused when the `Modal` is dismissed. The custom `Modal` determines when the native `Modal` is dismissed, and could then move focus to the just-shown UI.
### Inaccessible 3rd-party modals
Sometimes a library includes a component that should handle focus in a modal-like way, but doesn't. Examples include [`react-native-paper`'s Modal](https://github.com/callstack/react-native-paper/issues/3912) and [`react-native-popup-menu`'s Menu](https://github.com/instea/react-native-popup-menu/issues/138). The components in the `FocusControl` object can often improve focus management for these libraries.
`FocusControl` provides three components:
- A `FocusControl.Provider` that sets up shared focus-related state.
- A `FocusControl.MainAppContent` that should wrap the main application content (everything that isn't part of a modal).
- A `FocusControl.ModalWrapper` that should be used to wrap content within modals. This allows `FocusControl` to determine whether a modal is visible.
When a modal is visible, the `MainAppContent` is wrapped with an `inert` `AccessibleView`, preventing it from receiving accessibility focus. This traps focus within the visible modal components.
In general, prefer Joplin's `components/Modal` component to react-native-paper `Modal`s. As an example, however, a [`react-native-paper` `Modal`](https://callstack.github.io/react-native-paper/docs/components/Modal/) might be rendered with:
```tsx
<Portal>
<Modal
visible={visible}
onDismiss={onDismiss}
>
<FocusControl.ModalWrapper
state={visible ? ModalState.Open : ModalState.Closed}
>
{...content here...}
</FocusControl.ModalWrapper>
</Modal>
</Portal>
```
Above, the `FocusControl.ModalWrapper` communicates whether the dialog is visible to the global `<FocusControl.Provider>`. This allows the `MainAppContent` (not shown above) to be marked as focusable or unfocusable depending on whether the `Modal` is visible or not.
:::danger
The [`<Portal>`](https://callstack.github.io/react-native-paper/docs/components/Portal/) is important part of the example. A `<Portal>` is a react-native-paper component that renders its children outside of the main app content (near the global `PaperProvider`).
If the `<Portal>` is omitted, then the modal, and the `FocusControl.ModalWrapper`'s children, will be rendered within the main app content. This will cause them to be marked as unfocusable when the modal is visible, preventing screen readers from accessing the modal's content.
When adding a `FocusControl.ModalWrapper`, it's important to verify that the modal can still be used by a screen reader.
:::