Skip to content

Instantly share code, notes, and snippets.

@mrnkr
Created February 10, 2019 19:22
Show Gist options
  • Save mrnkr/5bc8be2839c123281fcfa521d8d40b0b to your computer and use it in GitHub Desktop.
Save mrnkr/5bc8be2839c123281fcfa521d8d40b0b to your computer and use it in GitHub Desktop.
React list cell implementation with buttons hidden below the cell content when on mobile (alla Apple Mail on iOS)
.my-cell {
position: relative;
height: 64px;
width: 100%;
margin-top: 1rem;
margin-bottom: 1rem;
.my-cell-content {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding-left: 1rem;
padding-right: 1rem;
background-color: #FFFFFF;
}
.my-cell-buttons {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: stretch;
span {
height: 64px;
width: 64px;
}
.my-cell-buttons-left {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.my-cell-buttons-right {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
}
}
@media only screen and (min-width: 1088px) {
.my-cell {
height: 64px;
width: 100%;
margin-top: 1rem;
margin-bottom: 1rem;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: stretch;
.my-cell-content {
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding-left: 1rem;
padding-right: 1rem;
transform: translateX(0) !important;
}
.my-cell-buttons {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: stretch;
span {
height: 64px;
width: 64px;
background-color: transparent !important;
color: #000000 !important;
cursor: pointer;
}
.my-cell-buttons-left {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.my-cell-buttons-right {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
}
}
}
import React, { Component } from 'react';
import Hammer from 'hammerjs';
import './Cell.scss';
const BUTTON_WIDTH = 64;
interface Props {
buttons?: { icon: string; color: string; backgroundColor: string; side: 'left' | 'right'; onClick: () => void }[];
sensitivity?: number;
onClick?: () => void;
}
interface State {
currentPosition: number;
translateValue: number;
}
export default class Cell extends Component<Props, State> {
public state: State = {
currentPosition: 0,
translateValue: 0
}
private sliderEl: React.RefObject<HTMLDivElement> = React.createRef();
private sliderManager?: HammerManager;
public componentDidMount(): void {
const { sliderEl } = this;
if (!sliderEl.current)
return;
this.sliderManager = new Hammer.Manager(sliderEl.current);
this.sliderManager.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
this.sliderManager.on('pan', this.handlePanGesture);
}
public render(): JSX.Element {
const { buttons = [], children, onClick = () => null } = this.props;
const { translateValue } = this.state;
return (
<div
ref={this.sliderEl}
className="my-cell"
>
<div className="my-cell-buttons">
<div className="my-cell-buttons-left">
{
buttons
.filter(btn => btn.side === 'left')
.map(btn =>
<span
key={btn.icon}
className="icon is-medium"
style={{
backgroundColor: btn.backgroundColor,
color: btn.color
}}
onClick={btn.onClick}
>
<i className={`ion-${btn.icon}`}></i>
</span>
)
}
</div>
<div className="my-cell-buttons-right">
{
buttons
.filter(btn => btn.side === 'right')
.map(btn =>
<span
key={btn.icon}
className="icon is-medium"
style={{
backgroundColor: btn.backgroundColor,
color: btn.color
}}
onClick={btn.onClick}
>
<i className={`ion-${btn.icon}`}></i>
</span>
)
}
</div>
</div>
<div
className="my-cell-content"
style={{
transform: `translateX(${translateValue}px)`,
transition: 'transform ease-out 0.45s'
}}
onClick={onClick}
>
{children}
</div>
</div>
);
}
private goTo = (position: number) => {
const { buttons = [] } = this.props;
if (position < 0) {
this.setState({
translateValue: buttons.reduce((acum, cur) => acum + (cur.side === 'left' ? 1 : 0), 0) * 64,
currentPosition: buttons.reduce((acum, cur) => acum + (cur.side === 'left' ? 1 : 0), 0) > 0 ? -1 : 0
});
} else if (position > 0) {
this.setState({
translateValue: - buttons.reduce((acum, cur) => acum + (cur.side === 'right' ? 1 : 0), 0) * 64,
currentPosition: buttons.reduce((acum, cur) => acum + (cur.side === 'right' ? 1 : 0), 0) ? 1 : 0
});
} else {
this.setState({ translateValue: 0, currentPosition: 0 });
}
}
private handlePanGesture = (e: HammerInput) => {
const { sliderEl } = this;
const { sensitivity = 32, buttons = [] } = this.props;
const { currentPosition } = this.state;
if (!sliderEl.current)
return;
const offset = currentPosition === 1 ?
buttons.reduce((acum, cur) => acum + (cur.side === 'left' ? 1 : 0), 0) :
(currentPosition === 2 ?
buttons.reduce((acum, cur) => acum + (cur.side === 'right' ? 1 : 0), 0) :
0);
this.setState({ translateValue: BUTTON_WIDTH * offset + e.deltaX });
if (!e.isFinal) {
return;
}
if (Math.abs(e.deltaX) > sensitivity) {
this.goTo(e.deltaX > 0 ? currentPosition - 1 : currentPosition + 1);
} else {
this.goTo(currentPosition);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment