feat(polls): let creators set max selections for multiple-choice

The poll creator only offered single (max_selections 1) or multiple = pick ALL
options — no way to run a "pick your top 2" poll, even though the display side
already enforces an arbitrary max_selections ("Select up to N"). Add a "Voters
can pick up to N of M options" control shown for multiple-choice polls. Defaults
to the option count (preserving the old select-all behavior) until lowered;
clamped to [2, filled option count] on submit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 16:06:32 -04:00
co-authored by Claude Opus 4.8
parent 85ac8de5d9
commit a4660a8163
2 changed files with 34 additions and 1 deletions
+1
View File
@@ -866,6 +866,7 @@ player.kick).
- `PollCreator.tsx` creates stable `m.poll.start` events (with a text fallback body for non-poll clients)
- Supports 2 to 10 answer options; single-choice or multiple-choice
- **Max selections** — for a multiple-choice poll, a "Voters can pick up to N of M options" control sets `max_selections` (2 … option count), so you can run "pick your top 2" polls rather than only "select all". Defaults to the option count (unchanged "select all that apply" behavior) until you lower it; the display side already enforces the cap ("Select up to N")
- **Results visibility toggle** — _Show live results_ (disclosed, default) vs _Hidden until ended_ (undisclosed)
- Accessible via the `Icons.OrderList` button in the composer toolbar
+33 -1
View File
@@ -34,6 +34,10 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
const [question, setQuestion] = useState('');
const [options, setOptions] = useState<string[]>(['', '']);
const [isMultiple, setIsMultiple] = useState(false);
// For multiple-choice polls: the most options a voter may pick. Defaults high
// so an untouched multiple poll means "select all that apply" (the previous
// behavior); the effective value is clamped to the current option count.
const [maxSelections, setMaxSelections] = useState(10);
// Results visibility: disclosed (live results, default) vs undisclosed (hidden
// until the poll is ended).
const [disclosed, setDisclosed] = useState(true);
@@ -85,7 +89,9 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
'm.poll': {
question: { 'm.text': trimmedQuestion },
answers: filledOptions.map((o, i) => ({ 'm.id': `${i}`, 'm.text': o })),
max_selections: isMultiple ? filledOptions.length : 1,
max_selections: isMultiple
? Math.min(Math.max(2, maxSelections), filledOptions.length)
: 1,
kind: disclosed ? 'm.poll.disclosed' : 'm.poll.undisclosed',
},
body: fallbackBody,
@@ -222,6 +228,32 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
);
})}
</Box>
{isMultiple && (
<Box alignItems="Center" gap="200" style={{ marginTop: config.space.S200 }}>
<Text as="label" htmlFor="poll-max-select" size="T200" priority="400">
Voters can pick up to
</Text>
<Input
id="poll-max-select"
variant="Background"
size="300"
type="number"
min={2}
max={options.length}
value={Math.min(maxSelections, options.length)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMaxSelections(
Math.min(options.length, Math.max(2, parseInt(e.target.value, 10) || 2)),
)
}
style={{ width: '4rem' }}
aria-label="Maximum selections per voter"
/>
<Text size="T200" priority="300">
of {options.length} options
</Text>
</Box>
)}
</Box>
{/* Results visibility */}