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
+183 -168
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 { Box, Button, Icon, IconButton, Icons, Text, config } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { stopPropagation } from '../../utils/keyboard';
interface PollCreatorProps {
roomId: string;
@@ -35,7 +52,10 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
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();
if (!trimmedQuestion) {
setError('Please enter a question.');
@@ -68,182 +88,177 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
};
return (
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.5)',
}}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
style={{
background: 'var(--bg-surface)',
borderRadius: config.radii.R400,
padding: config.space.S500,
width: '100%',
maxWidth: '420px',
display: 'flex',
flexDirection: 'column',
gap: config.space.S300,
boxShadow: '0 8px 32px rgba(0,0,0,0.24)',
}}
>
<Box direction="Row" alignItems="Center" justifyContent="SpaceBetween">
<Text size="H4">Create Poll</Text>
<IconButton
size="300"
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
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: onClose,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Box
as="form"
role="dialog"
aria-modal="true"
aria-labelledby="poll-creator-title"
onSubmit={handleSubmit}
direction="Column"
style={{
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',
width: '100%',
boxSizing: 'border-box',
background: color.Surface.Container,
borderRadius: config.radii.R400,
boxShadow: color.Other.Shadow,
width: '100vw',
maxWidth: 440,
overflow: 'hidden',
}}
type="text"
placeholder="Ask a question..."
value={question}
onChange={(e) => setQuestion(e.target.value)}
autoFocus
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}>
<Text size="L400">Options</Text>
{options.map((opt, index) => (
<div
key={index}
>
{/* Header */}
<Header
variant="Surface"
size="500"
style={{
display: 'flex',
alignItems: 'center',
gap: config.space.S100,
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
>
<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}`}
value={opt}
onChange={(e) => handleOptionChange(index, e.target.value)}
/>
<IconButton
size="300"
radii="300"
variant="SurfaceVariant"
onClick={() => handleRemoveOption(index)}
disabled={options.length <= 2}
aria-label={`Remove option ${index + 1}`}
>
<Icon src={Icons.Cross} size="100" />
<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>
</div>
))}
{options.length < 10 && (
<button
type="button"
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" />
<span>Add Option</span>
</button>
)}
</div>
</Header>
<Box direction="Column" gap="100">
<Text size="L400">Selection Type</Text>
<Box gap="200">
{(['single', 'multiple'] as const).map((type) => {
const active = type === 'multiple' ? isMultiple : !isMultiple;
return (
{/* 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}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQuestion(e.target.value)}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
</Box>
{/* Options */}
<Box direction="Column" gap="200">
<Text size="L400">Options</Text>
{options.map((opt, index) => (
// eslint-disable-next-line react/no-array-index-key
<Box key={index} alignItems="Center" gap="200">
<Input
style={{ flex: 1 }}
variant="Background"
placeholder={`Option ${index + 1}`}
value={opt}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleOptionChange(index, e.target.value)
}
aria-label={`Option ${index + 1}`}
/>
<IconButton
size="300"
radii="300"
variant="SurfaceVariant"
type="button"
onClick={() => handleRemoveOption(index)}
disabled={options.length <= 2}
aria-label={`Remove option ${index + 1}`}
>
<Icon src={Icons.Cross} size="100" />
</IconButton>
</Box>
))}
{options.length < 10 && (
<Box>
<Button
type="button"
size="300"
variant="Secondary"
fill="None"
radii="300"
before={<Icon src={Icons.Plus} size="100" />}
onClick={handleAddOption}
>
<Text size="B300">Add Option</Text>
</Button>
</Box>
)}
</Box>
{/* Selection type */}
<Box direction="Column" gap="100">
<Text size="L400">Selection Type</Text>
<Box gap="200">
{(['single', 'multiple'] as const).map((type) => {
const active = type === 'multiple' ? isMultiple : !isMultiple;
return (
<Button
key={type}
type="button"
size="300"
variant="Primary"
fill={active ? 'Solid' : 'None'}
radii="300"
onClick={() => setIsMultiple(type === 'multiple')}
>
<Text size="B300">
{type === 'single' ? 'Single choice' : 'Multiple choice'}
</Text>
</Button>
);
})}
</Box>
</Box>
{/* Error */}
{error && (
<Text size="T300" style={{ color: color.Critical.Main }}>
{error}
</Text>
)}
{/* Footer actions */}
<Box gap="200" justifyContent="End">
<Button
key={type}
size="300"
variant="Primary"
fill={active ? 'Solid' : 'None'}
type="button"
size="400"
variant="Secondary"
fill="None"
radii="300"
onClick={() => setIsMultiple(type === 'multiple')}
onClick={onClose}
>
<Text size="B300">{type === 'single' ? 'Single choice' : 'Multiple choice'}</Text>
<Text size="B400">Cancel</Text>
</Button>
);
})}
<Button
type="submit"
size="400"
variant="Primary"
fill="Solid"
radii="300"
aria-disabled={submitting}
before={
submitting ? <Spinner variant="Primary" fill="Solid" size="200" /> : undefined
}
>
<Text size="B400">{submitting ? 'Creating…' : 'Create Poll'}</Text>
</Button>
</Box>
</Box>
</Box>
</Box>
{error && (
<Text size="T200" style={{ color: 'var(--tc-danger-normal)' }}>
{error}
</Text>
)}
<Box direction="Row" justifyContent="End" gap="200">
<Button
size="400"
variant="Secondary"
fill="Soft"
radii="300"
onClick={onClose}
type="button"
>
<Text size="B400">Cancel</Text>
</Button>
<Button
size="400"
variant="Primary"
fill="Solid"
radii="300"
onClick={handleSubmit}
type="button"
disabled={submitting}
>
<Text size="B400">{submitting ? 'Creating…' : 'Create Poll'}</Text>
</Button>
</Box>
</div>
</div>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}