Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions apps/design_system_gallery/lib/components/buttons/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ class _PlaygroundDemoState extends State<_PlaygroundDemo> {
description: 'Whether the button toggles its selected state on tap.',
);

final hasLongPress = context.knobs.boolean(
label: 'Long Press',
description: 'Whether the button responds to a long press.',
);

final showLeadingIcon =
!isIconOnly &&
context.knobs.boolean(
Expand All @@ -92,22 +97,23 @@ class _PlaygroundDemoState extends State<_PlaygroundDemo> {
description: 'Show an icon after the label.',
);

void onTap() {
if (isSelectable) {
setState(() => _isSelected = !_isSelected);
}
void showMessage(String message) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(
isSelectable ? 'Button ${_isSelected ? 'selected' : 'deselected'}' : 'Button tapped!',
),
duration: const Duration(seconds: 1),
),
SnackBar(content: Text(message), duration: const Duration(seconds: 1)),
);
}

void onTap() {
if (isSelectable) {
setState(() => _isSelected = !_isSelected);
}
showMessage(isSelectable ? 'Button ${_isSelected ? 'selected' : 'deselected'}' : 'Button tapped!');
}

final onLongPress = hasLongPress && !isDisabled ? () => showMessage('Button long-pressed!') : null;

return Center(
child: isIconOnly
? StreamButton.icon(
Expand All @@ -118,6 +124,7 @@ class _PlaygroundDemoState extends State<_PlaygroundDemo> {
isFloating: isFloating ? true : null,
isSelected: isSelectable ? _isSelected : null,
onPressed: isDisabled ? null : onTap,
onLongPress: onLongPress,
)
: StreamButton(
style: style,
Expand All @@ -126,6 +133,7 @@ class _PlaygroundDemoState extends State<_PlaygroundDemo> {
isFloating: isFloating ? true : null,
isSelected: isSelectable ? _isSelected : null,
onPressed: isDisabled ? null : onTap,
onLongPress: onLongPress,
iconLeft: showLeadingIcon ? const Icon(Icons.add) : null,
iconRight: showTrailingIcon ? const Icon(Icons.arrow_forward) : null,
child: Text(label),
Expand Down
1 change: 1 addition & 0 deletions packages/stream_core_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### ✨ Features

- Added `onLongPress` to `StreamButton` and `StreamButton.icon`.
- `StreamMessageText` now links a web address written without a scheme, such
as `example.com`, when it ends in a known top-level domain. It opens over
`https://`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class StreamButton extends StatelessWidget {
super.key,
required Widget child,
VoidCallback? onPressed,
VoidCallback? onLongPress,
StreamButtonStyle style = .primary,
StreamButtonType type = .solid,
StreamButtonSize size = .medium,
Expand All @@ -84,6 +85,7 @@ class StreamButton extends StatelessWidget {
}) : props = .new(
child: child,
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
type: type,
size: size,
Expand Down Expand Up @@ -112,6 +114,7 @@ class StreamButton extends StatelessWidget {
super.key,
required Widget icon,
VoidCallback? onPressed,
VoidCallback? onLongPress,
StreamButtonStyle style = .primary,
StreamButtonType type = .solid,
StreamButtonSize size = .medium,
Expand All @@ -122,6 +125,7 @@ class StreamButton extends StatelessWidget {
StreamButtonThemeStyle? themeStyle,
}) : props = .new(
onPressed: onPressed,
onLongPress: onLongPress,
style: style,
type: type,
size: size,
Expand Down Expand Up @@ -165,6 +169,7 @@ class StreamButtonProps {
const StreamButtonProps({
this.child,
this.onPressed,
this.onLongPress,
this.style = .primary,
this.type = .solid,
this.size = .medium,
Expand All @@ -183,11 +188,18 @@ class StreamButtonProps {
/// [iconLeft] as its sole icon (see [StreamButton.icon]).
final Widget? child;

/// Called when the button is pressed.
/// Called when the button is tapped or otherwise activated.
///
/// If null, the button will be disabled.
/// If this callback and [onLongPress] are null, then the button will be
/// disabled.
final VoidCallback? onPressed;

/// Called when the button is long-pressed.
///
/// If this callback and [onPressed] are null, then the button will be
/// disabled.
final VoidCallback? onLongPress;

/// The visual style variant of the button.
///
/// Determines the color scheme used (primary, secondary, destructive).
Expand Down Expand Up @@ -373,6 +385,7 @@ class _DefaultStreamButtonState extends State<DefaultStreamButton> {
child: ElevatedButton(
autofocus: props.autofocus,
onPressed: props.onPressed,
onLongPress: props.onLongPress,
statesController: _statesController,
style: ButtonStyle(
tapTargetSize: themeStyle.tapTargetSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,70 @@ void main() {
});
});

group('StreamButton.icon long press', () {
testWidgets('calls onLongPress instead of showing the tooltip', (tester) async {
var pressed = 0;
var longPressed = 0;
await tester.pumpWidget(
_withStreamTheme(
StreamButton.icon(
icon: const Icon(Icons.send),
tooltip: 'Send',
onPressed: () => pressed++,
onLongPress: () => longPressed++,
),
),
);

await tester.longPress(find.byType(StreamButton));
await tester.pumpAndSettle();

expect(longPressed, 1);
expect(pressed, 0);
expect(find.text('Send'), findsNothing);
});

testWidgets('exposes a long-press action to accessibility services', (tester) async {
final handle = tester.ensureSemantics();

await tester.pumpWidget(
_withStreamTheme(
StreamButton.icon(
icon: const Icon(Icons.send),
tooltip: 'Send',
onPressed: () {},
onLongPress: () {},
),
),
);

expect(
tester.getSemantics(find.byType(StreamButton)),
isSemantics(tooltip: 'Send', isButton: true, hasTapAction: true, hasLongPressAction: true),
);

handle.dispose();
});

testWidgets('still calls onPressed on a tap', (tester) async {
var pressed = 0;
await tester.pumpWidget(
_withStreamTheme(
StreamButton.icon(
icon: const Icon(Icons.send),
tooltip: 'Send',
onPressed: () => pressed++,
onLongPress: () {},
),
),
);

await tester.tap(find.byType(StreamButton));

expect(pressed, 1);
});
});

group('StreamButton (with child) a11y', () {
testWidgets('label comes from child Text widget', (tester) async {
final handle = tester.ensureSemantics();
Expand Down
Loading