import React, { useEffect, useRef } from 'react'; import { Pressable, StyleSheet, Text, View, findNodeHandle, AccessibilityInfo, Platform, } from 'react-native'; import type { TooltipProps } from '../core/types'; import { announce, formatStepAnnouncement, getButtonLabel, } from '../utils/accessibility'; /** * A tooltip component that displays step-by-step guidance in a coachmark tour. * * @remarks * This component renders a positioned tooltip with title, description, progress indicators, * and navigation buttons. It includes comprehensive accessibility support with screen reader * announcements and focus management. * * @param theme - Theme configuration for styling the tooltip * @param title - Optional title text displayed at the top of the tooltip * @param description - Optional description text providing additional context * @param pos - Position coordinates {x, y} for absolute positioning of the tooltip * @param index - Zero-based index of the current step in the tour * @param count - Total number of steps in the tour * @param onNext - Callback function invoked when the Next/Done button is pressed * @param onBack - Optional callback function invoked when the Back button is pressed * @param onSkip - Optional callback function invoked when the Skip button is pressed * @param onLayout - Optional callback invoked when the tooltip layout is measured, receives width and height * * @example * ```tsx * console.log('Next step')} * onSkip={() => console.log('Skip tour')} * /> * ``` */ export const Tooltip: React.FC = ({ theme, title, description, pos, index, count, onNext, onBack, onSkip, onLayout, }) => { const isLast = index === count - 1; const containerRef = useRef(null); useEffect(() => { const message = formatStepAnnouncement( { title, description }, index, count ); announce(message); if (containerRef.current && Platform.OS !== 'web') { const reactTag = findNodeHandle(containerRef.current); if (reactTag) { AccessibilityInfo.setAccessibilityFocus(reactTag); } } }, [index, title, description, count]); return ( { const { width, height } = e.nativeEvent.layout; onLayout?.({ width, height }); }} accessible accessibilityRole="alert" accessibilityLabel={formatStepAnnouncement( { title, description }, index, count )} accessibilityLiveRegion="polite" accessibilityViewIsModal importantForAccessibility="yes" > {!!title && ( {title} )} {!!description && ( {description} )} {Array(count) .fill(0) .map((_, i) => ( ))} {onBack && index > 0 && ( Back )} {onSkip && !isLast && ( Skip )} {isLast ? 'Done' : 'Next'} ); }; const styles = StyleSheet.create({ container: { shadowColor: '#000', shadowOpacity: 0.2, shadowRadius: 8, shadowOffset: { width: 0, height: 4 }, elevation: 4, }, title: { fontWeight: '700', fontSize: 16, marginBottom: 6, }, desc: { fontSize: 14, marginBottom: 12, lineHeight: 20, }, progress: { flexDirection: 'row', justifyContent: 'center', gap: 6, marginBottom: 12, }, dot: { width: 6, height: 6, borderRadius: 3, }, row: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, }, btn: { paddingVertical: 8, paddingHorizontal: 14, borderRadius: 10, }, btnText: { color: '#fff', fontWeight: '600', fontSize: 14, }, });