@@ -56,6 +56,7 @@ class PollOptionListItem extends StatelessWidget {
5656 super .key,
5757 required this .option,
5858 this .hintText,
59+ this .focusNode,
5960 this .onChanged,
6061 });
6162
@@ -65,6 +66,9 @@ class PollOptionListItem extends StatelessWidget {
6566 /// Hint to be displayed in the poll option list item.
6667 final String ? hintText;
6768
69+ /// The focus node for the text field.
70+ final FocusNode ? focusNode;
71+
6872 /// Callback called when the poll option item is changed.
6973 final ValueSetter <PollOptionItem >? onChanged;
7074
@@ -90,6 +94,7 @@ class PollOptionListItem extends StatelessWidget {
9094 borderRadius: borderRadius,
9195 errorText: option.error,
9296 errorStyle: theme.optionsTextFieldErrorStyle,
97+ focusNode: focusNode,
9398 onChanged: (text) => onChanged? .call (option.copyWith (text: text)),
9499 ),
95100 ),
@@ -115,6 +120,7 @@ class PollOptionReorderableListView extends StatefulWidget {
115120 this .itemHintText,
116121 this .allowDuplicate = false ,
117122 this .initialOptions = const [],
123+ this .optionsRange,
118124 this .onOptionsChanged,
119125 });
120126
@@ -132,6 +138,12 @@ class PollOptionReorderableListView extends StatefulWidget {
132138 /// The initial list of poll options.
133139 final List <PollOptionItem > initialOptions;
134140
141+ /// The range of allowed options (min and max).
142+ ///
143+ /// If `null` , there are no limits. If only min or max is specified,
144+ /// the other bound is unlimited.
145+ final Range <int >? optionsRange;
146+
135147 /// Callback called when the items are updated or reordered.
136148 final ValueSetter <List <PollOptionItem >>? onOptionsChanged;
137149
@@ -142,9 +154,59 @@ class PollOptionReorderableListView extends StatefulWidget {
142154
143155class _PollOptionReorderableListViewState
144156 extends State <PollOptionReorderableListView > {
145- late var _options = < String , PollOptionItem > {
146- for (final option in widget.initialOptions) option.id: option,
147- };
157+ late Map <String , FocusNode > _focusNodes;
158+ late Map <String , PollOptionItem > _options;
159+
160+ @override
161+ void initState () {
162+ super .initState ();
163+ _initializeOptions ();
164+ }
165+
166+ @override
167+ void dispose () {
168+ _disposeOptions ();
169+ super .dispose ();
170+ }
171+
172+ void _initializeOptions () {
173+ _focusNodes = < String , FocusNode > {};
174+ _options = < String , PollOptionItem > {};
175+
176+ for (final option in widget.initialOptions) {
177+ _options[option.id] = option;
178+ _focusNodes[option.id] = FocusNode ();
179+ }
180+
181+ // Ensure we have at least the minimum number of options
182+ _ensureMinimumOptions (notifyParent: true );
183+ }
184+
185+ void _ensureMinimumOptions ({bool notifyParent = false }) {
186+ // Ensure we have at least the minimum number of options
187+ final minOptions = widget.optionsRange? .min ?? 1 ;
188+
189+ var optionsAdded = false ;
190+ while (_options.length < minOptions) {
191+ final option = PollOptionItem ();
192+ _options[option.id] = option;
193+ _focusNodes[option.id] = FocusNode ();
194+ optionsAdded = true ;
195+ }
196+
197+ // Notify parent if we added options and it's requested
198+ if (optionsAdded && notifyParent) {
199+ WidgetsBinding .instance.addPostFrameCallback ((_) {
200+ widget.onOptionsChanged? .call ([..._options.values]);
201+ });
202+ }
203+ }
204+
205+ void _disposeOptions () {
206+ _focusNodes.values.forEach ((it) => it.dispose ());
207+ _focusNodes.clear ();
208+ _options.clear ();
209+ }
148210
149211 @override
150212 void didUpdateWidget (covariant PollOptionReorderableListView oldWidget) {
@@ -159,9 +221,8 @@ class _PollOptionReorderableListViewState
159221 );
160222
161223 if (optionItemEquality.equals (currOptions, newOptions) case false ) {
162- _options = < String , PollOptionItem > {
163- for (final option in widget.initialOptions) option.id: option,
164- };
224+ _disposeOptions ();
225+ _initializeOptions ();
165226 }
166227 }
167228
@@ -216,8 +277,10 @@ class _PollOptionReorderableListViewState
216277
217278 return value.copyWith (error: _validateOption (value));
218279 });
280+ });
219281
220- // Notify the parent widget about the change
282+ // Notify the parent widget about the change
283+ WidgetsBinding .instance.addPostFrameCallback ((_) {
221284 widget.onOptionsChanged? .call ([..._options.values]);
222285 });
223286 }
@@ -234,23 +297,50 @@ class _PollOptionReorderableListViewState
234297 _options = < String , PollOptionItem > {
235298 for (final option in options) option.id: option,
236299 };
300+ });
237301
238- // Notify the parent widget about the change
302+ // Notify the parent widget about the change
303+ WidgetsBinding .instance.addPostFrameCallback ((_) {
239304 widget.onOptionsChanged? .call ([..._options.values]);
240305 });
241306 }
242307
243308 void _onAddOptionPressed () {
309+ // Check if we've reached the maximum number of options allowed
310+ if (widget.optionsRange? .max case final maxOptions? ) {
311+ // Don't add more options if we've reached the limit
312+ if (_options.length >= maxOptions) return ;
313+ }
314+
315+ // Create a new option item
316+ final option = PollOptionItem ();
317+
244318 setState (() {
245- // Create a new option and add it to the map.
246- final option = PollOptionItem ();
247319 _options[option.id] = option;
320+ _focusNodes[option.id] = FocusNode ();
321+ });
248322
249- // Notify the parent widget about the change
323+ // Notify the parent widget about the change and request focus on the
324+ // newly added option text field.
325+ WidgetsBinding .instance.addPostFrameCallback ((_) {
250326 widget.onOptionsChanged? .call ([..._options.values]);
327+ _focusNodes[option.id]? .requestFocus ();
251328 });
252329 }
253330
331+ bool get _canAddMoreOptions {
332+ // Don't allow adding if there's already an empty option
333+ final hasEmptyOption = _options.values.any ((it) => it.text.isEmpty);
334+ if (hasEmptyOption) return false ;
335+
336+ // Check max options limit
337+ if (widget.optionsRange? .max case final maxOptions? ) {
338+ return _options.length < maxOptions;
339+ }
340+
341+ return true ;
342+ }
343+
254344 @override
255345 Widget build (BuildContext context) {
256346 final theme = StreamPollCreatorTheme .of (context);
@@ -271,13 +361,15 @@ class _PollOptionReorderableListViewState
271361 physics: const NeverScrollableScrollPhysics (),
272362 proxyDecorator: _proxyDecorator,
273363 separatorBuilder: (_, __) => const SizedBox (height: 8 ),
364+ onReorderStart: (_) => FocusScope .of (context).unfocus (),
274365 onReorder: _onOptionReorder,
275366 itemBuilder: (context, index) {
276367 final option = _options.values.elementAt (index);
277368 return PollOptionListItem (
278369 key: Key (option.id),
279370 option: option,
280371 hintText: widget.itemHintText,
372+ focusNode: _focusNodes[option.id],
281373 onChanged: _onOptionChanged,
282374 );
283375 },
@@ -287,7 +379,7 @@ class _PollOptionReorderableListViewState
287379 SizedBox (
288380 width: double .infinity,
289381 child: FilledButton .tonal (
290- onPressed: _onAddOptionPressed,
382+ onPressed: _canAddMoreOptions ? _onAddOptionPressed : null ,
291383 style: TextButton .styleFrom (
292384 alignment: Alignment .centerLeft,
293385 textStyle: theme.optionsTextFieldStyle,
0 commit comments