Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BottomNavigation] onClick does not fire if tapped while scrolling #22524

Merged
merged 15 commits into from Sep 9, 2020
Merged
120 changes: 120 additions & 0 deletions docs/src/pages/components/bottom-navigation/FixedBottomNavigation.js
@@ -0,0 +1,120 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import RestoreIcon from '@material-ui/icons/Restore';
import FavoriteIcon from '@material-ui/icons/Favorite';
import ArchiveIcon from '@material-ui/icons/Archive';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import ListItemText from '@material-ui/core/ListItemText';
import Avatar from '@material-ui/core/Avatar';

const useStyles = makeStyles({
bottomNav: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
},
});

function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}

const messageExamples = [
{
primary: 'Brunch this week?',
secondary:
"I'll be in the neighbourhood this week. Let's grab a bite to eat",
person: '/static/images/avatar/5.jpg',
},
{
primary: 'Birthday Gift',
secondary: `Do you have a suggestion for a good present for John on his work
anniversary. I am really confused & would love your thoughts on it.`,
person: '/static/images/avatar/1.jpg',
},
{
primary: 'Recipe to try',
secondary:
'I am try out this new BBQ recipe, I think this might be amazing',
person: '/static/images/avatar/2.jpg',
},
{
primary: 'Yes!',
secondary: 'I have the tickets to the ReactConf for this year.',
person: '/static/images/avatar/3.jpg',
},
{
primary: "Doctor's Appointment",
secondary:
'My appointment for the doctor was rescheduled for next Saturday.',
person: '/static/images/avatar/4.jpg',
},
{
primary: 'Discussion',
secondary: `Menus that are generated by the bottom app bar (such as a bottom
navigation drawer or overflow menu) open as bottom sheets at a higher elevation
than the bar.`,
person: '/static/images/avatar/5.jpg',
},
{
primary: 'Summer BBQ',
secondary: `Who wants to have a cookout this weekend? I just got some furniture
for my backyard and would love to fire up the grill.`,
person: '/static/images/avatar/1.jpg',
},
];

function refreshMessages() {
return Array.from(new Array(100)).map(
() => messageExamples[getRandomInt(messageExamples.length)],
);
}

export default function FixedBottomNavigation() {
const classes = useStyles();
const [value, setValue] = React.useState(0);
const [messages, setMessages] = React.useState(() => refreshMessages());

React.useEffect(() => {
setMessages(refreshMessages());
}, [value, setMessages]);

return (
<div>
<div style={{ marginBottom: 80 }}>
<Typography variant="h5" gutterBottom>
Threads
</Typography>
<List>
{messages.map(({ primary, secondary, person }, index) => (
<ListItem button key={index}>
<ListItemAvatar>
<Avatar alt="Profile Picture" src={person} />
</ListItemAvatar>
<ListItemText primary={primary} secondary={secondary} />
</ListItem>
))}
</List>
</div>

<BottomNavigation
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
showLabels
className={classes.bottomNav}
>
<BottomNavigationAction label="Recents" icon={<RestoreIcon />} />
<BottomNavigationAction label="Favorites" icon={<FavoriteIcon />} />
<BottomNavigationAction label="Archive" icon={<ArchiveIcon />} />
</BottomNavigation>
</div>
);
}
126 changes: 126 additions & 0 deletions docs/src/pages/components/bottom-navigation/FixedBottomNavigation.tsx
@@ -0,0 +1,126 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import RestoreIcon from '@material-ui/icons/Restore';
import FavoriteIcon from '@material-ui/icons/Favorite';
import ArchiveIcon from '@material-ui/icons/Archive';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import ListItemText from '@material-ui/core/ListItemText';
import Avatar from '@material-ui/core/Avatar';

const useStyles = makeStyles({
bottomNav: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
},
});

function getRandomInt(max: number) {
return Math.floor(Math.random() * Math.floor(max));
}

interface MessageExample {
primary: string;
secondary: string;
person: string;
}

const messageExamples: MessageExample[] = [
{
primary: 'Brunch this week?',
secondary:
"I'll be in the neighbourhood this week. Let's grab a bite to eat",
person: '/static/images/avatar/5.jpg',
},
{
primary: 'Birthday Gift',
secondary: `Do you have a suggestion for a good present for John on his work
anniversary. I am really confused & would love your thoughts on it.`,
person: '/static/images/avatar/1.jpg',
},
{
primary: 'Recipe to try',
secondary:
'I am try out this new BBQ recipe, I think this might be amazing',
person: '/static/images/avatar/2.jpg',
},
{
primary: 'Yes!',
secondary: 'I have the tickets to the ReactConf for this year.',
person: '/static/images/avatar/3.jpg',
},
{
primary: "Doctor's Appointment",
secondary:
'My appointment for the doctor was rescheduled for next Saturday.',
person: '/static/images/avatar/4.jpg',
},
{
primary: 'Discussion',
secondary: `Menus that are generated by the bottom app bar (such as a bottom
navigation drawer or overflow menu) open as bottom sheets at a higher elevation
than the bar.`,
person: '/static/images/avatar/5.jpg',
},
{
primary: 'Summer BBQ',
secondary: `Who wants to have a cookout this weekend? I just got some furniture
for my backyard and would love to fire up the grill.`,
person: '/static/images/avatar/1.jpg',
},
];

function refreshMessages(): MessageExample[] {
return Array.from(new Array(100)).map(
() => messageExamples[getRandomInt(messageExamples.length)],
);
}

export default function FixedBottomNavigation() {
const classes = useStyles();
const [value, setValue] = React.useState(0);
const [messages, setMessages] = React.useState(() => refreshMessages());

React.useEffect(() => {
setMessages(refreshMessages());
}, [value, setMessages]);

return (
<div>
<div style={{ marginBottom: 80 }}>
<Typography variant="h5" gutterBottom>
Threads
</Typography>
<List>
{messages.map(({ primary, secondary, person }, index) => (
<ListItem button key={index}>
<ListItemAvatar>
<Avatar alt="Profile Picture" src={person} />
</ListItemAvatar>
<ListItemText primary={primary} secondary={secondary} />
</ListItem>
))}
</List>
</div>

<BottomNavigation
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
showLabels
className={classes.bottomNav}
>
<BottomNavigationAction label="Recents" icon={<RestoreIcon />} />
<BottomNavigationAction label="Favorites" icon={<FavoriteIcon />} />
<BottomNavigationAction label="Archive" icon={<ArchiveIcon />} />
</BottomNavigation>
</div>
);
}
Expand Up @@ -24,3 +24,9 @@ When there are only **three** actions, display both icons and text labels at all
If there are **four** or **five** actions, display inactive views as icons only.

{{"demo": "pages/components/bottom-navigation/LabelBottomNavigation.js", "bg": true}}

## Fixed Bottom Navigation

Keep Bottom Navigation fixed to the bottom, no matter the amount of content on-screen.

{{"demo": "pages/components/bottom-navigation/FixedBottomNavigation.js", "bg": true, "iframe": true, "maxWidth": 600}}
Expand Up @@ -60,6 +60,8 @@ const BottomNavigationAction = React.forwardRef(function BottomNavigationAction(
icon,
label,
onChange,
onTouchStart,
onTouchEnd,
onClick,
// eslint-disable-next-line react/prop-types -- private, always overridden by BottomNavigation
selected,
Expand All @@ -68,7 +70,43 @@ const BottomNavigationAction = React.forwardRef(function BottomNavigationAction(
...other
} = props;

const touchStartPos = React.useRef();
const touchTimer = React.useRef();

React.useEffect(() => {
return () => clearTimeout(touchTimer.current);
EliasJorgensen marked this conversation as resolved.
Show resolved Hide resolved
}, [touchTimer]);

function handleTouchStart(event) {
if (onTouchStart) onTouchStart(event);
EliasJorgensen marked this conversation as resolved.
Show resolved Hide resolved

const { clientX, clientY } = event.touches[0];

touchStartPos.current = {
clientX,
clientY,
};
}

function handleTouchEnd(event) {
if (onTouchEnd) onTouchEnd(event);

const target = event.target;
const { clientX, clientY } = event.changedTouches[0];

if (
Math.abs(clientX - touchStartPos.current.clientX) < 10 &&
Math.abs(clientY - touchStartPos.current.clientY) < 10
) {
touchTimer.current = setTimeout(() => {
target.dispatchEvent(new Event('click', { bubbles: true }));
}, 10);
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we could have something more reliable than an arbitrary 10ms delay.

}
}

const handleChange = (event) => {
clearTimeout(touchTimer.current);

if (onChange) {
onChange(event, value);
}
Expand All @@ -91,6 +129,8 @@ const BottomNavigationAction = React.forwardRef(function BottomNavigationAction(
)}
focusRipple
onClick={handleChange}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
{...other}
>
<span className={classes.wrapper}>
Expand Down Expand Up @@ -142,6 +182,14 @@ BottomNavigationAction.propTypes = {
* @ignore
*/
onClick: PropTypes.func,
/**
* @ignore
*/
onTouchEnd: PropTypes.func,
/**
* @ignore
*/
onTouchStart: PropTypes.func,
/**
* If `true`, the `BottomNavigationAction` will show its label.
* By default, only the selected `BottomNavigationAction`
Expand Down