Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/mobile/__tests__/components/ui/app-time-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ function columns(tree: any): any[] {

function optionIn(column: any, label: string): any {
return column
.findAll((node: any) => node.type === 'TouchableOpacity')
.findAll((node: any) => node.type === Pressable)
.find((node: any) => node.props.accessibilityLabel === label)
}

function doneButton(tree: any): any {
return tree.root.find(
(node: any) =>
node.type === 'TouchableOpacity' && node.props.accessibilityLabel === 'common.done',
node.type === Pressable && node.props.accessibilityLabel === 'common.done',
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { Pressable } from 'react-native'
import { describe, expect, it, vi } from 'vitest'

import { OfflineUnavailableState } from '@/components/ui/offline-unavailable-state'
Expand Down Expand Up @@ -53,7 +54,7 @@ describe('OfflineUnavailableState', () => {
)
expect(summary.props.accessibilityLiveRegion).toBe('polite')

const button = tree.root.findByType('TouchableOpacity')
const button = tree.root.findByType(Pressable)
expect(button.props.accessibilityRole).toBe('button')
expect(button.props.accessibilityLabel).toBe('Try again')
expect(button.props.accessibilityState).toEqual({ disabled: true })
Expand All @@ -75,7 +76,7 @@ describe('OfflineUnavailableState', () => {

expect(
tree.root.findAll(
(node: any) => node.type === 'TouchableOpacity' && node.props.onPress,
(node: any) => node.type === Pressable && node.props.onPress,
),
).toHaveLength(0)
expect(
Expand Down
45 changes: 33 additions & 12 deletions apps/mobile/components/habit-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ export interface HabitListHandle {

const TOUR_FEATURED_HABIT_ID = 'tour-habit-2'

const SKELETON_KEYS = [
'skeleton-1',
'skeleton-2',
'skeleton-3',
'skeleton-4',
'skeleton-5',
]

// react-doctor-disable-next-line no-giant-component -- core list orchestrator already decomposed into ./habit-list/* submodules (empty-state, date-group-section, drill-view, move-parent-dialog, tree-helpers, styles); the remaining body is cohesive list state + handlers, extraction deferred to avoid regression without device QA https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
function HabitList(
{
Expand Down Expand Up @@ -410,6 +419,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
return topLevelHabits.filter(
(habit) => !habit.isCompleted || recentlyCompletedIds.has(habit.id),
)
// react-doctor-disable-next-line exhaustive-deps -- topLevelHabits is the extracted habitsQuery.data.topLevelHabits and already listed; the analyzer wants the qualified member path but the alias tracks it https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [recentlyCompletedIds, showCompleted, topLevelHabits, view, visibility])

const dateGroups = useMemo<HabitListDateGroup[]>(() => {
Expand All @@ -423,6 +433,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
? t('habits.overdue')
: formatDateGroupLabel(bucket.key, deviceLocale, t),
}))
// react-doctor-disable-next-line exhaustive-deps -- deviceLocale is the extracted i18n.language and already listed; the analyzer wants the qualified member path but the alias tracks it https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [deviceLocale, t, view, visibleHabits])

const allLoadedIds = useMemo(() => {
Expand Down Expand Up @@ -468,10 +479,12 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
)

useEffect(() => {
// react-doctor-disable-next-line no-pass-data-to-parent, no-pass-live-state-to-parent, no-prop-callback-in-effect -- documented parent-mirror callback: HabitList computes allCollapsed internally and notifies the parent so it can mirror it in render-time state (refs cannot be read during render — see the prop JSDoc) https://github.com/thomasluizon/orbit-ui-mobile/issues/243
onAllCollapsedChange?.(allCollapsed)
}, [allCollapsed, onAllCollapsedChange])

useEffect(() => {
// react-doctor-disable-next-line no-pass-data-to-parent, no-pass-live-state-to-parent, no-prop-callback-in-effect -- documented parent-mirror callback: HabitList computes allLoadedIds internally and notifies the parent so it can mirror it in render-time state (refs cannot be read during render — see the prop JSDoc) https://github.com/thomasluizon/orbit-ui-mobile/issues/243
onAllLoadedIdsChange?.(allLoadedIds)
}, [allLoadedIds, onAllLoadedIdsChange])

Expand Down Expand Up @@ -525,6 +538,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
const activeDragItemsRef = useRef(activeDragItems)
useEffect(() => {
activeDragItemsRef.current = activeDragItems
// react-doctor-disable-next-line exhaustive-deps -- activeDragItems already combines dragOverrideItems and flatItems (both the analyzer flags); listing the combined value is sufficient, no staleness https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [activeDragItems])
const isDndEnabled = view !== 'all' && !isSelectMode

Expand All @@ -538,6 +552,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
habitsById,
selectedDateStr,
}
// react-doctor-disable-next-line exhaustive-deps -- getChildren/habitsById are the extracted habitsQuery members and already listed; the analyzer wants the qualified paths but the aliases track them https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [getChildren, isListView, visibility, habitsById, selectedDateStr])

const childrenProgressMap = useMemo(() => {
Expand Down Expand Up @@ -613,6 +628,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
}

return map
// react-doctor-disable-next-line exhaustive-deps -- getChildren/habitsById are the extracted habitsQuery members and already listed; the analyzer wants the qualified paths but the aliases track them https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [getChildren, habitsById, isListView, visibility])

const getChildrenProgress = useCallback(
Expand Down Expand Up @@ -813,6 +829,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
targetParentId,
draggedId,
),
// react-doctor-disable-next-line exhaustive-deps -- getChildren/habitsById/maxHabitDepth are extracted habitsQuery/appConfig members already listed; the analyzer wants the qualified paths but the aliases track them https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[getChildren, habitsById, maxHabitDepth, t],
)

Expand All @@ -822,6 +839,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
{ topLevelHabits, getChildren, validateMoveTarget, t },
movingHabitId,
)
// react-doctor-disable-next-line exhaustive-deps -- topLevelHabits/getChildren are extracted habitsQuery members already listed; the analyzer wants the qualified paths but the aliases track them https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [getChildren, movingHabitId, t, topLevelHabits, validateMoveTarget])

const selectedMoveOption = useMemo(
Expand Down Expand Up @@ -1011,6 +1029,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
restoreCollapsedStateAfterDrag()
}
},
// react-doctor-disable-next-line exhaustive-deps -- getChildren/habitsById are extracted habitsQuery members already listed; the analyzer wants the qualified paths but the aliases track them https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[
getChildren,
habitsById,
Expand Down Expand Up @@ -1047,6 +1066,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
}
},
}),
// react-doctor-disable-next-line exhaustive-deps -- refetch is the extracted habitsQuery.refetch and already listed; the analyzer wants the qualified path but the alias tracks it https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[
allCollapsed,
allLoadedIds,
Expand Down Expand Up @@ -1222,6 +1242,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(

return walk(parentId, depth)
},
// react-doctor-disable-next-line exhaustive-deps -- maxHabitDepth is the extracted appConfig.limits.maxHabitDepth and already listed; the analyzer wants the qualified path but the alias tracks it https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[
collapsedIds,
getVisibleChildren,
Expand Down Expand Up @@ -1256,6 +1277,15 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(

const keyExtractor = useCallback((item: DragItem) => item.id, [])

const renderSkeletonItem = useCallback(
() => (
<View style={styles.sectionInset}>
<SkeletonCard styles={styles} />
</View>
),
[styles],
)

const renderEmptyState = useCallback(
(currentView: 'today' | 'all' | 'general') => (
<HabitListEmptyState
Expand Down Expand Up @@ -1289,6 +1319,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
progressViewOffset={insets.top}
/>
),
// react-doctor-disable-next-line exhaustive-deps -- isFetching/isLoading/refetch are extracted habitsQuery members already listed; the analyzer wants the qualified paths but the aliases track them https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[tokens.primary, insets.top, isFetching, isLoading, refetch],
)

Expand Down Expand Up @@ -1482,19 +1513,9 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(
return (
<>
<FlatList
data={[
'skeleton-1',
'skeleton-2',
'skeleton-3',
'skeleton-4',
'skeleton-5',
]}
data={SKELETON_KEYS}
keyExtractor={(item) => item}
renderItem={() => (
<View style={styles.sectionInset}>
<SkeletonCard styles={styles} />
</View>
)}
renderItem={renderSkeletonItem}
ListHeaderComponent={listHeaderComponent}
contentContainerStyle={[
styles.skeletonContainer,
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/components/habit-list/confirm-dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface HabitListConfirmDialogsProps {
/** The cluster of habit-list confirmation dialogs (delete / duplicate / skip /
* force-log / auto-log-parent). Driven entirely by open-flag props and handlers
* owned by the parent HabitList. */
// react-doctor-disable-next-line no-many-boolean-props -- private single-use dialog aggregator; each flag independently gates one dialog's visibility, not a combinatorial public API https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function HabitListConfirmDialogs({
t,
showDeleteConfirm,
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/components/habit-list/date-group-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface HabitListDateGroup {
habits: NormalizedHabit[]
}

// react-doctor-disable-next-line only-export-components -- co-located date-label helper dedicated to this section; Fast Refresh dev-only, no runtime effect https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function formatDateGroupLabel(
key: string,
locale: string,
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/components/habit-list/empty-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export function SkeletonCard({ styles: cardStyles }: Readonly<{ styles: Skeleton
)
}

// react-doctor-disable-next-line only-export-components -- co-located empty-state message helper dedicated to this module; Fast Refresh dev-only, no runtime effect https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function getEmptyHabitsMessage(
view: 'today' | 'all' | 'general',
t: (key: string) => string,
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/components/habit-list/move-parent-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function MoveTargetRow({
>
<View style={styles.moveOptionHeader}>
{Array.from({ length: option.depth }, (_, index) => (
// react-doctor-disable-next-line no-array-index-as-key -- decorative indent rails: identical stateless spacers keyed by position, with no data identity to preserve https://github.com/thomasluizon/orbit-ui-mobile/issues/243
<View key={index} style={styles.rail}>
<View style={styles.railLine} />
</View>
Expand Down Expand Up @@ -199,6 +200,7 @@ export function MoveParentDialog({
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* react-doctor-disable-next-line rn-no-scrollview-mapped-list -- bounded move-target picker inside a bottom sheet; search collapses long lists (threshold 8) and nesting a VirtualizedList in the sheet ScrollView is discouraged https://github.com/thomasluizon/orbit-ui-mobile/issues/243 */}
{treeRows.map((option) => (
<MoveTargetRow
key={option.id}
Expand Down
48 changes: 27 additions & 21 deletions apps/mobile/components/habits/checklist-templates.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCallback, useMemo, useState } from 'react'
import {
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { Plus, X } from 'lucide-react-native'
Expand Down Expand Up @@ -89,17 +89,19 @@ export function ChecklistTemplates({
<View style={styles.container}>
<View style={styles.actionsRow}>
{items.length > 0 && !showSave ? (
<TouchableOpacity
style={styles.saveChip}
<Pressable
style={({ pressed }) => [
styles.saveChip,
pressed ? { opacity: 0.8 } : null,
]}
onPress={() => setShowSave(true)}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel={t('habits.form.saveAsTemplate')}
accessibilityHint={t('habits.form.templateNamePlaceholder')}
>
<Plus size={14} color={tokens.fg2} strokeWidth={2} />
<Text style={styles.saveChipText}>{t('habits.form.saveAsTemplate')}</Text>
</TouchableOpacity>
</Pressable>
) : null}

{templates.length > 0 ? (
Expand All @@ -108,31 +110,33 @@ export function ChecklistTemplates({
<View style={styles.chipsRow}>
{templates.map((template) => (
<View key={template.id} style={styles.chip}>
<TouchableOpacity
style={styles.chipLoadButton}
<Pressable
style={({ pressed }) => [
styles.chipLoadButton,
pressed ? { opacity: 0.8 } : null,
]}
onPress={() => handleLoad(template.id)}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel={template.name}
accessibilityHint={t('habits.form.templates')}
>
<Text style={styles.chipText}>{template.name}</Text>
</TouchableOpacity>
<TouchableOpacity
</Pressable>
<Pressable
accessibilityLabel={t('common.delete')}
accessibilityRole="button"
accessibilityHint={template.name}
accessibilityState={{ disabled: isDeletingThisTemplate(template.id) }}
style={[
style={({ pressed }) => [
styles.chipDeleteButton,
isDeletingThisTemplate(template.id) && styles.chipDeleteButtonDisabled,
pressed ? { opacity: 0.8 } : null,
]}
onPress={() => handleDelete(template.id)}
disabled={isDeletingThisTemplate(template.id)}
activeOpacity={0.8}
>
<X size={13} color={tokens.fg3} strokeWidth={1.8} />
</TouchableOpacity>
</Pressable>
</View>
))}
</View>
Expand All @@ -152,32 +156,34 @@ export function ChecklistTemplates({
onSubmitEditing={handleSave}
returnKeyType="done"
/>
<TouchableOpacity
style={[
<Pressable
style={({ pressed }) => [
styles.saveButton,
(!templateName.trim() || createTemplate.isPending) && styles.saveButtonDisabled,
pressed ? { opacity: 0.8 } : null,
]}
onPress={handleSave}
disabled={!templateName.trim() || createTemplate.isPending}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel={t('common.save')}
accessibilityState={{ disabled: !templateName.trim() || createTemplate.isPending }}
>
<Text style={styles.saveButtonText}>{t('common.save')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.closeButton}
</Pressable>
<Pressable
style={({ pressed }) => [
styles.closeButton,
pressed ? { opacity: 0.8 } : null,
]}
onPress={() => {
setTemplateName('')
setShowSave(false)
}}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel={t('common.close')}
>
<X size={16} color={tokens.fg3} strokeWidth={1.8} />
</TouchableOpacity>
</Pressable>
</View>
) : null}
</View>
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/components/habits/create-habit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ interface CreateHabitModalProps {
parentHabit?: NormalizedHabit | null
}

// react-doctor-disable-next-line no-giant-component -- form-modal shell already decomposed into create-habit-modal/* and HabitFormFields subcomponents; the remaining body is cohesive submit/suggest/reset orchestration, extraction deferred to avoid regression without device QA https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function CreateHabitModal({
open,
onClose,
Expand Down Expand Up @@ -146,6 +147,7 @@ export function CreateHabitModal({

useEffect(() => {
if (!open || !isSubHabitMode || !profile || profile.hasProAccess) return
// react-doctor-disable-next-line no-prop-callback-in-effect -- access gate: closes the sub-habit modal and redirects non-pro users to /upgrade; a side-effecting gate, not a state sync to the parent https://github.com/thomasluizon/orbit-ui-mobile/issues/243
onClose()
router.push('/upgrade')
}, [isSubHabitMode, onClose, open, profile, router])
Expand Down Expand Up @@ -298,6 +300,7 @@ export function CreateHabitModal({
),
)
}
// react-doctor-disable-next-line exhaustive-deps -- hasProAccess is derived from profile.hasProAccess every render and already listed; no staleness possible https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [
formHelpers,
isSubHabitMode,
Expand Down Expand Up @@ -360,6 +363,7 @@ export function CreateHabitModal({
: t('habits.form.aiSuggestError'),
)
}
// react-doctor-disable-next-line exhaustive-deps -- hasProAccess is derived from profile.hasProAccess every render and already listed; no staleness possible https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [formHelpers, hasProAccess, i18n.language, showError, showInfo, showSuccess, suggestion, t])

const isPending = createHabit.isPending || createSubHabit.isPending
Expand Down
Loading
Loading