fix: poll creator modal — replace non-existent CSS vars with folds tokens
CI / Build & Quality Checks (push) Successful in 10m17s

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 715cd0076f
commit 6d028e3749
+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 { 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,182 +88,177 @@ 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) => { <Box
if (e.target === e.currentTarget) onClose(); as="form"
}} role="dialog"
> aria-modal="true"
<div aria-labelledby="poll-creator-title"
style={{ onSubmit={handleSubmit}
background: 'var(--bg-surface)', direction="Column"
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
style={{ style={{
background: 'var(--bg-surface-low)', background: color.Surface.Container,
border: '1px solid var(--bg-surface-border)', borderRadius: config.radii.R400,
borderRadius: config.radii.R300, boxShadow: color.Other.Shadow,
padding: `${config.space.S200} ${config.space.S300}`, width: '100vw',
color: 'var(--tc-surface-high)', maxWidth: 440,
fontSize: '14px', overflow: 'hidden',
outline: 'none',
width: '100%',
boxSizing: 'border-box',
}} }}
type="text" >
placeholder="Ask a question..." {/* Header */}
value={question} <Header
onChange={(e) => setQuestion(e.target.value)} variant="Surface"
autoFocus size="500"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}>
<Text size="L400">Options</Text>
{options.map((opt, index) => (
<div
key={index}
style={{ style={{
display: 'flex', padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
alignItems: 'center', borderBottomWidth: config.borderWidth.B300,
gap: config.space.S100,
}} }}
> >
<input <Box grow="Yes">
style={{ <Text id="poll-creator-title" size="H4">
flex: 1, Create Poll
background: 'var(--bg-surface-low)', </Text>
border: '1px solid var(--bg-surface-border)', </Box>
borderRadius: config.radii.R300, <IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
padding: `${config.space.S200} ${config.space.S300}`, <Icon src={Icons.Cross} />
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" />
</IconButton> </IconButton>
</div> </Header>
))}
{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>
<Box direction="Column" gap="100"> {/* Body */}
<Text size="L400">Selection Type</Text> <Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Box gap="200"> {/* Question */}
{(['single', 'multiple'] as const).map((type) => { <Box direction="Column" gap="100">
const active = type === 'multiple' ? isMultiple : !isMultiple; <Text as="label" htmlFor="poll-question" size="L400">
return ( 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 <Button
key={type} type="button"
size="300" size="400"
variant="Primary" variant="Secondary"
fill={active ? 'Solid' : 'None'} fill="None"
radii="300" 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>
); <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>
</Box> </FocusTrap>
</OverlayCenter>
{error && ( </Overlay>
<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>
); );
} }