refactor(app): fix type-aware lint errors in UI components (#710)
This commit is contained in:
@@ -159,7 +159,9 @@ interface MarkdownWithStableRendererProps {
|
|||||||
|
|
||||||
const MarkdownWithStableRenderer = Markdown as ComponentType<MarkdownWithStableRendererProps>;
|
const MarkdownWithStableRenderer = Markdown as ComponentType<MarkdownWithStableRendererProps>;
|
||||||
const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer);
|
const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer);
|
||||||
const markdownStyleMapping = (theme: Theme) => ({ style: createMarkdownStyles(theme) }) as never;
|
const markdownStyleMapping = (theme: Theme): Partial<MarkdownWithStableRendererProps> => ({
|
||||||
|
style: createMarkdownStyles(theme),
|
||||||
|
});
|
||||||
|
|
||||||
const ThemedMicVocal = withUnistyles(MicVocal);
|
const ThemedMicVocal = withUnistyles(MicVocal);
|
||||||
const ThemedTodoCheckIcon = withUnistyles(Check);
|
const ThemedTodoCheckIcon = withUnistyles(Check);
|
||||||
@@ -428,6 +430,8 @@ function getUserMessageAttachmentLabel(attachment: AgentAttachment): string {
|
|||||||
return `Issue #${attachment.number}`;
|
return `Issue #${attachment.number}`;
|
||||||
case "text":
|
case "text":
|
||||||
return attachment.title ?? "Text attachment";
|
return attachment.title ?? "Text attachment";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +638,7 @@ const AssistantMarkdownResolvedImage = memo(function AssistantMarkdownResolvedIm
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cachedMetadata) {
|
if (cachedMetadata) {
|
||||||
setLoadState(getAssistantImageLoadStateFromMetadata(cachedMetadata));
|
setLoadState(getAssistantImageLoadStateFromMetadata(cachedMetadata));
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadState({ status: "loading" });
|
setLoadState({ status: "loading" });
|
||||||
@@ -924,11 +928,11 @@ function getInlineCodeAutoLinkUrl(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = markdownParser.linkify.match(trimmed) as Array<{
|
const matches: Array<{
|
||||||
index: number;
|
index: number;
|
||||||
lastIndex: number;
|
lastIndex: number;
|
||||||
url: string;
|
url: string;
|
||||||
}> | null;
|
}> | null = markdownParser.linkify.match(trimmed);
|
||||||
if (!matches || matches.length !== 1) {
|
if (!matches || matches.length !== 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -950,7 +954,7 @@ function nodeHasParentType(parent: unknown, type: string): boolean {
|
|||||||
typeof parent === "object" &&
|
typeof parent === "object" &&
|
||||||
parent !== null &&
|
parent !== null &&
|
||||||
"type" in parent &&
|
"type" in parent &&
|
||||||
(parent as { type?: string }).type === type
|
(parent as Record<"type", unknown>)["type"] === type
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1638,16 +1642,13 @@ export const AssistantMessage = memo(function AssistantMessage({
|
|||||||
style={styles.link}
|
style={styles.link}
|
||||||
onPress={handleLinkPress}
|
onPress={handleLinkPress}
|
||||||
>
|
>
|
||||||
{Children.map(children, (child) =>
|
{Children.map(children, (child) => {
|
||||||
isValidElement(child)
|
if (!isValidElement(child)) return child;
|
||||||
? cloneElement(child, {
|
const childProps = child.props as { style?: StyleProp<TextStyle> };
|
||||||
style: [
|
return cloneElement(child, {
|
||||||
(child.props as { style?: StyleProp<TextStyle> }).style,
|
style: [childProps.style, { color: styles.link.color }],
|
||||||
{ color: styles.link.color as string | undefined },
|
} as Partial<{ style: StyleProp<TextStyle> }>);
|
||||||
],
|
})}
|
||||||
} as Partial<{ style: StyleProp<TextStyle> }>)
|
|
||||||
: child,
|
|
||||||
)}
|
|
||||||
</MarkdownLink>
|
</MarkdownLink>
|
||||||
),
|
),
|
||||||
image: (
|
image: (
|
||||||
@@ -2421,12 +2422,13 @@ function useDetailWheelPropagationBlocker(input: {
|
|||||||
const { detailWrapperRef, enabled } = input;
|
const { detailWrapperRef, enabled } = input;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
const node = detailWrapperRef.current as unknown as HTMLElement | null;
|
const rawRef: unknown = detailWrapperRef.current;
|
||||||
if (!node || typeof node.addEventListener !== "function") {
|
if (!(rawRef instanceof HTMLElement)) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
const node = rawRef;
|
||||||
const stopWheelPropagation = (event: WheelEvent) => {
|
const stopWheelPropagation = (event: WheelEvent) => {
|
||||||
if (shouldStopDetailWheelPropagation(node, event)) {
|
if (shouldStopDetailWheelPropagation(node, event)) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -2596,7 +2598,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
enabled: !isNative && isExpanded && hasDetailContent,
|
enabled: !isNative && isExpanded && hasDetailContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const shimmerLabelStyle = useMemo(
|
const shimmerLabelStyle = useMemo<StyleProp<TextStyle>>(
|
||||||
() =>
|
() =>
|
||||||
buildShimmerTextStyle({
|
buildShimmerTextStyle({
|
||||||
isWebShimmer,
|
isWebShimmer,
|
||||||
@@ -2605,7 +2607,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
webShimmerTrackStart,
|
webShimmerTrackStart,
|
||||||
webShimmerTrackEnd,
|
webShimmerTrackEnd,
|
||||||
offsetX: labelOffsetX,
|
offsetX: labelOffsetX,
|
||||||
}) as never,
|
}),
|
||||||
[
|
[
|
||||||
isWebShimmer,
|
isWebShimmer,
|
||||||
webShimmerPeakWidth,
|
webShimmerPeakWidth,
|
||||||
@@ -2616,7 +2618,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const shimmerSecondaryStyle = useMemo(
|
const shimmerSecondaryStyle = useMemo<StyleProp<TextStyle>>(
|
||||||
() =>
|
() =>
|
||||||
buildShimmerTextStyle({
|
buildShimmerTextStyle({
|
||||||
isWebShimmer,
|
isWebShimmer,
|
||||||
@@ -2625,7 +2627,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
webShimmerTrackStart,
|
webShimmerTrackStart,
|
||||||
webShimmerTrackEnd,
|
webShimmerTrackEnd,
|
||||||
offsetX: secondaryOffsetX,
|
offsetX: secondaryOffsetX,
|
||||||
}) as never,
|
}),
|
||||||
[
|
[
|
||||||
isWebShimmer,
|
isWebShimmer,
|
||||||
webShimmerPeakWidth,
|
webShimmerPeakWidth,
|
||||||
@@ -2807,9 +2809,9 @@ function areExpandableBadgePropsEqual(previous: ExpandableBadgeProps, next: Expa
|
|||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
args?: unknown | null;
|
args?: unknown;
|
||||||
result?: unknown | null;
|
result?: unknown;
|
||||||
error?: unknown | null;
|
error?: unknown;
|
||||||
status: "executing" | "running" | "completed" | "failed" | "canceled";
|
status: "executing" | "running" | "completed" | "failed" | "canceled";
|
||||||
detail?: ToolCallDetail;
|
detail?: ToolCallDetail;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
@@ -2948,7 +2950,7 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onInlineDetailsExpandedChange) {
|
if (!onInlineDetailsExpandedChange) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
onInlineDetailsExpandedChange(false);
|
onInlineDetailsExpandedChange(false);
|
||||||
|
|||||||
@@ -125,6 +125,26 @@ interface SplitPaneDropData {
|
|||||||
paneId: string;
|
paneId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWorkspaceTabDragData(data: unknown): data is WorkspaceTabDragData {
|
||||||
|
return typeof data === "object" && data !== null && Reflect.get(data, "kind") === "workspace-tab";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSplitPaneDropData(data: unknown): data is SplitPaneDropData {
|
||||||
|
return (
|
||||||
|
typeof data === "object" && data !== null && Reflect.get(data, "kind") === "split-pane-drop"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function asWorkspaceTabDragData(data: unknown): WorkspaceTabDragData | undefined {
|
||||||
|
return isWorkspaceTabDragData(data) ? data : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asDragOverData(data: unknown): WorkspaceTabDragData | SplitPaneDropData | undefined {
|
||||||
|
if (isWorkspaceTabDragData(data)) return data;
|
||||||
|
if (isSplitPaneDropData(data)) return data;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMoveTabToPane"> {
|
interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout" | "onMoveTabToPane"> {
|
||||||
node: SplitNode;
|
node: SplitNode;
|
||||||
uiTabs: WorkspaceTab[];
|
uiTabs: WorkspaceTab[];
|
||||||
@@ -185,10 +205,10 @@ const MountedTabSlot = memo(function MountedTabSlot({
|
|||||||
[buildPaneContentModel, paneId, tabDescriptor],
|
[buildPaneContentModel, paneId, tabDescriptor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrapperStyle = useMemo(
|
const wrapperStyle = useMemo(() => {
|
||||||
() => ({ display: (isVisible ? "flex" : "none") as "flex" | "none", flex: 1 }),
|
const display: "flex" | "none" = isVisible ? "flex" : "none";
|
||||||
[isVisible],
|
return { display, flex: 1 };
|
||||||
);
|
}, [isVisible]);
|
||||||
const handleFocusPane = useCallback(() => {
|
const handleFocusPane = useCallback(() => {
|
||||||
onFocusPane(paneId);
|
onFocusPane(paneId);
|
||||||
}, [onFocusPane, paneId]);
|
}, [onFocusPane, paneId]);
|
||||||
@@ -388,8 +408,8 @@ export function SplitContainer({
|
|||||||
}, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]);
|
}, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]);
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||||
const data = event.active.data.current as WorkspaceTabDragData | undefined;
|
const data = asWorkspaceTabDragData(event.active.data.current);
|
||||||
if (data?.kind !== "workspace-tab") {
|
if (!data) {
|
||||||
setActiveDragTabId(null);
|
setActiveDragTabId(null);
|
||||||
setDropPreview(null);
|
setDropPreview(null);
|
||||||
setTabDropPreview(null);
|
setTabDropPreview(null);
|
||||||
@@ -406,11 +426,8 @@ export function SplitContainer({
|
|||||||
|
|
||||||
const updateDropPreview = useCallback(
|
const updateDropPreview = useCallback(
|
||||||
(event: Pick<DragMoveEvent, "active" | "over"> | Pick<DragOverEvent, "active" | "over">) => {
|
(event: Pick<DragMoveEvent, "active" | "over"> | Pick<DragOverEvent, "active" | "over">) => {
|
||||||
const activeData = event.active.data.current as WorkspaceTabDragData | undefined;
|
const activeData = asWorkspaceTabDragData(event.active.data.current);
|
||||||
const overData = event.over?.data.current as
|
const overData = asDragOverData(event.over?.data.current);
|
||||||
| WorkspaceTabDragData
|
|
||||||
| SplitPaneDropData
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (activeData?.kind !== "workspace-tab") {
|
if (activeData?.kind !== "workspace-tab") {
|
||||||
setDropPreview(null);
|
setDropPreview(null);
|
||||||
@@ -513,11 +530,8 @@ export function SplitContainer({
|
|||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
const activeData = event.active.data.current as WorkspaceTabDragData | undefined;
|
const activeData = asWorkspaceTabDragData(event.active.data.current);
|
||||||
const overData = event.over?.data.current as
|
const overData = asDragOverData(event.over?.data.current);
|
||||||
| WorkspaceTabDragData
|
|
||||||
| SplitPaneDropData
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
setActiveDragTabId(null);
|
setActiveDragTabId(null);
|
||||||
|
|
||||||
@@ -905,17 +919,14 @@ function SplitPaneView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNative) {
|
if (isNative) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const paneElement = paneRef.current as unknown as HTMLElement | null;
|
const rawRef: unknown = paneRef.current;
|
||||||
if (
|
if (!(rawRef instanceof HTMLElement)) {
|
||||||
!paneElement ||
|
return () => {};
|
||||||
typeof paneElement.addEventListener !== "function" ||
|
|
||||||
typeof paneElement.removeEventListener !== "function"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const paneElement = rawRef;
|
||||||
|
|
||||||
const handlePanePointerDown = (event: PointerEvent) => {
|
const handlePanePointerDown = (event: PointerEvent) => {
|
||||||
if (!shouldFocusPaneFromEventTarget(event.target)) {
|
if (!shouldFocusPaneFromEventTarget(event.target)) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -142,6 +143,16 @@ function clamp(value: number, min: number, max: number): number {
|
|||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTerminalState(value: unknown): value is TerminalState {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"rows" in value &&
|
||||||
|
"cols" in value &&
|
||||||
|
"grid" in value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ensureTerminalScrollbarStyle(): void {
|
function ensureTerminalScrollbarStyle(): void {
|
||||||
if (typeof document === "undefined") {
|
if (typeof document === "undefined") {
|
||||||
return;
|
return;
|
||||||
@@ -229,20 +240,41 @@ export default function TerminalEmulator({
|
|||||||
const [isScrollVisible, setIsScrollVisible] = useState(false);
|
const [isScrollVisible, setIsScrollVisible] = useState(false);
|
||||||
const [isScrollActive, setIsScrollActive] = useState(false);
|
const [isScrollActive, setIsScrollActive] = useState(false);
|
||||||
|
|
||||||
|
const domBridgeRef = useRef<DOMImperativeFactory | null>(null);
|
||||||
useDOMImperativeHandle(
|
useDOMImperativeHandle(
|
||||||
ref as Ref<DOMImperativeFactory>,
|
domBridgeRef,
|
||||||
() =>
|
(): DOMImperativeFactory => ({
|
||||||
({
|
writeOutput: (...args) => {
|
||||||
writeOutput: (text: string) => {
|
const text = args[0];
|
||||||
runtimeRef.current?.write({ text });
|
if (typeof text === "string") runtimeRef.current?.write({ text });
|
||||||
},
|
},
|
||||||
renderSnapshot: (state: TerminalState | null) => {
|
renderSnapshot: (...args) => {
|
||||||
|
const state = args[0];
|
||||||
|
if (state === null) {
|
||||||
|
runtimeRef.current?.renderSnapshot({ state: null });
|
||||||
|
} else if (isTerminalState(state)) {
|
||||||
runtimeRef.current?.renderSnapshot({ state });
|
runtimeRef.current?.renderSnapshot({ state });
|
||||||
},
|
}
|
||||||
clear: () => {
|
},
|
||||||
runtimeRef.current?.clear();
|
clear: () => {
|
||||||
},
|
runtimeRef.current?.clear();
|
||||||
}) as unknown as DOMImperativeFactory,
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
(): TerminalEmulatorHandle => ({
|
||||||
|
writeOutput: (text: string) => {
|
||||||
|
runtimeRef.current?.write({ text });
|
||||||
|
},
|
||||||
|
renderSnapshot: (state: TerminalState | null) => {
|
||||||
|
runtimeRef.current?.renderSnapshot({ state });
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
runtimeRef.current?.clear();
|
||||||
|
},
|
||||||
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -259,7 +291,7 @@ export default function TerminalEmulator({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = rootRef.current;
|
const root = rootRef.current;
|
||||||
if (!root || !swipeGesturesEnabled) {
|
if (!root || !swipeGesturesEnabled) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SWIPE_MIN_PX = 22;
|
const SWIPE_MIN_PX = 22;
|
||||||
@@ -376,7 +408,7 @@ export default function TerminalEmulator({
|
|||||||
const host = hostRef.current;
|
const host = hostRef.current;
|
||||||
const root = rootRef.current;
|
const root = rootRef.current;
|
||||||
if (!host || !root) {
|
if (!host || !root) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtime = new TerminalEmulatorRuntime();
|
const runtime = new TerminalEmulatorRuntime();
|
||||||
@@ -423,7 +455,7 @@ export default function TerminalEmulator({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusRequestToken <= 0) {
|
if (focusRequestToken <= 0) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
runtimeRef.current?.resize({ force: true });
|
runtimeRef.current?.resize({ force: true });
|
||||||
return focusWithRetries({
|
return focusWithRetries({
|
||||||
@@ -451,14 +483,14 @@ export default function TerminalEmulator({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const host = hostRef.current;
|
const host = hostRef.current;
|
||||||
if (!host) {
|
if (!host) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportElement = host.querySelector<HTMLElement>(".xterm-viewport");
|
const viewportElement = host.querySelector<HTMLElement>(".xterm-viewport");
|
||||||
if (!viewportElement) {
|
if (!viewportElement) {
|
||||||
viewportRef.current = null;
|
viewportRef.current = null;
|
||||||
setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 });
|
setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 });
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportRef.current = viewportElement;
|
viewportRef.current = viewportElement;
|
||||||
@@ -566,7 +598,7 @@ export default function TerminalEmulator({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDraggingScrollbar) {
|
if (!isDraggingScrollbar) {
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
type ReactElement,
|
type ReactElement,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type Ref,
|
type Ref,
|
||||||
type MutableRefObject,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -89,7 +88,7 @@ function useControllableOpenState({
|
|||||||
}): [boolean, (next: boolean) => void] {
|
}): [boolean, (next: boolean) => void] {
|
||||||
const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen));
|
const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen));
|
||||||
const isControlled = typeof open === "boolean";
|
const isControlled = typeof open === "boolean";
|
||||||
const value = isControlled ? Boolean(open) : internalOpen;
|
const value = isControlled ? open : internalOpen;
|
||||||
const setValue = useCallback(
|
const setValue = useCallback(
|
||||||
(next: boolean) => {
|
(next: boolean) => {
|
||||||
if (!isControlled) setInternalOpen(next);
|
if (!isControlled) setInternalOpen(next);
|
||||||
@@ -175,25 +174,23 @@ function computePosition({
|
|||||||
return { x, y, actualPlacement };
|
return { x, y, actualPlacement };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCallable(fn: unknown): fn is (...args: unknown[]) => void {
|
||||||
|
return typeof fn === "function";
|
||||||
|
}
|
||||||
|
|
||||||
function coerceEventPoint(event: unknown): { pageX: number; pageY: number } | null {
|
function coerceEventPoint(event: unknown): { pageX: number; pageY: number } | null {
|
||||||
const wrapper = event as
|
if (typeof event !== "object" || event === null) {
|
||||||
| {
|
return null;
|
||||||
nativeEvent?: { pageX?: number; pageY?: number; clientX?: number; clientY?: number };
|
}
|
||||||
pageX?: number;
|
const nativeEvent = Reflect.get(event, "nativeEvent");
|
||||||
pageY?: number;
|
const native = typeof nativeEvent === "object" && nativeEvent !== null ? nativeEvent : event;
|
||||||
clientX?: number;
|
const pageX = Reflect.get(native, "pageX");
|
||||||
clientY?: number;
|
const pageY = Reflect.get(native, "pageY");
|
||||||
}
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
const nativeEvent = wrapper?.nativeEvent ?? wrapper;
|
|
||||||
const pageX = nativeEvent?.pageX;
|
|
||||||
const pageY = nativeEvent?.pageY;
|
|
||||||
if (typeof pageX === "number" && typeof pageY === "number") {
|
if (typeof pageX === "number" && typeof pageY === "number") {
|
||||||
return { pageX, pageY };
|
return { pageX, pageY };
|
||||||
}
|
}
|
||||||
const clientX = nativeEvent?.clientX;
|
const clientX = Reflect.get(native, "clientX");
|
||||||
const clientY = nativeEvent?.clientY;
|
const clientY = Reflect.get(native, "clientY");
|
||||||
if (typeof clientX === "number" && typeof clientY === "number") {
|
if (typeof clientX === "number" && typeof clientY === "number") {
|
||||||
return { pageX: clientX, pageY: clientY };
|
return { pageX: clientX, pageY: clientY };
|
||||||
}
|
}
|
||||||
@@ -206,7 +203,7 @@ function assignRef<T>(ref: Ref<T> | undefined, value: T): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ref && typeof ref === "object") {
|
if (ref && typeof ref === "object") {
|
||||||
(ref as MutableRefObject<T>).current = value;
|
Object.assign(ref, { current: value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,10 +323,13 @@ export function ContextMenuTrigger({
|
|||||||
if (isNative) {
|
if (isNative) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const e = event as { preventDefault?: () => void; stopPropagation?: () => void } | undefined;
|
if (typeof event === "object" && event !== null) {
|
||||||
e?.preventDefault?.();
|
const preventDefault = Reflect.get(event, "preventDefault");
|
||||||
e?.stopPropagation?.();
|
const stopPropagation = Reflect.get(event, "stopPropagation");
|
||||||
openAtEvent(event as GestureResponderEvent);
|
if (isCallable(preventDefault)) preventDefault.call(event);
|
||||||
|
if (isCallable(stopPropagation)) stopPropagation.call(event);
|
||||||
|
}
|
||||||
|
openAtEvent(event);
|
||||||
},
|
},
|
||||||
[openAtEvent],
|
[openAtEvent],
|
||||||
);
|
);
|
||||||
@@ -337,7 +337,7 @@ export function ContextMenuTrigger({
|
|||||||
const pressableStyle = useCallback(
|
const pressableStyle = useCallback(
|
||||||
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => {
|
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => {
|
||||||
if (typeof style === "function") {
|
if (typeof style === "function") {
|
||||||
return style({ pressed, hovered: Boolean(hovered), open: ctx.open });
|
return style({ pressed, hovered, open: ctx.open });
|
||||||
}
|
}
|
||||||
return style;
|
return style;
|
||||||
},
|
},
|
||||||
@@ -434,40 +434,29 @@ export function ContextMenuContent({
|
|||||||
|
|
||||||
// Measure trigger when opening (fallback) and capture point anchors.
|
// Measure trigger when opening (fallback) and capture point anchors.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useMobileSheet) {
|
if (useMobileSheet || !open) {
|
||||||
setTriggerRect(null);
|
setTriggerRect(null);
|
||||||
setContentSize(null);
|
setContentSize(null);
|
||||||
setPosition(null);
|
setPosition(null);
|
||||||
return;
|
return () => {};
|
||||||
}
|
|
||||||
|
|
||||||
if (!open) {
|
|
||||||
setTriggerRect(null);
|
|
||||||
setContentSize(null);
|
|
||||||
setPosition(null);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anchorRect) {
|
if (anchorRect) {
|
||||||
setTriggerRect(anchorRect);
|
setTriggerRect(anchorRect);
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!triggerRef.current) {
|
if (!triggerRef.current) {
|
||||||
setTriggerRect(null);
|
setTriggerRect(null);
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
measureElement(triggerRef.current).then((rect) => {
|
void measureElement(triggerRef.current).then((rect) => {
|
||||||
if (cancelled) return;
|
if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
|
||||||
setTriggerRect({
|
return undefined;
|
||||||
...rect,
|
|
||||||
y: rect.y + statusBarHeight,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
type ReactElement,
|
type ReactElement,
|
||||||
type Ref,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@@ -82,24 +81,20 @@ function shouldOpenOnFocus(): boolean {
|
|||||||
return !isWeb || lastInputWasKeyboard;
|
return !isWeb || lastInputWasKeyboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
function composeEventHandlers<E>(
|
function isCallable(fn: unknown): fn is (...args: unknown[]) => void {
|
||||||
original?: (event: E) => void,
|
return typeof fn === "function";
|
||||||
injected?: (event: E) => void,
|
|
||||||
): (event: E) => void {
|
|
||||||
return (event: E) => {
|
|
||||||
original?.(event);
|
|
||||||
injected?.(event);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignRef<T>(ref: Ref<T> | undefined, value: T): void {
|
function composeEventHandlers(
|
||||||
if (typeof ref === "function") {
|
original: unknown,
|
||||||
ref(value);
|
injected: (event: unknown) => void,
|
||||||
return;
|
): (event: unknown) => void {
|
||||||
}
|
return (event: unknown) => {
|
||||||
if (ref && typeof ref === "object") {
|
if (isCallable(original)) {
|
||||||
(ref as { current: T }).current = value;
|
original(event);
|
||||||
}
|
}
|
||||||
|
injected(event);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useControllableOpenState({
|
function useControllableOpenState({
|
||||||
@@ -320,7 +315,7 @@ export function TooltipTrigger({
|
|||||||
|
|
||||||
const handleHoverIn = useCallback(
|
const handleHoverIn = useCallback(
|
||||||
(e?: unknown) => {
|
(e?: unknown) => {
|
||||||
onHoverIn?.(e as never);
|
if (isCallable(onHoverIn)) onHoverIn(e);
|
||||||
scheduleOpen();
|
scheduleOpen();
|
||||||
},
|
},
|
||||||
[onHoverIn, scheduleOpen],
|
[onHoverIn, scheduleOpen],
|
||||||
@@ -328,7 +323,7 @@ export function TooltipTrigger({
|
|||||||
|
|
||||||
const handleHoverOut = useCallback(
|
const handleHoverOut = useCallback(
|
||||||
(e?: unknown) => {
|
(e?: unknown) => {
|
||||||
onHoverOut?.(e as never);
|
if (isCallable(onHoverOut)) onHoverOut(e);
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
[onHoverOut, close],
|
[onHoverOut, close],
|
||||||
@@ -336,7 +331,7 @@ export function TooltipTrigger({
|
|||||||
|
|
||||||
const handleFocus = useCallback(
|
const handleFocus = useCallback(
|
||||||
(e: unknown) => {
|
(e: unknown) => {
|
||||||
onFocus?.(e as never);
|
if (isCallable(onFocus)) onFocus(e);
|
||||||
if (!ctx.enabled || disabled) return;
|
if (!ctx.enabled || disabled) return;
|
||||||
if (!shouldOpenOnFocus()) return;
|
if (!shouldOpenOnFocus()) return;
|
||||||
clearOpenTimer();
|
clearOpenTimer();
|
||||||
@@ -347,7 +342,7 @@ export function TooltipTrigger({
|
|||||||
|
|
||||||
const handleBlur = useCallback(
|
const handleBlur = useCallback(
|
||||||
(e: unknown) => {
|
(e: unknown) => {
|
||||||
onBlur?.(e as never);
|
if (isCallable(onBlur)) onBlur(e);
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
[close, onBlur],
|
[close, onBlur],
|
||||||
@@ -355,7 +350,7 @@ export function TooltipTrigger({
|
|||||||
|
|
||||||
const handlePress = useCallback(
|
const handlePress = useCallback(
|
||||||
(e: unknown) => {
|
(e: unknown) => {
|
||||||
onPress?.(e as never);
|
if (isCallable(onPress)) onPress(e);
|
||||||
if (!ctx.enabled || disabled) {
|
if (!ctx.enabled || disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -394,26 +389,33 @@ export function TooltipTrigger({
|
|||||||
throw new Error("TooltipTrigger with asChild expects a single React element child");
|
throw new Error("TooltipTrigger with asChild expects a single React element child");
|
||||||
}
|
}
|
||||||
|
|
||||||
const childProps = child.props as Record<string, unknown>;
|
const rawProps: unknown = child.props;
|
||||||
const mergedProps = {
|
if (typeof rawProps !== "object" || rawProps === null) {
|
||||||
...childProps,
|
throw new Error("TooltipTrigger asChild child must have props object");
|
||||||
|
}
|
||||||
|
const mergedProps: Record<string, unknown> = {
|
||||||
|
...Object.assign({}, rawProps),
|
||||||
...triggerProps,
|
...triggerProps,
|
||||||
disabled: childProps.disabled || disabled,
|
disabled: Reflect.get(rawProps, "disabled") || disabled,
|
||||||
onHoverIn: composeEventHandlers(childProps.onHoverIn as never, handleHoverIn),
|
onHoverIn: composeEventHandlers(Reflect.get(rawProps, "onHoverIn"), handleHoverIn),
|
||||||
onHoverOut: composeEventHandlers(childProps.onHoverOut as never, handleHoverOut),
|
onHoverOut: composeEventHandlers(Reflect.get(rawProps, "onHoverOut"), handleHoverOut),
|
||||||
onFocus: composeEventHandlers(childProps.onFocus as never, handleFocus),
|
onFocus: composeEventHandlers(Reflect.get(rawProps, "onFocus"), handleFocus),
|
||||||
onBlur: composeEventHandlers(childProps.onBlur as never, handleBlur),
|
onBlur: composeEventHandlers(Reflect.get(rawProps, "onBlur"), handleBlur),
|
||||||
onPress: composeEventHandlers(childProps.onPress as never, handlePress),
|
onPress: composeEventHandlers(Reflect.get(rawProps, "onPress"), handlePress),
|
||||||
onPointerEnter: composeEventHandlers(childProps.onPointerEnter as never, handleHoverIn),
|
onPointerEnter: composeEventHandlers(Reflect.get(rawProps, "onPointerEnter"), handleHoverIn),
|
||||||
onPointerLeave: composeEventHandlers(childProps.onPointerLeave as never, handleHoverOut),
|
onPointerLeave: composeEventHandlers(Reflect.get(rawProps, "onPointerLeave"), handleHoverOut),
|
||||||
onMouseEnter: composeEventHandlers(childProps.onMouseEnter as never, handleHoverIn),
|
onMouseEnter: composeEventHandlers(Reflect.get(rawProps, "onMouseEnter"), handleHoverIn),
|
||||||
onMouseLeave: composeEventHandlers(childProps.onMouseLeave as never, handleHoverOut),
|
onMouseLeave: composeEventHandlers(Reflect.get(rawProps, "onMouseLeave"), handleHoverOut),
|
||||||
} as Record<string, unknown>;
|
};
|
||||||
|
|
||||||
const existingRefProp = childProps[triggerRefProp] as Ref<View | null> | undefined;
|
const existingRefProp = Reflect.get(rawProps, triggerRefProp);
|
||||||
mergedProps[triggerRefProp] = (node: View | null) => {
|
mergedProps[triggerRefProp] = (node: View) => {
|
||||||
assignRef(existingRefProp, node);
|
if (isCallable(existingRefProp)) {
|
||||||
assignRef(ctx.triggerRef, node);
|
existingRefProp(node);
|
||||||
|
} else if (existingRefProp && typeof existingRefProp === "object") {
|
||||||
|
Object.assign(existingRefProp, { current: node });
|
||||||
|
}
|
||||||
|
Object.assign(ctx.triggerRef, { current: node });
|
||||||
};
|
};
|
||||||
|
|
||||||
return cloneElement(child, mergedProps);
|
return cloneElement(child, mergedProps);
|
||||||
@@ -453,16 +455,15 @@ export function TooltipContent({
|
|||||||
setTriggerRect(null);
|
setTriggerRect(null);
|
||||||
setContentSize(null);
|
setContentSize(null);
|
||||||
setPosition(null);
|
setPosition(null);
|
||||||
return;
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
measureElement(ctx.triggerRef.current).then((rect) => {
|
void measureElement(ctx.triggerRef.current).then((rect) => {
|
||||||
if (cancelled) return;
|
if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
|
||||||
setTriggerRect({ ...rect, y: rect.y + statusBarHeight });
|
return undefined;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user