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,266 @@
|
||||
// A modal component with different defaults and better support for web.
|
||||
// On Android and iOS, this wraps the default <Modal> component. On web,
|
||||
// it uses a <dialog>.
|
||||
|
||||
import * as React from 'react';
|
||||
import { RefObject, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Animated, GestureResponderEvent, Modal, Platform, Pressable, ScrollView, ScrollViewProps, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import FocusControl from './accessibility/FocusControl/FocusControl';
|
||||
import { msleep, Second } from '@joplin/utils/time';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ModalState } from './accessibility/FocusControl/types';
|
||||
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import KeyboardAvoidingView from './KeyboardAvoidingView';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||
|
||||
type AllScrollViewProps = ScrollViewProps & { ref?: React.Ref<ScrollView> };
|
||||
|
||||
type OnClose = ()=> void;
|
||||
type OnShow = ()=> void;
|
||||
export interface ModalElementProps {
|
||||
visible: boolean;
|
||||
|
||||
// If provided, acts similar to the React Native modal's "onRequestClose", must be provided
|
||||
// but can be `null` to prevent the default close behavior.
|
||||
onClose: OnClose|null;
|
||||
onShow?: OnShow;
|
||||
// iOS only: Called once the modal has finished its dismissal transition
|
||||
onDismissed?: ()=> void;
|
||||
animationType?: 'fade'|'none';
|
||||
// When false, render within the surrounding portal rather than creating a
|
||||
// separate native/top-layer modal.
|
||||
useNativeModal?: boolean;
|
||||
wrapContent?: (view: React.ReactNode)=> React.ReactNode;
|
||||
|
||||
statusBarTranslucent?: boolean;
|
||||
|
||||
children: React.ReactNode;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
backgroundColor?: string;
|
||||
modalBackgroundStyle?: ViewStyle;
|
||||
// Extra styles for the accessibility tools dismiss button. For example,
|
||||
// this might be used to display the dismiss button near the top of the
|
||||
// screen (rather than the bottom).
|
||||
dismissButtonStyle?: ViewStyle;
|
||||
|
||||
// If scrollOverflow is provided, the modal is wrapped in a vertical
|
||||
// ScrollView. This allows the user to scroll parts of dialogs into
|
||||
// view that would otherwise be clipped by the screen edge.
|
||||
scrollOverflow?: boolean|AllScrollViewProps;
|
||||
}
|
||||
|
||||
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
|
||||
const safeArea = useSafeAreaPadding();
|
||||
const keyboardState = useKeyboardState();
|
||||
return useMemo(() => {
|
||||
const safeAreaPadding = {
|
||||
paddingRight: safeArea.paddingRight,
|
||||
paddingLeft: safeArea.paddingLeft,
|
||||
paddingTop: safeArea.paddingTop,
|
||||
paddingBottom: keyboardState.keyboardVisible ? 0 : safeArea.paddingBottom,
|
||||
};
|
||||
|
||||
// On Android, the top-level container seems to need to be absolutely positioned
|
||||
// to prevent it from being larger than the screen size:
|
||||
const absoluteFill = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
} satisfies ViewStyle;
|
||||
|
||||
return StyleSheet.create({
|
||||
modalBackground: {
|
||||
...safeAreaPadding,
|
||||
...(hasScrollView ? {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
} : absoluteFill),
|
||||
|
||||
// When hasScrollView, the modal background is wrapped in a ScrollView. In this case, it's
|
||||
// possible to scroll content outside the background into view. To prevent the edge of the
|
||||
// background from being visible, the background color is applied to the ScrollView container
|
||||
// instead:
|
||||
backgroundColor: hasScrollView ? null : backgroundColor,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
...absoluteFill,
|
||||
flex: 1,
|
||||
},
|
||||
modalScrollView: {
|
||||
backgroundColor,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
modalScrollViewContent: {
|
||||
// Make the scroll view's scrolling region at least as tall as its container.
|
||||
// This makes it possible to vertically center the content of scrollable modals.
|
||||
flexGrow: 1,
|
||||
},
|
||||
dismissButton: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
height: 12,
|
||||
width: '100%',
|
||||
zIndex: -1,
|
||||
},
|
||||
});
|
||||
}, [hasScrollView, safeArea, keyboardState, backgroundColor]);
|
||||
};
|
||||
|
||||
const useBackgroundTouchListeners = (onRequestClose: OnClose|null, backdropRef: RefObject<View>) => {
|
||||
const onShouldBackgroundCaptureTouch = useCallback((event: GestureResponderEvent) => {
|
||||
return event.target === backdropRef.current && event.nativeEvent.touches.length === 1;
|
||||
}, [backdropRef]);
|
||||
|
||||
const onBackgroundTouchFinished = useCallback((event: GestureResponderEvent) => {
|
||||
if (event.target === backdropRef.current) {
|
||||
onRequestClose?.();
|
||||
}
|
||||
}, [onRequestClose, backdropRef]);
|
||||
|
||||
return { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished };
|
||||
};
|
||||
|
||||
const useModalStatus = (containerComponent: View|null, visible: boolean) => {
|
||||
const contentMounted = !!containerComponent;
|
||||
const [controlsFocus, setControlsFocus] = useState(false);
|
||||
useAsyncEffect(async (event) => {
|
||||
if (contentMounted) {
|
||||
setControlsFocus(true);
|
||||
} else {
|
||||
// Accessibility: Work around Android's default focus-setting behavior.
|
||||
// By default, React Native's Modal on Android sets focus about 0.8 seconds
|
||||
// after the modal is dismissed. As a result, the Modal controls focus until
|
||||
// roughly one second after the modal is dismissed.
|
||||
if (Platform.OS === 'android') {
|
||||
await msleep(Second);
|
||||
}
|
||||
|
||||
if (!event.cancelled) {
|
||||
setControlsFocus(false);
|
||||
}
|
||||
}
|
||||
}, [contentMounted]);
|
||||
|
||||
let modalStatus = ModalState.Closed;
|
||||
if (controlsFocus) {
|
||||
modalStatus = visible ? ModalState.Open : ModalState.Closing;
|
||||
} else if (visible) {
|
||||
modalStatus = ModalState.Open;
|
||||
}
|
||||
return modalStatus;
|
||||
};
|
||||
|
||||
const ModalElement: React.FC<ModalElementProps> = ({
|
||||
children,
|
||||
containerStyle,
|
||||
backgroundColor,
|
||||
scrollOverflow,
|
||||
modalBackgroundStyle: extraModalBackgroundStyles,
|
||||
dismissButtonStyle,
|
||||
onClose,
|
||||
onDismissed,
|
||||
wrapContent,
|
||||
useNativeModal = true,
|
||||
...forwardedProps
|
||||
}) => {
|
||||
const styles = useStyles(!!scrollOverflow, backgroundColor);
|
||||
|
||||
// contentWrapper adds padding. To allow styling the region outside of the modal
|
||||
// (e.g. to add a background), the content is wrapped twice.
|
||||
const content = (
|
||||
<Animated.View style={containerStyle}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const [containerComponent, setContainerComponent] = useState<View|null>(null);
|
||||
const modalStatus = useModalStatus(containerComponent, forwardedProps.visible);
|
||||
|
||||
const containerRef = useRef<View|null>(null);
|
||||
containerRef.current = containerComponent;
|
||||
const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(onClose, containerRef);
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
onClose?.();
|
||||
onDismissed?.();
|
||||
}, [onClose, onDismissed]);
|
||||
|
||||
// A close button for accessibility tools. Since iOS accessibility focus order is based on the position
|
||||
// of the element on the screen, the close button is placed after the modal content, rather than behind.
|
||||
const closeButton = onClose ? <Pressable
|
||||
style={[styles.dismissButton, dismissButtonStyle]}
|
||||
onPress={onClose}
|
||||
accessibilityLabel={_('Close dialog')}
|
||||
accessibilityRole='button'
|
||||
/> : null;
|
||||
|
||||
let contentAndBackdrop: React.ReactNode = <View
|
||||
ref={setContainerComponent}
|
||||
style={[styles.modalBackground, extraModalBackgroundStyles]}
|
||||
onStartShouldSetResponder={onShouldBackgroundCaptureTouch}
|
||||
onResponderRelease={onBackgroundTouchFinished}
|
||||
>
|
||||
{content}
|
||||
{closeButton}
|
||||
</View>;
|
||||
if (wrapContent) {
|
||||
contentAndBackdrop = wrapContent(contentAndBackdrop);
|
||||
}
|
||||
|
||||
const extraScrollViewProps = (typeof scrollOverflow === 'object' ? scrollOverflow : {});
|
||||
const modalContent = scrollOverflow ? (
|
||||
<KeyboardAvoidingView style={styles.keyboardAvoidingView} enabled={true}>
|
||||
<ScrollView
|
||||
{...extraScrollViewProps}
|
||||
style={[styles.modalScrollView, extraScrollViewProps.style]}
|
||||
contentContainerStyle={[styles.modalScrollViewContent, extraScrollViewProps.contentContainerStyle]}
|
||||
>{contentAndBackdrop}</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
) : contentAndBackdrop;
|
||||
|
||||
const result = (
|
||||
<FocusControl.ModalWrapper state={modalStatus}>
|
||||
{useNativeModal ? <ModalComponent
|
||||
// supportedOrientations: On iOS, this allows the dialog to be shown in non-portrait orientations.
|
||||
supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}
|
||||
animationType='fade'
|
||||
transparent
|
||||
|
||||
// Web:
|
||||
onClose={onClose}
|
||||
// iOS only: Called after closing
|
||||
onDismiss={onDismiss}
|
||||
// Called before closing on Android and sometimes called before closing on iOS
|
||||
onRequestClose={onClose}
|
||||
{...forwardedProps}
|
||||
>
|
||||
{modalContent}
|
||||
</ModalComponent> : (forwardedProps.visible ? modalContent : null)}
|
||||
</FocusControl.ModalWrapper>
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// On web, prefer a <Dialog> element for improved behavior when multiple dialogs
|
||||
// are open at the same time. See https://github.com/laurent22/joplin/issues/11799.
|
||||
const ModalComponent = Platform.OS === 'web' ? (props: ModalElementProps) => {
|
||||
return <Dialog
|
||||
open={props.visible}
|
||||
onCancel={props.onClose}
|
||||
onShow={props.onShow}
|
||||
// 2026-09-03: Work around dialog display issues on Safari by autofocusing
|
||||
// the root dialog element.
|
||||
onBeforeShow={dialog => dialog.setAttribute('autofocus', 'true')}
|
||||
preventAutoCloseOnCancel={true}
|
||||
>
|
||||
{props.children}
|
||||
</Dialog>;
|
||||
} : Modal;
|
||||
|
||||
export default ModalElement;
|
||||
在新工单中引用
屏蔽一个用户