feat: Initial clean release of CHM
一些检查失败了
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
一些检查失败了
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
这个提交包含在:
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
type AutoFocusCallback = ()=> void;
|
||||
|
||||
interface AutoFocusControl {
|
||||
// It isn't always possible to autofocus (e.g. due to a dialog obscuring focus).
|
||||
canAutoFocus(): boolean;
|
||||
|
||||
// Sets the callback to be triggered when it becomes possible to autofocus
|
||||
setAutofocusCallback(callback: AutoFocusCallback): void;
|
||||
removeAutofocusCallback(callback: AutoFocusCallback): void;
|
||||
}
|
||||
|
||||
export const AutoFocusContext = createContext<AutoFocusControl|null>(null);
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
allowAutoFocus: boolean;
|
||||
}
|
||||
|
||||
const AutoFocusProvider: React.FC<Props> = ({ allowAutoFocus, children }) => {
|
||||
const [autoFocusCallback, setAutofocusCallback] = useState<AutoFocusCallback|null>(null);
|
||||
const allowAutoFocusRef = useRef(allowAutoFocus);
|
||||
allowAutoFocusRef.current = allowAutoFocus;
|
||||
|
||||
useEffect(() => {
|
||||
if (allowAutoFocus && autoFocusCallback) {
|
||||
autoFocusCallback();
|
||||
setAutofocusCallback(null);
|
||||
}
|
||||
}, [autoFocusCallback, allowAutoFocus]);
|
||||
|
||||
const removeAutofocusCallback = useCallback((toRemove: AutoFocusCallback) => {
|
||||
setAutofocusCallback(callback => {
|
||||
// Update the callback only if it's different
|
||||
if (callback === toRemove) {
|
||||
return null;
|
||||
} else {
|
||||
return callback;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const autoFocusControl = useMemo((): AutoFocusControl => {
|
||||
return {
|
||||
canAutoFocus: () => {
|
||||
return allowAutoFocusRef.current;
|
||||
},
|
||||
setAutofocusCallback: (callback) => {
|
||||
setAutofocusCallback(() => callback);
|
||||
},
|
||||
removeAutofocusCallback,
|
||||
};
|
||||
}, [removeAutofocusCallback, setAutofocusCallback]);
|
||||
|
||||
return <AutoFocusContext.Provider value={autoFocusControl}>
|
||||
{children}
|
||||
</AutoFocusContext.Provider>;
|
||||
};
|
||||
|
||||
export default AutoFocusProvider;
|
||||
@@ -0,0 +1,11 @@
|
||||
import FocusControlProvider from './FocusControlProvider';
|
||||
import MainAppContent from './MainAppContent';
|
||||
import ModalWrapper from './ModalWrapper';
|
||||
|
||||
const FocusControl = {
|
||||
Provider: FocusControlProvider,
|
||||
ModalWrapper,
|
||||
MainAppContent,
|
||||
};
|
||||
|
||||
export default FocusControl;
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { ModalState } from './types';
|
||||
|
||||
export interface FocusControl {
|
||||
setModalState(dialogId: string, state: ModalState): void;
|
||||
|
||||
hasOpenModal: boolean;
|
||||
hasClosingModal: boolean;
|
||||
}
|
||||
|
||||
export const FocusControlContext = createContext<FocusControl|null>(null);
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const FocusControlProvider: React.FC<Props> = props => {
|
||||
type ModalStates = Record<string, ModalState>;
|
||||
const [modalStates, setModalStates] = useState<ModalStates>({});
|
||||
|
||||
const setModalOpen = useCallback((dialogId: string, state: ModalState) => {
|
||||
setModalStates(modalStates => {
|
||||
modalStates = { ...modalStates };
|
||||
if (state === ModalState.Closed) {
|
||||
delete modalStates[dialogId];
|
||||
} else {
|
||||
modalStates[dialogId] = state;
|
||||
}
|
||||
return modalStates;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const modalStateValues = Object.values(modalStates);
|
||||
const hasOpenModal = modalStateValues.includes(ModalState.Open);
|
||||
const hasClosingModal = modalStateValues.includes(ModalState.Closing);
|
||||
|
||||
const focusControl = useMemo((): FocusControl => {
|
||||
return {
|
||||
hasOpenModal: hasOpenModal,
|
||||
hasClosingModal: hasClosingModal,
|
||||
setModalState: setModalOpen,
|
||||
};
|
||||
}, [hasOpenModal, hasClosingModal, setModalOpen]);
|
||||
|
||||
return <FocusControlContext.Provider value={focusControl}>
|
||||
{props.children}
|
||||
</FocusControlContext.Provider>;
|
||||
};
|
||||
|
||||
export default FocusControlProvider;
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import AccessibleView from '../AccessibleView';
|
||||
import { FocusControlContext } from './FocusControlProvider';
|
||||
import { useContext } from 'react';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
import AutoFocusProvider from './AutoFocusProvider';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
// A region that should not be accessibility focusable while a dialog
|
||||
// is open.
|
||||
const MainAppContent: React.FC<Props> = props => {
|
||||
const { hasOpenModal, hasClosingModal } = useContext(FocusControlContext);
|
||||
const blockFocus = hasOpenModal;
|
||||
const allowAutoFocus = !hasClosingModal && !blockFocus;
|
||||
|
||||
return <AccessibleView inert={blockFocus} style={props.style}>
|
||||
<AutoFocusProvider allowAutoFocus={allowAutoFocus}>
|
||||
{props.children}
|
||||
</AutoFocusProvider>
|
||||
</AccessibleView>;
|
||||
};
|
||||
|
||||
export default MainAppContent;
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useId } from 'react';
|
||||
import { FocusControlContext } from './FocusControlProvider';
|
||||
import { ModalState } from './types';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
state: ModalState;
|
||||
}
|
||||
|
||||
// A wrapper component that notifies the focus handler that a modal-like component
|
||||
// is visible. Modals that capture focus should wrap their content in this component.
|
||||
const ModalWrapper: React.FC<Props> = props => {
|
||||
const { setModalState: setDialogState } = useContext(FocusControlContext);
|
||||
const id = useId();
|
||||
useEffect(() => {
|
||||
if (!setDialogState) {
|
||||
throw new Error('ModalContent components must have a FocusControlProvider as an ancestor. Is FocusControlProvider part of the provider stack?');
|
||||
}
|
||||
setDialogState(id, props.state);
|
||||
}, [id, props.state, setDialogState]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setDialogState?.(id, ModalState.Closed);
|
||||
};
|
||||
}, [id, setDialogState]);
|
||||
|
||||
return <>
|
||||
{props.children}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default ModalWrapper;
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export -- FocusControl currently only has one shared type for external use
|
||||
export enum ModalState {
|
||||
// When `Open`, a modal blocks focus for the main app content.
|
||||
Open,
|
||||
// When `Closing`, a modal doesn't block main app content focus, but focus
|
||||
// shouldn't be moved to the main app content yet.
|
||||
// This is useful for native Modals, which have their own focus handling logic.
|
||||
// If Joplin moves accessibility focus before the native Modal focus handling
|
||||
// has completed, the Joplin-specified accessibility focus may be ignored.
|
||||
Closing,
|
||||
Closed,
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户