@@ -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
@@ -241,14 +302,41 @@ class _PollOptionReorderableListViewState
241302 }
242303
243304 void _onAddOptionPressed () {
305+ // Check if we've reached the maximum number of options allowed
306+ if (widget.optionsRange? .max case final maxOptions? ) {
307+ // Don't add more options if we've reached the limit
308+ if (_options.length >= maxOptions) return ;
309+ }
310+
244311 setState (() {
245312 // Create a new option and add it to the map.
246313 final option = PollOptionItem ();
247314 _options[option.id] = option;
248315
316+ // Create focus node for the new option
317+ _focusNodes[option.id] = FocusNode ();
318+
249319 // Notify the parent widget about the change
250320 widget.onOptionsChanged? .call ([..._options.values]);
251321 });
322+
323+ // Focus on the newly created option after the widget rebuilds
324+
325+ final newOption = _options.values.last;
326+ _focusNodes[newOption.id]? .requestFocus ();
327+ }
328+
329+ bool get _canAddMoreOptions {
330+ // Don't allow adding if there's already an empty option
331+ final hasEmptyOption = _options.values.any ((it) => it.text.isEmpty);
332+ if (hasEmptyOption) return false ;
333+
334+ // Check max options limit
335+ if (widget.optionsRange? .max case final maxOptions? ) {
336+ return _options.length < maxOptions;
337+ }
338+
339+ return true ;
252340 }
253341
254342 @override
@@ -271,13 +359,15 @@ class _PollOptionReorderableListViewState
271359 physics: const NeverScrollableScrollPhysics (),
272360 proxyDecorator: _proxyDecorator,
273361 separatorBuilder: (_, __) => const SizedBox (height: 8 ),
362+ onReorderStart: (_) => FocusScope .of (context).unfocus (),
274363 onReorder: _onOptionReorder,
275364 itemBuilder: (context, index) {
276365 final option = _options.values.elementAt (index);
277366 return PollOptionListItem (
278367 key: Key (option.id),
279368 option: option,
280369 hintText: widget.itemHintText,
370+ focusNode: _focusNodes[option.id],
281371 onChanged: _onOptionChanged,
282372 );
283373 },
@@ -287,7 +377,7 @@ class _PollOptionReorderableListViewState
287377 SizedBox (
288378 width: double .infinity,
289379 child: FilledButton .tonal (
290- onPressed: _onAddOptionPressed,
380+ onPressed: _canAddMoreOptions ? _onAddOptionPressed : null ,
291381 style: TextButton .styleFrom (
292382 alignment: Alignment .centerLeft,
293383 textStyle: theme.optionsTextFieldStyle,
0 commit comments