fix: poll creator modal — replace non-existent CSS vars with folds tokens

The modal was built with raw <div>/<input> and CSS custom properties
(--bg-surface, --bg-surface-low, --tc-surface-high, etc.) that don't
exist in Cinny's vanilla-extract theme, causing invisible/unstyled
inputs and a transparent background.

Rewrite to match the ReportRoomModal pattern:
- Overlay + OverlayBackdrop + OverlayCenter for the backdrop
- FocusTrap (clickOutsideDeactivates, escapeDeactivates via stopPropagation)
- Box as="form" with color.Surface.Container background and color.Other.Shadow
- Header variant="Surface" for the title bar
- folds Input variant="Background" for all text fields (replaces raw <input>)
- color.Critical.Main for error text
- Spinner before prop on submit button while submitting
- All spacing/radii from config.* tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:03:45 -04:00
parent 3eabc8f4dd
commit 194d52a808
+119 -104
View File
@@ -1,7 +1,24 @@
import React, { useState } from 'react'; import React, { FormEventHandler, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Button,
Header,
Icon,
IconButton,
Icons,
Input,
Overlay,
OverlayBackdrop,
OverlayCenter,
Spinner,
Text,
color,
config,
} from 'folds';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import { Box, Button, Icon, IconButton, Icons, Text, config } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { stopPropagation } from '../../utils/keyboard';
interface PollCreatorProps { interface PollCreatorProps {
roomId: string; roomId: string;
@@ -35,7 +52,10 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
setOptions((prev) => prev.filter((_, i) => i !== index)); setOptions((prev) => prev.filter((_, i) => i !== index));
}; };
const handleSubmit = async () => { const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
if (submitting) return;
const trimmedQuestion = question.trim(); const trimmedQuestion = question.trim();
if (!trimmedQuestion) { if (!trimmedQuestion) {
setError('Please enter a question.'); setError('Please enter a question.');
@@ -68,131 +88,116 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
}; };
return ( return (
<div <Overlay open backdrop={<OverlayBackdrop />}>
style={{ <OverlayCenter>
position: 'fixed', <FocusTrap
inset: 0, focusTrapOptions={{
zIndex: 1000, initialFocus: false,
display: 'flex', onDeactivate: onClose,
alignItems: 'center', clickOutsideDeactivates: true,
justifyContent: 'center', escapeDeactivates: stopPropagation,
background: 'rgba(0,0,0,0.5)',
}}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}} }}
> >
<div <Box
as="form"
role="dialog"
aria-modal="true"
aria-labelledby="poll-creator-title"
onSubmit={handleSubmit}
direction="Column"
style={{ style={{
background: 'var(--bg-surface)', background: color.Surface.Container,
borderRadius: config.radii.R400, borderRadius: config.radii.R400,
padding: config.space.S500, boxShadow: color.Other.Shadow,
width: '100%', width: '100vw',
maxWidth: '420px', maxWidth: 440,
display: 'flex', overflow: 'hidden',
flexDirection: 'column',
gap: config.space.S300,
boxShadow: '0 8px 32px rgba(0,0,0,0.24)',
}} }}
> >
<Box direction="Row" alignItems="Center" justifyContent="SpaceBetween"> {/* Header */}
<Text size="H4">Create Poll</Text> <Header
<IconButton variant="Surface"
size="300" size="500"
radii="300"
variant="SurfaceVariant"
onClick={onClose}
aria-label="Close poll creator"
>
<Icon src={Icons.Cross} size="100" />
</IconButton>
</Box>
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}>
<Text size="L400">Question</Text>
<input
style={{ style={{
background: 'var(--bg-surface-low)', padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
border: '1px solid var(--bg-surface-border)', borderBottomWidth: config.borderWidth.B300,
borderRadius: config.radii.R300,
padding: `${config.space.S200} ${config.space.S300}`,
color: 'var(--tc-surface-high)',
fontSize: '14px',
outline: 'none',
width: '100%',
boxSizing: 'border-box',
}} }}
type="text" >
placeholder="Ask a question..." <Box grow="Yes">
<Text id="poll-creator-title" size="H4">
Create Poll
</Text>
</Box>
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
{/* Body */}
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
{/* Question */}
<Box direction="Column" gap="100">
<Text as="label" htmlFor="poll-question" size="L400">
Question
</Text>
<Input
id="poll-question"
variant="Background"
placeholder="Ask a question…"
value={question} value={question}
onChange={(e) => setQuestion(e.target.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQuestion(e.target.value)}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus autoFocus
/> />
</div> </Box>
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}> {/* Options */}
<Box direction="Column" gap="200">
<Text size="L400">Options</Text> <Text size="L400">Options</Text>
{options.map((opt, index) => ( {options.map((opt, index) => (
<div // eslint-disable-next-line react/no-array-index-key
key={index} <Box key={index} alignItems="Center" gap="200">
style={{ <Input
display: 'flex', style={{ flex: 1 }}
alignItems: 'center', variant="Background"
gap: config.space.S100,
}}
>
<input
style={{
flex: 1,
background: 'var(--bg-surface-low)',
border: '1px solid var(--bg-surface-border)',
borderRadius: config.radii.R300,
padding: `${config.space.S200} ${config.space.S300}`,
color: 'var(--tc-surface-high)',
fontSize: '14px',
outline: 'none',
boxSizing: 'border-box',
}}
type="text"
placeholder={`Option ${index + 1}`} placeholder={`Option ${index + 1}`}
value={opt} value={opt}
onChange={(e) => handleOptionChange(index, e.target.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleOptionChange(index, e.target.value)
}
aria-label={`Option ${index + 1}`}
/> />
<IconButton <IconButton
size="300" size="300"
radii="300" radii="300"
variant="SurfaceVariant" variant="SurfaceVariant"
type="button"
onClick={() => handleRemoveOption(index)} onClick={() => handleRemoveOption(index)}
disabled={options.length <= 2} disabled={options.length <= 2}
aria-label={`Remove option ${index + 1}`} aria-label={`Remove option ${index + 1}`}
> >
<Icon src={Icons.Cross} size="100" /> <Icon src={Icons.Cross} size="100" />
</IconButton> </IconButton>
</div> </Box>
))} ))}
{options.length < 10 && ( {options.length < 10 && (
<button <Box>
<Button
type="button" type="button"
size="300"
variant="Secondary"
fill="None"
radii="300"
before={<Icon src={Icons.Plus} size="100" />}
onClick={handleAddOption} onClick={handleAddOption}
style={{
display: 'flex',
alignItems: 'center',
gap: config.space.S100,
background: 'none',
border: 'none',
cursor: 'pointer',
color: 'var(--tc-surface-low)',
fontSize: '13px',
padding: `${config.space.S100} 0`,
alignSelf: 'flex-start',
}}
> >
<Icon src={Icons.Plus} size="100" /> <Text size="B300">Add Option</Text>
<span>Add Option</span> </Button>
</button> </Box>
)} )}
</div> </Box>
{/* Selection type */}
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">Selection Type</Text> <Text size="L400">Selection Type</Text>
<Box gap="200"> <Box gap="200">
@@ -201,49 +206,59 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
return ( return (
<Button <Button
key={type} key={type}
type="button"
size="300" size="300"
variant="Primary" variant="Primary"
fill={active ? 'Solid' : 'None'} fill={active ? 'Solid' : 'None'}
radii="300" radii="300"
onClick={() => setIsMultiple(type === 'multiple')} onClick={() => setIsMultiple(type === 'multiple')}
> >
<Text size="B300">{type === 'single' ? 'Single choice' : 'Multiple choice'}</Text> <Text size="B300">
{type === 'single' ? 'Single choice' : 'Multiple choice'}
</Text>
</Button> </Button>
); );
})} })}
</Box> </Box>
</Box> </Box>
{/* Error */}
{error && ( {error && (
<Text size="T200" style={{ color: 'var(--tc-danger-normal)' }}> <Text size="T300" style={{ color: color.Critical.Main }}>
{error} {error}
</Text> </Text>
)} )}
<Box direction="Row" justifyContent="End" gap="200"> {/* Footer actions */}
<Box gap="200" justifyContent="End">
<Button <Button
type="button"
size="400" size="400"
variant="Secondary" variant="Secondary"
fill="Soft" fill="None"
radii="300" radii="300"
onClick={onClose} onClick={onClose}
type="button"
> >
<Text size="B400">Cancel</Text> <Text size="B400">Cancel</Text>
</Button> </Button>
<Button <Button
type="submit"
size="400" size="400"
variant="Primary" variant="Primary"
fill="Solid" fill="Solid"
radii="300" radii="300"
onClick={handleSubmit} aria-disabled={submitting}
type="button" before={
disabled={submitting} submitting ? <Spinner variant="Primary" fill="Solid" size="200" /> : undefined
}
> >
<Text size="B400">{submitting ? 'Creating…' : 'Create Poll'}</Text> <Text size="B400">{submitting ? 'Creating…' : 'Create Poll'}</Text>
</Button> </Button>
</Box> </Box>
</div> </Box>
</div> </Box>
</FocusTrap>
</OverlayCenter>
</Overlay>
); );
} }