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
94 changes: 93 additions & 1 deletion __mocks__/platform-bible-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
* without extra transform configuration. This stub provides the subset used by the extension.
*/

import { Children, cloneElement, forwardRef, isValidElement, useEffect, useRef } from 'react';
import {
Children,
cloneElement,
createContext,
forwardRef,
isValidElement,
useContext,
useEffect,
useMemo,
useRef,
} from 'react';
import type {
ChangeEventHandler,
CSSProperties,
Expand Down Expand Up @@ -576,6 +586,88 @@ export function Switch({
);
}

/**
* Context carrying the {@link RadioGroup}'s selected value and change handler down to each
* {@link RadioGroupItem}, mirroring how the real Radix-based component coordinates its items.
*/
const RadioGroupContext = createContext<{
onValueChange?: (value: string) => void;
value?: string;
}>({});

/**
* Stub radio group rendered as a `<div role="radiogroup">` that shares its selected value and change
* handler with its {@link RadioGroupItem} children via context. The real component is built on Radix
* primitives; this stub reproduces just the controlled selection behavior tests drive.
*
* @param props - Component props.
* @param props.children - The {@link RadioGroupItem}s (and any surrounding markup).
* @param props.className - CSS class names.
* @param props.onValueChange - Called with an item's value when it becomes selected.
* @param props.value - Currently selected item value.
* @returns A `<div role="radiogroup">` providing selection context to its items.
*/
export function RadioGroup({
children,
className,
onValueChange,
value,
}: Readonly<{
children?: ReactNode;
className?: string;
onValueChange?: (value: string) => void;
value?: string;
}>): ReactElement {
const contextValue = useMemo(() => ({ onValueChange, value }), [onValueChange, value]);
return (
<div className={className} role="radiogroup">
<RadioGroupContext.Provider value={contextValue}>{children}</RadioGroupContext.Provider>
</div>
);
}

/**
* Stub radio item rendered as a native `<input type="radio">` so `toBeChecked`, `toBeDisabled`, and
* click/`check` interactions work in tests. It reads the enclosing {@link RadioGroup}'s value from
* context, marking itself checked when they match and reporting its own value on change. (The real
* component renders a `<button role="radio">`; a native radio is close enough for these tests and
* plays nicely with jest-dom's checked/disabled matchers.)
*
* @param props - Component props.
* @param props.className - CSS class names.
* @param props['data-testid'] - Test identifier.
* @param props.disabled - Whether the item is disabled.
* @param props.id - HTML `id` attribute (a {@link Label}'s `htmlFor` binds to it).
* @param props.value - This item's value, compared against the group value to derive `checked`.
* @returns A native `<input type="radio">` wired to the group's selection state.
*/
export function RadioGroupItem({
className,
'data-testid': testId,
disabled,
id,
value,
}: Readonly<{
className?: string;
'data-testid'?: string;
disabled?: boolean;
id?: string;
value: string;
}>): ReactElement {
const { onValueChange, value: groupValue } = useContext(RadioGroupContext);
return (
<input
checked={groupValue === value}
className={className}
data-testid={testId}
disabled={disabled}
id={id}
onChange={() => onValueChange?.(value)}
type="radio"
/>
);
}

/**
* Stub popover root that renders its children unconditionally. The extension conditionally mounts
* the content component while open (so its draft state re-initializes per open), so visibility
Expand Down
52 changes: 26 additions & 26 deletions src/components/modals/WipeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useLocalizedStrings } from '@papi/frontend/react';
import { Button } from 'platform-bible-react';
import { Button, Label, RadioGroup, RadioGroupItem } from 'platform-bible-react';
import { useState } from 'react';
import { ModalShell } from './ModalShell';

Expand Down Expand Up @@ -60,40 +60,40 @@ export function WipeModal({
{localizedStrings['%interlinearizer_wipe_modal_prompt%']}
</p>

<div className="tw:flex tw:flex-col tw:gap-3 tw:mb-4">
<label className="tw:flex tw:flex-col tw:gap-0.5 tw:text-sm">
<span className="tw:flex tw:gap-2 tw:items-center tw:font-medium">
<input
type="radio"
name="wipe-scope"
checked={scope === 'book'}
disabled={!hasActiveBook}
onChange={() => setScope('book')}
<RadioGroup
className="tw:mb-4 tw:gap-3"
onValueChange={(value) => setScope(value === 'book' ? 'book' : 'all')}
value={scope}
>
<div className="tw:flex tw:flex-col tw:gap-0.5 tw:text-sm">
<div className="tw:flex tw:gap-2 tw:items-center tw:font-medium">
<RadioGroupItem
data-testid="wipe-scope-book"
disabled={!hasActiveBook}
id="wipe-scope-book"
value="book"
/>
{localizedStrings['%interlinearizer_wipe_modal_scope_book%']}
</span>
<Label htmlFor="wipe-scope-book">
{localizedStrings['%interlinearizer_wipe_modal_scope_book%']}
</Label>
</div>
<span className="tw:ps-6 tw:text-muted-foreground">
{localizedStrings['%interlinearizer_wipe_modal_scope_book_description%']}
</span>
</label>
</div>

<label className="tw:flex tw:flex-col tw:gap-0.5 tw:text-sm">
<span className="tw:flex tw:gap-2 tw:items-center tw:font-medium">
<input
type="radio"
name="wipe-scope"
checked={scope === 'all'}
onChange={() => setScope('all')}
data-testid="wipe-scope-all"
/>
{localizedStrings['%interlinearizer_wipe_modal_scope_all%']}
</span>
<div className="tw:flex tw:flex-col tw:gap-0.5 tw:text-sm">
<div className="tw:flex tw:gap-2 tw:items-center tw:font-medium">
<RadioGroupItem data-testid="wipe-scope-all" id="wipe-scope-all" value="all" />
<Label htmlFor="wipe-scope-all">
{localizedStrings['%interlinearizer_wipe_modal_scope_all%']}
</Label>
</div>
<span className="tw:ps-6 tw:text-muted-foreground">
{localizedStrings['%interlinearizer_wipe_modal_scope_all_description%']}
</span>
</label>
</div>
</div>
</RadioGroup>

<div className="tw:modal-actions">
<Button variant="secondary" onClick={onCancel}>
Expand Down
Loading