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
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ vi.mock('@orbit/shared/utils', async (importOriginal) => {
})

vi.mock('@/components/habits/habit-form-fields', () => ({
HabitFormFields: ({ children }: { children?: React.ReactNode }) =>
React.createElement('HabitFormFields', null, children),
HabitFormFields: (props: { children?: React.ReactNode }) =>
React.createElement('HabitFormFields', props, props.children),
}))

vi.mock('@/components/ui/pro-badge', () => ({
Expand Down Expand Up @@ -199,4 +199,26 @@ describe('CreateHabitModal (mobile)', () => {
tree.root.findAll((node: any) => node.props?.testID === 'pro-badge'),
).toHaveLength(1)
})

it('disables the submit button until the title has content', () => {
const tree = renderModal(<CreateHabitModal open onClose={vi.fn()} />)

const findSubmit = () =>
tree.root.findAll(
(node: any) =>
node.type === 'TouchableOpacity' &&
node.props.accessibilityLabel === 'habits.createHabit',
)[0]

expect(findSubmit().props.disabled).toBe(true)

const fields = tree.root.findAll(
(node: any) => node.type === 'HabitFormFields',
)[0]
TestRenderer.act(() => {
fields.props.onTitlePresenceChange(true)
})

expect(findSubmit().props.disabled).toBe(false)
})
})
39 changes: 39 additions & 0 deletions apps/mobile/__tests__/components/habits/habit-form-fields.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,43 @@ describe('HabitFormFields (mobile)', () => {
expect(hasText('habits.form.reminder')).toBe(true)
expect(hasText('habits.form.tags')).toBe(true)
})

it('reports title presence to onTitlePresenceChange as the title is typed and cleared', async () => {
const formHelpers = createMockFormHelpers({ title: '' })
const tags = createMockTags()
const onTitlePresenceChange = vi.fn()
let tree: any

await TestRenderer.act(async () => {
tree = TestRenderer.create(
<HabitFormFields
formHelpers={formHelpers}
tags={tags}
selectedGoalIds={[]}
atGoalLimit={false}
onToggleGoal={vi.fn()}
reminderTimes={[]}
onReminderTimesChange={vi.fn()}
onTitlePresenceChange={onTitlePresenceChange}
/>,
)
})

const titleInput = tree.root.findAll(
(node: any) =>
node.type === 'TextInput' &&
node.props.accessibilityLabel === 'habits.form.title',
)[0]
expect(titleInput).toBeTruthy()

await TestRenderer.act(async () => {
titleInput.props.onChangeText('Read a book')
})
expect(onTitlePresenceChange).toHaveBeenLastCalledWith(true)

await TestRenderer.act(async () => {
titleInput.props.onChangeText(' ')
})
expect(onTitlePresenceChange).toHaveBeenLastCalledWith(false)
})
})
5 changes: 4 additions & 1 deletion apps/mobile/components/habits/create-habit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function CreateHabitModal({
const [selectedGoalIds, setSelectedGoalIds] = useState<string[]>([])
const [subHabits, setSubHabits] = useState<SubHabitEntry[]>([])
const [reminderTimes, setReminderTimes] = useState<number[]>([0, 15])
const [titleFilled, setTitleFilled] = useState(false)
const [reminderWasManuallyToggled, setReminderWasManuallyToggled] = useState(false)
const flushBufferedInputsRef = useRef<() => void>(() => {})
const [initialTagIdsSnapshot, setInitialTagIdsSnapshot] = useState('[]')
Expand Down Expand Up @@ -145,6 +146,7 @@ export function CreateHabitModal({
const fallbackDate = initialDate ?? formatAPIDate(new Date())

setReminderWasManuallyToggled(false)
setTitleFilled(false)
formHelpers.form.reset(buildEmptyHabitFormValues(fallbackDate))
tags.resetTags()
setSelectedGoalIds([])
Expand Down Expand Up @@ -312,7 +314,7 @@ export function CreateHabitModal({
])

const isPending = createHabit.isPending || createSubHabit.isPending
const submitDisabled = isPending || !formHelpers.form.formState.isValid
const submitDisabled = isPending || !titleFilled

const updateSubHabitValue = useCallback((id: string, value: string) => {
setSubHabits((prev) =>
Expand Down Expand Up @@ -357,6 +359,7 @@ export function CreateHabitModal({
onReminderTimesChange={setReminderTimes}
onReminderEnabledChange={handleReminderEnabledChange}
onFlushBufferedInputsReady={handleBufferedInputsReady}
onTitlePresenceChange={setTitleFilled}
>
{!isSubHabitMode ? (
<SubHabitEditor
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/components/habits/edit-habit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function EditHabitModal({
const [initialTagIds, setInitialTagIds] = useState('[]')
const [initialGoalIds, setInitialGoalIds] = useState('[]')
const [initialReminderTimes, setInitialReminderTimes] = useState('[0,15]')
const [titleFilled, setTitleFilled] = useState(false)

const atGoalLimit = selectedGoalIds.length >= 10
const isDirty =
Expand Down Expand Up @@ -123,6 +124,7 @@ export function EditHabitModal({
if (open && habit) {
const prefill = buildEditHabitFormState(habit, habitDetail)
formHelpers.form.reset(prefill.formValues)
setTitleFilled(prefill.formValues.title.trim().length > 0)
setOriginalEndDate(prefill.originalEndDate)
setReminderTimes(prefill.reminderTimes)
tags.resetTags(prefill.selectedTagIds)
Expand Down Expand Up @@ -201,7 +203,7 @@ export function EditHabitModal({
translate,
])

const submitDisabled = updateHabit.isPending || !formHelpers.form.formState.isValid
const submitDisabled = updateHabit.isPending || !titleFilled

return (
<>
Expand Down Expand Up @@ -229,6 +231,7 @@ export function EditHabitModal({
reminderTimes={reminderTimes}
onReminderTimesChange={setReminderTimes}
onFlushBufferedInputsReady={handleBufferedInputsReady}
onTitlePresenceChange={setTitleFilled}
defaultExpanded={true}
/>

Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/components/habits/habit-form-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface HabitFormFieldsProps {
onReminderTimesChange: (times: number[]) => void;
onReminderEnabledChange?: (nextEnabled: boolean) => void;
onFlushBufferedInputsReady?: (flush: () => void) => void;
onTitlePresenceChange?: (hasTitle: boolean) => void;
/** When true, advanced fields are visible by default (used in edit modal) */
defaultExpanded?: boolean;
children?: ReactNode;
Expand All @@ -53,6 +54,7 @@ export function HabitFormFields({
onReminderTimesChange,
onReminderEnabledChange,
onFlushBufferedInputsReady,
onTitlePresenceChange,
defaultExpanded = false,
children,
}: Readonly<HabitFormFieldsProps>) {
Expand Down Expand Up @@ -135,6 +137,7 @@ export function HabitFormFields({
error={errors.title?.message}
registerFlush={registerBufferedInputFlusher}
onCommit={(val) => setValue("title", val, { shouldDirty: true })}
onDraftChange={(val) => onTitlePresenceChange?.(val.trim().length > 0)}
styles={styles}
tokens={tokens}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface TitleSectionProps {
error: string | undefined;
registerFlush: (flush: () => void) => () => void;
onCommit: (value: string) => void;
onDraftChange?: (value: string) => void;
styles: HabitFormStyles;
tokens: AppTokens;
}
Expand All @@ -19,6 +20,7 @@ export function TitleSection({
error,
registerFlush,
onCommit,
onDraftChange,
styles,
tokens,
}: Readonly<TitleSectionProps>) {
Expand All @@ -36,6 +38,7 @@ export function TitleSection({
placeholderTextColor={tokens.fg3}
style={styles.input}
onCommit={onCommit}
onDraftChange={onDraftChange}
accessibilityLabel={t("habits.form.title")}
/>
{error && (
Expand Down
Loading