Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 1467191

Browse files
committed
Update the CSS transition for breadcrumbs
The actual transition length might need adjusting, but this is fairly close to what was requested.
1 parent b5f9c4b commit 1467191

File tree

5 files changed

+89
-28
lines changed

5 files changed

+89
-28
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"react-dom": "^16.9.0",
9595
"react-focus-lock": "^2.2.1",
9696
"react-resizable": "^1.10.1",
97+
"react-transition-group": "^4.4.1",
9798
"resize-observer-polyfill": "^1.5.0",
9899
"sanitize-html": "^1.18.4",
99100
"text-encoding-utf-8": "^1.0.1",
@@ -126,6 +127,7 @@
126127
"@types/qrcode": "^1.3.4",
127128
"@types/react": "^16.9",
128129
"@types/react-dom": "^16.9.8",
130+
"@types/react-transition-group": "^4.4.0",
129131
"@types/zxcvbn": "^4.4.0",
130132
"babel-eslint": "^10.0.3",
131133
"babel-jest": "^24.9.0",

res/css/views/rooms/_RoomBreadcrumbs2.scss

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,32 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
@keyframes breadcrumb-popin {
18-
0% {
19-
// Ideally we'd use `width` instead of `opacity`, but we only
20-
// have 16 nanoseconds to render the frame, and width is expensive.
21-
opacity: 0;
22-
transform: scale(0);
23-
}
24-
100% {
25-
opacity: 1;
26-
transform: scale(1);
27-
}
28-
}
29-
3017
.mx_RoomBreadcrumbs2 {
18+
width: 100%;
19+
3120
// Create a flexbox for the crumbs
3221
display: flex;
3322
flex-direction: row;
3423
align-items: flex-start;
35-
width: 100%;
3624

3725
.mx_RoomBreadcrumbs2_crumb {
3826
margin-right: 8px;
3927
width: 32px;
28+
}
29+
30+
// These classes come from the CSSTransition component. There's many more classes we
31+
// could care about, but this is all we worried about for now. The animation works by
32+
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
33+
// sliding it into view.
34+
&.mx_RoomBreadcrumbs2-enter {
35+
margin-left: -40px; // 32px for the avatar, 8px for the margin
36+
}
37+
&.mx_RoomBreadcrumbs2-enter-active {
38+
margin-left: 0;
4039

41-
// React loves to add elements, so only target the one we want to animate
42-
&:first-child {
43-
animation: breadcrumb-popin 0.3s;
44-
}
40+
// Timing function is as-requested by design.
41+
// NOTE: The transition time MUST match the value passed to CSSTransition!
42+
transition: margin-left 300ms cubic-bezier(0.66, 0.02, 0.36, 1);
4543
}
4644

4745
.mx_RoomBreadcrumbs2_placeholder {

src/components/views/rooms/RoomBreadcrumbs2.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
2323
import defaultDispatcher from "../../../dispatcher/dispatcher";
2424
import Analytics from "../../../Analytics";
2525
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
26+
import { CSSTransition, TransitionGroup } from "react-transition-group";
2627

2728
/*******************************************************************
2829
* CAUTION *
@@ -36,6 +37,14 @@ interface IProps {
3637
}
3738

3839
interface IState {
40+
// Both of these control the animation for the breadcrumbs. For details on the
41+
// actual animation, see the CSS.
42+
//
43+
// doAnimation is to lie to the CSSTransition component (see onBreadcrumbsUpdate
44+
// for info). skipFirst is used to try and reduce jerky animation - also see the
45+
// breadcrumb update function for info on that.
46+
doAnimation: boolean;
47+
skipFirst: boolean;
3948
}
4049

4150
export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState> {
@@ -44,6 +53,11 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
4453
constructor(props: IProps) {
4554
super(props);
4655

56+
this.state = {
57+
doAnimation: true, // technically we want animation on mount, but it won't be perfect
58+
skipFirst: false, // render the thing, as boring as it is
59+
};
60+
4761
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
4862
}
4963

@@ -54,7 +68,16 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
5468

5569
private onBreadcrumbsUpdate = () => {
5670
if (!this.isMounted) return;
57-
this.forceUpdate(); // we have no state, so this is the best we can do
71+
72+
// We need to trick the CSSTransition component into updating, which means we need to
73+
// tell it to not animate, then to animate a moment later. This causes two updates
74+
// which means two renders. The skipFirst change is so that our don't-animate state
75+
// doesn't show the breadcrumb we're about to reveal as it causes a visual jump/jerk.
76+
// The second update, on the next available tick, causes the "enter" animation to start
77+
// again and this time we want to show the newest breadcrumb because it'll be hidden
78+
// off screen for the animation.
79+
this.setState({doAnimation: false, skipFirst: true});
80+
setTimeout(() => this.setState({doAnimation: true, skipFirst: false}), 0);
5881
};
5982

6083
private viewRoom = (room: Room, index: number) => {
@@ -77,14 +100,26 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
77100
)
78101
});
79102

80-
if (tiles.length === 0) {
81-
tiles.push(
82-
<div className="mx_RoomBreadcrumbs2_placeholder">
83-
{_t("No recently visited rooms")}
103+
if (tiles.length > 0) {
104+
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
105+
return (
106+
<CSSTransition
107+
appear={true} in={this.state.doAnimation} timeout={300}
108+
classNames='mx_RoomBreadcrumbs2'
109+
>
110+
<div className='mx_RoomBreadcrumbs2'>
111+
{tiles.slice(this.state.skipFirst ? 1 : 0)}
112+
</div>
113+
</CSSTransition>
114+
);
115+
} else {
116+
return (
117+
<div className='mx_RoomBreadcrumbs2'>
118+
<div className="mx_RoomBreadcrumbs2_placeholder">
119+
{_t("No recently visited rooms")}
120+
</div>
84121
</div>
85122
);
86123
}
87-
88-
return <div className='mx_RoomBreadcrumbs2'>{tiles}</div>;
89124
}
90125
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"types": [
1616
"node",
1717
"react",
18-
"flux"
18+
"flux",
19+
"react-transition-group"
1920
]
2021
},
2122
"include": [

yarn.lock

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@
968968
core-js-pure "^3.0.0"
969969
regenerator-runtime "^0.13.4"
970970

971-
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4":
971+
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
972972
version "7.10.2"
973973
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
974974
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
@@ -1352,6 +1352,13 @@
13521352
dependencies:
13531353
"@types/react" "*"
13541354

1355+
"@types/react-transition-group@^4.4.0":
1356+
version "4.4.0"
1357+
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
1358+
integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
1359+
dependencies:
1360+
"@types/react" "*"
1361+
13551362
"@types/react@*", "@types/react@^16.9":
13561363
version "16.9.35"
13571364
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
@@ -2835,7 +2842,7 @@ cssstyle@^1.0.0:
28352842
dependencies:
28362843
cssom "0.3.x"
28372844

2838-
csstype@^2.2.0:
2845+
csstype@^2.2.0, csstype@^2.6.7:
28392846
version "2.6.10"
28402847
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
28412848
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
@@ -3054,6 +3061,14 @@ doctrine@^3.0.0:
30543061
dependencies:
30553062
esutils "^2.0.2"
30563063

3064+
dom-helpers@^5.0.1:
3065+
version "5.1.4"
3066+
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b"
3067+
integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==
3068+
dependencies:
3069+
"@babel/runtime" "^7.8.7"
3070+
csstype "^2.6.7"
3071+
30573072
dom-serializer@0, dom-serializer@^0.2.1:
30583073
version "0.2.2"
30593074
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -7136,6 +7151,16 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
71367151
react-is "^16.8.6"
71377152
scheduler "^0.19.1"
71387153

7154+
react-transition-group@^4.4.1:
7155+
version "4.4.1"
7156+
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
7157+
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
7158+
dependencies:
7159+
"@babel/runtime" "^7.5.5"
7160+
dom-helpers "^5.0.1"
7161+
loose-envify "^1.4.0"
7162+
prop-types "^15.6.2"
7163+
71397164
react@^16.9.0:
71407165
version "16.13.1"
71417166
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"

0 commit comments

Comments
 (0)