Skip to content

Fix SegmentedButton border doesn't respect hovered, focused, selected states - #161942

Closed
TahaTesser wants to merge 5 commits into
flutter:masterfrom
TahaTesser:segmented_button_side_states
Closed

TahaTesser wants to merge 5 commits into
flutter:masterfrom
TahaTesser:segmented_button_side_states

Conversation

@TahaTesser

Copy link
Copy Markdown
Contributor

Fixes SegmentedButton does not set its MaterialState for side

Code Sample

expand to view the code sample
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @override
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar calendarView = Calendar.day;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.resolveWith(
              (Set<WidgetState> states) {
                if (states.contains(WidgetState.disabled)) {
                  return const BorderSide(color: Colors.grey, width: 2);
                }
                if (states.contains(WidgetState.hovered)) {
                  return const BorderSide(color: Colors.green, width: 2);
                }
                if (states.contains(WidgetState.focused)) {
                  return const BorderSide(color: Colors.pinkAccent, width: 2);
                }
                // if (states.contains(WidgetState.selected)) {
                //   return const BorderSide(color: Colors.brown, width: 2);
                // }

                return const BorderSide(color: Colors.amber, width: 2);
              },
            ),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: Column(
            spacing: 20,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              SegmentedButton<Calendar>(
                segments: const <ButtonSegment<Calendar>>[
                  ButtonSegment<Calendar>(
                      value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)),
                  ButtonSegment<Calendar>(
                      value: Calendar.week,
                      label: Text('Week'),
                      icon: Icon(Icons.calendar_view_week)),
                  ButtonSegment<Calendar>(
                      value: Calendar.month,
                      label: Text('Month'),
                      icon: Icon(Icons.calendar_view_month)),
                  ButtonSegment<Calendar>(
                      value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)),
                ],
                selected: <Calendar>{calendarView},
              ),
              SegmentedButton<Calendar>(
                segments: const <ButtonSegment<Calendar>>[
                  ButtonSegment<Calendar>(
                      value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)),
                  ButtonSegment<Calendar>(
                      value: Calendar.week,
                      label: Text('Week'),
                      icon: Icon(Icons.calendar_view_week)),
                  ButtonSegment<Calendar>(
                      value: Calendar.month,
                      label: Text('Month'),
                      icon: Icon(Icons.calendar_view_month)),
                  ButtonSegment<Calendar>(
                      value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)),
                ],
                selected: <Calendar>{calendarView},
                onSelectionChanged: (Set<Calendar> newSelection) {
                  setState(() {
                    calendarView = newSelection.first;
                  });
                },
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Before

Screen.Recording.2025-01-21.at.15.23.24.mov

After

Screen.Recording.2025-01-21.at.15.22.33.mov

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. p: material_ui material_ui package in flutter/packages labels Jan 21, 2025
@TahaTesser
TahaTesser marked this pull request as ready for review January 21, 2025 16:01
@TahaTesser TahaTesser changed the title Fix SegmentedButton border doesn't respect hovered, focused state Fix SegmentedButton border doesn't respect hovered, focused, selected states Jan 22, 2025
@Piinks
Piinks requested a review from QuncCccccc January 29, 2025 19:19
@victorsanni
victorsanni self-requested a review January 29, 2025 19:19
Comment thread packages/flutter/test/material/segmented_button_test.dart Outdated

@QuncCccccc QuncCccccc left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the specs, it seems if a segment is disabled, we should be able to show the disabled state on border of the specific segment (not the whole SegmentedButton). So It's not clear what expected behavior would be when a specific segment is hovered, focused and pressed. Let me double check with the material team.

Comment thread packages/flutter/lib/src/material/segmented_button.dart Outdated
Comment thread packages/flutter/lib/src/material/segmented_button.dart
@QuncCccccc

Copy link
Copy Markdown
Contributor

Filed a bug and asked for clarification: b/394478041

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't heard about the update from the material team, but I do see there's a token to indicate the outline of the whole segmented button, so I think this feature makes sense:)

Sorry for the late response. Trying my best to speed up! I'll make sure we make some progress this week!

Comment thread packages/flutter/lib/src/material/segmented_button.dart Outdated
Comment thread packages/flutter/test/material/segmented_button_test.dart Outdated
@TahaTesser
TahaTesser removed the request for review from matanlurey February 26, 2025 14:42

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks so much for your patience.

Comment thread packages/flutter/lib/src/material/segmented_button.dart Outdated
Comment thread packages/flutter/lib/src/material/segmented_button.dart
@TahaTesser
TahaTesser requested review from victorsanni and removed request for victorsanni March 12, 2025 11:00
@TahaTesser

Copy link
Copy Markdown
Contributor Author

@victorsanni Could you please approve? :)

@victorsanni victorsanni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@victorsanni victorsanni added the autosubmit Merge PR when tree becomes green via auto submit App label Mar 12, 2025
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Mar 12, 2025
@auto-submit

auto-submit Bot commented Mar 12, 2025

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/161942, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@victorsanni

Copy link
Copy Markdown
Contributor

I was looking at the google testing failures, they are all image diffs. Seems like the disabled segmented button is now black instead of gray. I wasn't following the earlier conversation, but is that intended?

@QuncCccccc

QuncCccccc commented Mar 12, 2025 •

Copy link
Copy Markdown
Contributor

I'll look at the internal use case to see how they use the segmented buttons. I think we handle the disabled state correctly here🤔

@QuncCccccc
QuncCccccc self-requested a review March 14, 2025 22:31
@dkwingsmt

Copy link
Copy Markdown
Contributor

@QuncCccccc How's the investigation going? (from triage)

bool _focused = false;
bool get _selected => widget.selected.isNotEmpty;

Set<WidgetState> get _states => <WidgetState>{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we disable one of the segments by setting ButtonSegment.enabled to false, _states cannot really add WidgetState.disabled to the set, so the disabledBorder in line 628 is actually not the real disabled border and then incorrect side is applied to the disabled segment. This causes the google testing failures.

final BorderSide effectiveSide =
resolve<BorderSide?>((ButtonStyle? style) => style?.side) ?? BorderSide.none;
final OutlinedBorder enabledBorder = effectiveBorder.copyWith(side: effectiveSide);
final OutlinedBorder disabledBorder = effectiveBorder.copyWith(side: effectiveSide);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following my explanation above, this disabledBorder will never get the disabled value if a segment is disabled by setting ButtonSegment.enabled (like the screenshot below). So we might still need to use the original implementation to handle the disabled state.

Screenshot 2025-04-08 at 9 10 54 PM

@dkwingsmt

Copy link
Copy Markdown
Contributor

Gentle ping @TahaTesser

@QuncCccccc
QuncCccccc self-requested a review June 4, 2025 20:30
@TahaTesser TahaTesser closed this Jun 6, 2025
github-merge-queue Bot pushed a commit that referenced this pull request Jul 30, 2025
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](#159884)

## Tests

Adds 2 tests.
ksokolovskyi pushed a commit to ksokolovskyi/flutter that referenced this pull request Aug 19, 2025
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
flutter#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](flutter#159884)

## Tests

Adds 2 tests.
mboetger pushed a commit to mboetger/flutter that referenced this pull request Sep 18, 2025
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
flutter#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](flutter#159884)

## Tests

Adds 2 tests.
korca0220 pushed a commit to korca0220/flutter that referenced this pull request Sep 22, 2025
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
flutter#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](flutter#159884)

## Tests

Adds 2 tests.
lucaantonelli pushed a commit to lucaantonelli/flutter that referenced this pull request Nov 21, 2025
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
flutter#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](flutter#159884)

## Tests

Adds 2 tests.
chunhtai pushed a commit to chunhtai/packages that referenced this pull request Jun 12, 2026
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
flutter/flutter#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](flutter/flutter#159884)

## Tests

Adds 2 tests.
Nakiha pushed a commit to Nakiha/VoidPlayer-Flutter that referenced this pull request Sep 28, 2026
## Description

This PR fixes `SegmentedButton` border side not depending on the current
state.

It's based on one @TahaTesser 's great PR: see
flutter/flutter#161942. 🙏
I updated some logic related to disabled border which led to Taha’s PR
failing internal Google tests.
I also added a test to verify the disabled border.

<details><summary>Code sample for screenshots</summary>

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const SegmentedButtonApp());
}

enum Calendar { day, week, month, year }

class SegmentedButtonApp extends StatefulWidget {
  const SegmentedButtonApp({super.key});

  @OverRide
  State<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar? calendarView;

  @OverRide
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        segmentedButtonTheme: SegmentedButtonThemeData(
          style: ButtonStyle(
            side: WidgetStateProperty.fromMap(<WidgetStatesConstraint, BorderSide?>{
              WidgetState.disabled: const BorderSide(color: Colors.grey, width: 2),
              WidgetState.selected & WidgetState.hovered: const BorderSide(
                color: Colors.blue,
                width: 2,
              ),
              WidgetState.selected & WidgetState.focused: const BorderSide(
                color: Colors.pinkAccent,
                width: 2,
              ),
              WidgetState.hovered: const BorderSide(color: Colors.green, width: 2),
              WidgetState.focused: const BorderSide(color: Colors.purple, width: 2),
              WidgetState.any: const BorderSide(color: Colors.amber, width: 2),
            }),
          ),
        ),
      ),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                value: Calendar.day,
                label: Text('Day'),
                icon: Icon(Icons.calendar_view_day),
                enabled: false,
              ),
              ButtonSegment<Calendar>(
                value: Calendar.week,
                label: Text('Week'),
                icon: Icon(Icons.calendar_view_week),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.month,
                label: Text('Month'),
                icon: Icon(Icons.calendar_view_month),
              ),
              ButtonSegment<Calendar>(
                value: Calendar.year,
                label: Text('Year'),
                icon: Icon(Icons.calendar_today),
              ),
            ],
            selected: <Calendar>{?calendarView},
            emptySelectionAllowed: true,
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.isEmpty ? null : newSelection.first;
              });
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
      ),
    );
  }
}

``` 

</details> 

# Before


https://github.com/user-attachments/assets/1581f431-f87a-4af3-8ef6-f1f0d170e54a


# After


https://github.com/user-attachments/assets/156a8a64-3d9f-4323-9a1d-60624f5ac5d4


## Related Issue

Fixes [SegmentedButton does not set its MaterialState for side
](flutter/flutter#159884)

## Tests

Adds 2 tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels. p: material_ui material_ui package in flutter/packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SegmentedButton does not set its MaterialState for side

4 participants