Skip to content

Commit 92c4037

Browse files
authored
feat: add long press support for fab group (#3881)
1 parent 2c25126 commit 92c4037

File tree

3 files changed

+230
-4
lines changed

3 files changed

+230
-4
lines changed

example/src/Examples/FABExample.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as React from 'react';
2-
import { StyleSheet, View } from 'react-native';
2+
import { Alert, StyleSheet, View } from 'react-native';
33

44
import { FAB, Portal, Text } from 'react-native-paper';
55

66
import { useExampleTheme } from '..';
7+
import { isWeb } from '../../utils';
78
import ScreenWrapper from '../ScreenWrapper';
89

910
type FABVariant = 'primary' | 'secondary' | 'tertiary' | 'surface';
@@ -12,6 +13,8 @@ type FABMode = 'flat' | 'elevated';
1213

1314
const FABExample = () => {
1415
const [visible, setVisible] = React.useState<boolean>(true);
16+
const [toggleStackOnLongPress, setToggleStackOnLongPress] =
17+
React.useState<boolean>(false);
1518
const [open, setOpen] = React.useState<boolean>(false);
1619
const { isV3 } = useExampleTheme();
1720

@@ -142,6 +145,7 @@ const FABExample = () => {
142145
<FAB.Group
143146
open={open}
144147
icon={open ? 'calendar-today' : 'plus'}
148+
toggleStackOnLongPress={toggleStackOnLongPress}
145149
actions={[
146150
{ icon: 'plus', onPress: () => {} },
147151
{ icon: 'star', label: 'Star', onPress: () => {} },
@@ -152,10 +156,34 @@ const FABExample = () => {
152156
onPress: () => {},
153157
size: isV3 ? 'small' : 'medium',
154158
},
159+
{
160+
icon: toggleStackOnLongPress
161+
? 'gesture-tap'
162+
: 'gesture-tap-hold',
163+
label: toggleStackOnLongPress
164+
? 'Toggle on Press'
165+
: 'Toggle on Long Press',
166+
onPress: () => {
167+
setToggleStackOnLongPress(!toggleStackOnLongPress);
168+
},
169+
},
155170
]}
171+
enableLongPressWhenStackOpened
156172
onStateChange={({ open }: { open: boolean }) => setOpen(open)}
157173
onPress={() => {
158-
if (open) {
174+
if (toggleStackOnLongPress) {
175+
isWeb ? alert('Fab is Pressed') : Alert.alert('Fab is Pressed');
176+
// do something on press when the speed dial is closed
177+
} else if (open) {
178+
isWeb ? alert('Fab is Pressed') : Alert.alert('Fab is Pressed');
179+
// do something if the speed dial is open
180+
}
181+
}}
182+
onLongPress={() => {
183+
if (!toggleStackOnLongPress || open) {
184+
isWeb
185+
? alert('Fab is Long Pressed')
186+
: Alert.alert('Fab is Long Pressed');
159187
// do something if the speed dial is open
160188
}
161189
}}

src/components/FAB/FABGroup.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export type Props = {
3434
* - `containerStyle`: pass additional styles for the fab item label container, for example, `backgroundColor` @supported Available in 5.x
3535
* - `labelStyle`: pass additional styles for the fab item label, for example, `fontSize`
3636
* - `onPress`: callback that is called when `FAB` is pressed (required)
37+
* - `onLongPress`: callback that is called when `FAB` is long pressed
38+
* - `toggleStackOnLongPress`: callback that is called when `FAB` is long pressed
3739
* - `size`: size of action item. Defaults to `small`. @supported Available in v5.x
3840
* - `testID`: testID to be used on tests
3941
*/
@@ -72,6 +74,22 @@ export type Props = {
7274
* Function to execute on pressing the `FAB`.
7375
*/
7476
onPress?: (e: GestureResponderEvent) => void;
77+
/**
78+
* Function to execute on long pressing the `FAB`.
79+
*/
80+
onLongPress?: () => void;
81+
/**
82+
* Makes actions stack appear on long press instead of on press.
83+
*/
84+
toggleStackOnLongPress?: boolean;
85+
/**
86+
* Changes the delay for long press reaction.
87+
*/
88+
delayLongPress?: number;
89+
/**
90+
* Allows for onLongPress when stack is opened.
91+
*/
92+
enableLongPressWhenStackOpened?: boolean;
7593
/**
7694
* Whether the speed dial is open.
7795
*/
@@ -179,6 +197,8 @@ const FABGroup = ({
179197
icon,
180198
open,
181199
onPress,
200+
onLongPress,
201+
toggleStackOnLongPress,
182202
accessibilityLabel,
183203
theme: themeOverrides,
184204
style,
@@ -188,7 +208,9 @@ const FABGroup = ({
188208
testID,
189209
onStateChange,
190210
color: colorProp,
211+
delayLongPress = 200,
191212
variant = 'primary',
213+
enableLongPressWhenStackOpened = false,
192214
backdropColor: customBackdropColor,
193215
}: Props) => {
194216
const theme = useInternalTheme(themeOverrides);
@@ -416,8 +438,19 @@ const FABGroup = ({
416438
<FAB
417439
onPress={(e) => {
418440
onPress?.(e);
419-
toggle();
441+
if (!toggleStackOnLongPress || open) {
442+
toggle();
443+
}
444+
}}
445+
onLongPress={() => {
446+
if (!open || enableLongPressWhenStackOpened) {
447+
onLongPress?.();
448+
if (toggleStackOnLongPress) {
449+
toggle();
450+
}
451+
}
420452
}}
453+
delayLongPress={delayLongPress}
421454
icon={icon}
422455
color={colorProp}
423456
accessibilityLabel={accessibilityLabel}

src/components/__tests__/FABGroup.test.tsx

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { Animated } from 'react-native';
33

4-
import { render } from '@testing-library/react-native';
4+
import { act, fireEvent, render } from '@testing-library/react-native';
55
import color from 'color';
66

77
import { getTheme } from '../../core/theming';
@@ -220,3 +220,168 @@ it('animated value changes correctly', () => {
220220
transform: [{ scale: 1.5 }],
221221
});
222222
});
223+
224+
describe('Toggle Stack visibility', () => {
225+
it('toggles stack visibility on press', () => {
226+
const onStateChange = jest.fn();
227+
const { getByText } = render(
228+
<FAB.Group
229+
visible
230+
open={false}
231+
label="Stack test"
232+
icon=""
233+
onStateChange={onStateChange}
234+
actions={[
235+
{
236+
label: 'testing',
237+
onPress() {},
238+
icon: '',
239+
},
240+
]}
241+
/>
242+
);
243+
244+
act(() => {
245+
fireEvent(getByText('Stack test'), 'onPress');
246+
});
247+
248+
expect(onStateChange).toHaveBeenCalledTimes(1);
249+
});
250+
251+
it('does not toggle stack visibility on long press', () => {
252+
const onStateChange = jest.fn();
253+
const { getByText } = render(
254+
<FAB.Group
255+
visible
256+
open={false}
257+
label="Stack test"
258+
icon=""
259+
onStateChange={onStateChange}
260+
actions={[
261+
{
262+
label: 'testing',
263+
onPress() {},
264+
icon: '',
265+
},
266+
]}
267+
/>
268+
);
269+
270+
act(() => {
271+
fireEvent(getByText('Stack test'), 'onLongPress');
272+
});
273+
274+
expect(onStateChange).toHaveBeenCalledTimes(0);
275+
});
276+
277+
it('toggles stack visibility on long press with toggleStackOnLongPress prop', () => {
278+
const onStateChange = jest.fn();
279+
const { getByText } = render(
280+
<FAB.Group
281+
visible
282+
open={false}
283+
toggleStackOnLongPress
284+
label="Stack test"
285+
icon=""
286+
onStateChange={onStateChange}
287+
actions={[
288+
{
289+
label: 'testing',
290+
onPress() {},
291+
icon: '',
292+
},
293+
]}
294+
/>
295+
);
296+
297+
act(() => {
298+
fireEvent(getByText('Stack test'), 'onLongPress');
299+
});
300+
301+
expect(onStateChange).toHaveBeenCalledTimes(1);
302+
});
303+
304+
it('does not toggle stack visibility on press with toggleStackOnLongPress prop', () => {
305+
const onStateChange = jest.fn();
306+
const { getByText } = render(
307+
<FAB.Group
308+
visible
309+
open={false}
310+
toggleStackOnLongPress
311+
label="Stack test"
312+
icon=""
313+
onStateChange={onStateChange}
314+
actions={[
315+
{
316+
label: 'testing',
317+
onPress() {},
318+
icon: '',
319+
},
320+
]}
321+
/>
322+
);
323+
324+
act(() => {
325+
fireEvent(getByText('Stack test'), 'onPress');
326+
});
327+
328+
expect(onStateChange).toHaveBeenCalledTimes(0);
329+
});
330+
331+
it('does not trigger onLongPress when stack is opened', () => {
332+
const onStateChange = jest.fn();
333+
const onLongPress = jest.fn();
334+
const { getByText } = render(
335+
<FAB.Group
336+
visible
337+
open={true}
338+
label="Stack test"
339+
icon=""
340+
onStateChange={onStateChange}
341+
onLongPress={onLongPress}
342+
actions={[
343+
{
344+
label: 'testing',
345+
onPress() {},
346+
icon: '',
347+
},
348+
]}
349+
/>
350+
);
351+
352+
act(() => {
353+
fireEvent(getByText('Stack test'), 'onLongPress');
354+
});
355+
356+
expect(onLongPress).toHaveBeenCalledTimes(0);
357+
});
358+
359+
it('does trigger onLongPress when stack is opened and enableLongPressWhenStackOpened is true', () => {
360+
const onStateChange = jest.fn();
361+
const onLongPress = jest.fn();
362+
const { getByText } = render(
363+
<FAB.Group
364+
visible
365+
open={true}
366+
enableLongPressWhenStackOpened
367+
label="Stack test"
368+
icon=""
369+
onStateChange={onStateChange}
370+
onLongPress={onLongPress}
371+
actions={[
372+
{
373+
label: 'testing',
374+
onPress() {},
375+
icon: '',
376+
},
377+
]}
378+
/>
379+
);
380+
381+
act(() => {
382+
fireEvent(getByText('Stack test'), 'onLongPress');
383+
});
384+
385+
expect(onLongPress).toHaveBeenCalledTimes(1);
386+
});
387+
});

0 commit comments

Comments
 (0)