Skip to content

Commit 17a9a47

Browse files
hide and reveal frontend
1 parent 4045cda commit 17a9a47

9 files changed

Lines changed: 902 additions & 143 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { RadioGroup } from '@hypothesis/frontend-shared';
2+
3+
/**
4+
* The kind of assignment being created.
5+
*
6+
* - `reading`: a standard "Social annotation" reading assignment.
7+
* - `hide_and_reveal`: "Guided Social annotation" — students' annotations are
8+
* hidden from each other until an instructor reveals them (internally also
9+
* referred to as "Hide & Reveal" / checkpoints).
10+
*/
11+
export type AssignmentType = 'reading' | 'hide_and_reveal';
12+
13+
/** Human-readable label shown in the selector for each assignment type. */
14+
const ASSIGNMENT_TYPE_LABELS: Record<AssignmentType, string> = {
15+
reading: 'Social annotation',
16+
hide_and_reveal: 'Guided Social annotation',
17+
};
18+
19+
export type AssignmentTypeSelectorProps = {
20+
/**
21+
* Assignment types the instructor can choose from, in display order. Decided
22+
* by the backend. A new type also needs an entry in the `AssignmentType`
23+
* union and in `ASSIGNMENT_TYPE_LABELS` to render with a proper label.
24+
*/
25+
types: AssignmentType[];
26+
selected: AssignmentType;
27+
onChange: (type: AssignmentType) => void;
28+
};
29+
30+
/**
31+
* First step of the assignment-type workflow: lets instructors choose which
32+
* kind of assignment they are creating among the available `types`.
33+
*/
34+
export default function AssignmentTypeSelector({
35+
types,
36+
selected,
37+
onChange,
38+
}: AssignmentTypeSelectorProps) {
39+
return (
40+
<div>
41+
<RadioGroup
42+
data-testid="assignment-type-radio-group"
43+
aria-label="Assignment mode"
44+
direction="vertical"
45+
selected={selected}
46+
onChange={onChange}
47+
>
48+
{types.map(type => {
49+
// Fall back to the raw key if the backend sends a type this frontend
50+
// build doesn't know yet (deploy ordering), so the radio is never
51+
// blank. Statically unreachable, but `type` is backend JSON at runtime.
52+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
53+
const label = ASSIGNMENT_TYPE_LABELS[type] ?? type;
54+
return (
55+
<RadioGroup.Radio key={type} value={type}>
56+
{label}
57+
</RadioGroup.Radio>
58+
);
59+
})}
60+
</RadioGroup>
61+
</div>
62+
);
63+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { RadioGroup } from '@hypothesis/frontend-shared';
2+
import { useId } from 'preact/hooks';
3+
4+
/**
5+
* The kind of checkpoint that controls when hidden annotations are revealed.
6+
*
7+
* For now only `manual` is functional (the instructor reveals annotations
8+
* themselves); more options (e.g. a calendar/date-driven checkpoint) are
9+
* planned.
10+
*/
11+
export type CheckpointType = 'manual';
12+
13+
/** Values rendered as radios, including not-yet-available options. */
14+
type CheckpointOption = CheckpointType | 'more';
15+
16+
export type CheckpointSelectorProps = {
17+
selected: CheckpointType;
18+
onChange: (type: CheckpointType) => void;
19+
};
20+
21+
/**
22+
* Second step of the "Hide & Reveal" workflow: lets instructors choose how the
23+
* checkpoint (the point at which hidden annotations are revealed) works.
24+
*/
25+
export default function CheckpointSelector({
26+
selected,
27+
onChange,
28+
}: CheckpointSelectorProps) {
29+
const headingId = useId();
30+
31+
// The note below is specific to the "manual" reveal, so it only shows for that
32+
// option. `selected` is currently always 'manual' (the only enabled option),
33+
// but this keeps the association explicit for when more types are added.
34+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
35+
const showManualNote = selected === 'manual';
36+
37+
return (
38+
<div className="space-y-2">
39+
<h3 id={headingId} className="uppercase font-medium text-slate-600">
40+
Checkpoint
41+
</h3>
42+
<RadioGroup<CheckpointOption>
43+
data-testid="checkpoint-radio-group"
44+
aria-labelledby={headingId}
45+
direction="vertical"
46+
selected={selected}
47+
onChange={option => {
48+
// Ignore the disabled "coming soon" placeholder; anything else is a
49+
// real CheckpointType. Adding a new type needs no change here.
50+
if (option !== 'more') {
51+
onChange(option);
52+
}
53+
}}
54+
>
55+
<RadioGroup.Radio value="manual">Manual</RadioGroup.Radio>
56+
<RadioGroup.Radio value="more" disabled>
57+
More coming soon
58+
</RadioGroup.Radio>
59+
</RadioGroup>
60+
{showManualNote && (
61+
// No color class: inherits the base text color (black) per design.
62+
<p>
63+
Students will see when the settings have changed from
64+
&ldquo;Hide&rdquo; to &ldquo;Reveal&rdquo; in their notifications.
65+
</p>
66+
)}
67+
</div>
68+
);
69+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { IconButton, InfoIcon, Popover } from '@hypothesis/frontend-shared';
2+
import { useId, useRef, useState } from 'preact/hooks';
3+
4+
export type DueDateSelectorProps = {
5+
/** Currently selected due date as an ISO date string (`YYYY-MM-DD`), or null. */
6+
dueDate: string | null;
7+
onChange: (dueDate: string | null) => void;
8+
};
9+
10+
/**
11+
* Third step of the "Hide & Reveal" workflow: lets instructors pick the due
12+
* date, the point at which annotations are no longer tallied in auto grading.
13+
*/
14+
export default function DueDateSelector({
15+
dueDate,
16+
onChange,
17+
}: DueDateSelectorProps) {
18+
const headingId = useId();
19+
20+
// Explanation of the due date, shown in a tooltip (anchored to the info icon)
21+
// rather than inline. Mirrors the "Max points" popover in FilePickerApp.
22+
const infoIconRef = useRef<HTMLButtonElement | null>(null);
23+
const [infoPopoverOpen, setInfoPopoverOpen] = useState(false);
24+
25+
return (
26+
<div className="space-y-2">
27+
<div className="flex items-center gap-x-1">
28+
<h3 id={headingId} className="uppercase font-medium text-slate-600">
29+
Due Date
30+
</h3>
31+
<IconButton
32+
icon={InfoIcon}
33+
title="About due date"
34+
onClick={() => setInfoPopoverOpen(open => !open)}
35+
expanded={infoPopoverOpen}
36+
elementRef={infoIconRef}
37+
classes="text-[16px]"
38+
/>
39+
<Popover
40+
open={infoPopoverOpen}
41+
anchorElementRef={infoIconRef}
42+
onClose={() => setInfoPopoverOpen(false)}
43+
classes="p-2"
44+
placement="above"
45+
arrow
46+
>
47+
The point where annotations are no longer tallied in auto grading.
48+
</Popover>
49+
</div>
50+
<input
51+
type="date"
52+
data-testid="due-date-input"
53+
aria-labelledby={headingId}
54+
// The shared `Input` component does not support `type="date"`, so this
55+
// mirrors its base classes (`inputStyles`) to stay visually consistent,
56+
// including `touch:text-at-least-16px` which prevents iOS zoom-on-focus.
57+
className="focus-visible:ring focus-visible:outline-none ring-inset border rounded w-full p-2 bg-grey-0 focus:bg-white disabled:bg-grey-1 placeholder:text-grey-6 disabled:placeholder:text-grey-7 touch:text-at-least-16px"
58+
value={dueDate ?? ''}
59+
onChange={e => {
60+
const { value } = e.target as HTMLInputElement;
61+
onChange(value === '' ? null : value);
62+
}}
63+
/>
64+
</div>
65+
);
66+
}

0 commit comments

Comments
 (0)