Skip to content

Commit

Permalink
feat: Improved UI motion design (outline#2310)
Browse files Browse the repository at this point in the history
* feat: Improved UI motion design

* fix: Animation direction when screen placement causes context menu to be flipped
  • Loading branch information
tommoor authored Jul 12, 2021
1 parent 5689d96 commit 8e5a2b8
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 62 deletions.
39 changes: 27 additions & 12 deletions app/components/ContextMenu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import {
fadeIn,
fadeAndScaleIn,
fadeAndSlideIn,
fadeAndSlideUp,
fadeAndSlideDown,
mobileContextMenu,
} from "shared/styles/animations";
import usePrevious from "hooks/usePrevious";

type Props = {|
"aria-label": string,
visible?: boolean,
placement?: string,
animating?: boolean,
children: React.Node,
onOpen?: () => void,
Expand Down Expand Up @@ -44,13 +46,25 @@ export default function ContextMenu({
return (
<>
<Menu hideOnClickOutside preventBodyScroll {...rest}>
{(props) => (
<Position {...props}>
<Background dir="auto">
{rest.visible || rest.animating ? children : null}
</Background>
</Position>
)}
{(props) => {
// kind of hacky, but this is an effective way of telling which way
// the menu will _actually_ be placed when taking into account screen
// positioning.
const topAnchor = props.style.top === "0";
const rightAnchor = props.placement === "bottom-end";

return (
<Position {...props}>
<Background
dir="auto"
topAnchor={topAnchor}
rightAnchor={rightAnchor}
>
{rest.visible || rest.animating ? children : null}
</Background>
</Position>
);
}}
</Menu>
{(rest.visible || rest.animating) && (
<Portal>
Expand Down Expand Up @@ -91,7 +105,7 @@ const Position = styled.div`
`;

const Background = styled.div`
animation: ${fadeAndSlideIn} 200ms ease;
animation: ${mobileContextMenu} 200ms ease;
transform-origin: 50% 100%;
max-width: 100%;
background: ${(props) => props.theme.menuBackground};
Expand All @@ -109,9 +123,10 @@ const Background = styled.div`
}
${breakpoint("tablet")`
animation: ${fadeAndScaleIn} 200ms ease;
animation: ${(props) =>
props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease;
transform-origin: ${(props) =>
props.left !== undefined ? "25%" : "75%"} 0;
props.rightAnchor === "bottom-end" ? "75%" : "25%"} 0;
max-width: 276px;
background: ${(props) => props.theme.menuBackground};
box-shadow: ${(props) => props.theme.menuShadow};
Expand Down
7 changes: 5 additions & 2 deletions app/components/DocumentListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ function DocumentListItem(props: Props, ref) {
!document.isDraft && !document.isArchived && !document.isTemplate;
const can = policies.abilities(currentTeam.id);

const handleMenuOpen = React.useCallback(() => setMenuOpen(true), []);
const handleMenuClosed = React.useCallback(() => setMenuOpen(false), []);

return (
<DocumentLink
ref={ref}
Expand Down Expand Up @@ -143,8 +146,8 @@ function DocumentListItem(props: Props, ref) {
<DocumentMenu
document={document}
showPin={showPin}
onOpen={() => setMenuOpen(true)}
onClose={() => setMenuOpen(false)}
onOpen={handleMenuOpen}
onClose={handleMenuClosed}
modal={false}
/>
</Actions>
Expand Down
4 changes: 2 additions & 2 deletions app/components/HoverPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { transparentize } from "polished";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { fadeAndSlideIn } from "shared/styles/animations";
import { fadeAndSlideDown } from "shared/styles/animations";
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
Expand Down Expand Up @@ -136,7 +136,7 @@ function HoverPreview({ node, ...rest }: Props) {
}

const Animate = styled.div`
animation: ${fadeAndSlideIn} 150ms ease;
animation: ${fadeAndSlideDown} 150ms ease;
@media print {
display: none;
Expand Down
41 changes: 21 additions & 20 deletions app/components/PaginatedDocumentList.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import Document from "models/Document";
import DocumentListItem from "components/DocumentListItem";
Expand All @@ -19,24 +18,26 @@ type Props = {|
showTemplate?: boolean,
|};

@observer
class PaginatedDocumentList extends React.Component<Props> {
render() {
const { empty, heading, documents, fetch, options, ...rest } = this.props;

return (
<PaginatedList
items={documents}
empty={empty}
heading={heading}
fetch={fetch}
options={options}
renderItem={(item) => (
<DocumentListItem key={item.id} document={item} {...rest} />
)}
/>
);
}
}
const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
empty,
heading,
documents,
fetch,
options,
...rest
}: Props) {
return (
<PaginatedList
items={documents}
empty={empty}
heading={heading}
fetch={fetch}
options={options}
renderItem={(item) => (
<DocumentListItem key={item.id} document={item} {...rest} />
)}
/>
);
});

export default PaginatedDocumentList;
57 changes: 49 additions & 8 deletions app/components/Tab.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
// @flow
import { m } from "framer-motion";
import * as React from "react";
import { NavLink } from "react-router-dom";
import { NavLink, Route } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import { type Theme } from "types";

type Props = {
theme: Theme,
children: React.Node,
};

const TabLink = styled(NavLink)`
const NavLinkWithChildrenFunc = ({ to, exact = false, children, ...rest }) => (
<Route path={to} exact={exact}>
{({ match }) => (
<NavLink to={to} {...rest}>
{children(match)}
</NavLink>
)}
</Route>
);

const TabLink = styled(NavLinkWithChildrenFunc)`
position: relative;
display: inline-flex;
align-items: center;
Expand All @@ -20,19 +32,48 @@ const TabLink = styled(NavLink)`
&:hover {
color: ${(props) => props.theme.textSecondary};
border-bottom: 3px solid ${(props) => props.theme.divider};
padding-bottom: 5px;
}
`;

function Tab({ theme, ...rest }: Props) {
const Active = styled(m.div)`
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
width: 100%;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
background: ${(props) => props.theme.textSecondary};
`;

const transition = {
type: "spring",
stiffness: 500,
damping: 30,
};

function Tab({ theme, children, ...rest }: Props) {
const activeStyle = {
paddingBottom: "5px",
borderBottom: `3px solid ${theme.textSecondary}`,
color: theme.textSecondary,
};

return <TabLink {...rest} activeStyle={activeStyle} />;
return (
<TabLink {...rest} activeStyle={activeStyle}>
{(match) => (
<>
{children}
{match && (
<Active
layoutId="underline"
initial={false}
transition={transition}
/>
)}
</>
)}
</TabLink>
);
}

export default withTheme(Tab);
13 changes: 8 additions & 5 deletions app/components/Tabs.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import { AnimateSharedLayout } from "framer-motion";
import { transparentize } from "polished";
import * as React from "react";
import styled from "styled-components";
Expand Down Expand Up @@ -79,11 +80,13 @@ const Tabs = ({ children }: {| children: React.Node |}) => {
}, [width, updateShadows]);

return (
<Sticky>
<Nav ref={ref} onScroll={updateShadows} $shadowVisible={shadowVisible}>
{children}
</Nav>
</Sticky>
<AnimateSharedLayout>
<Sticky>
<Nav ref={ref} onScroll={updateShadows} $shadowVisible={shadowVisible}>
{children}
</Nav>
</Sticky>
</AnimateSharedLayout>
);
};

Expand Down
25 changes: 16 additions & 9 deletions app/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import "focus-visible";
import { LazyMotion } from "framer-motion";
import { createBrowserHistory } from "history";
import { Provider } from "mobx-react";
import * as React from "react";
Expand Down Expand Up @@ -49,22 +50,28 @@ if ("serviceWorker" in window.navigator) {
});
}

// Make sure to return the specific export containing the feature bundle.
const loadFeatures = () =>
import("./utils/motion.js").then((res) => res.default);

if (element) {
const App = () => (
<React.StrictMode>
<Provider {...stores}>
<Analytics>
<Theme>
<ErrorBoundary>
<Router history={history}>
<>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</>
</Router>
</LazyMotion>
</ErrorBoundary>
</Theme>
</Analytics>
Expand Down
3 changes: 3 additions & 0 deletions app/utils/motion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow
import { domMax } from "framer-motion";
export default domMax;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"flow-typed": "^3.3.1",
"focus-visible": "^5.1.0",
"fractional-index": "^1.0.0",
"framer-motion": "^4.1.17",
"fs-extra": "^4.0.2",
"http-errors": "1.4.0",
"i18next": "^19.8.3",
Expand Down Expand Up @@ -211,4 +212,4 @@
"js-yaml": "^3.13.1"
},
"version": "0.57.0"
}
}
26 changes: 25 additions & 1 deletion shared/styles/animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@ export const fadeAndScaleIn = keyframes`
}
`;

export const fadeAndSlideIn = keyframes`
export const fadeAndSlideDown = keyframes`
from {
opacity: 0;
transform: scale(.98) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0px);
}
`;

export const fadeAndSlideUp = keyframes`
from {
opacity: 0;
transform: scale(.98) translateY(10px);
Expand All @@ -30,6 +42,18 @@ export const fadeAndSlideIn = keyframes`
}
`;

export const mobileContextMenu = keyframes`
from {
opacity: 0;
transform: scale(.98) translateY(10vh);
}
to {
opacity: 1;
transform: scale(1) translateY(0px);
}
`;

export const bounceIn = keyframes`
from,
20%,
Expand Down
Loading

0 comments on commit 8e5a2b8

Please sign in to comment.