Skip to content

Commit 05dc857

Browse files
committed
fix(micro-journeys): refactor two-char layout to control its own animation in and out
1 parent 6a9b7c6 commit 05dc857

File tree

3 files changed

+152
-50
lines changed

3 files changed

+152
-50
lines changed

packages/micro-journeys/src/interactive-step.js

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { props, define } from '@bolt/core/utils';
22
import { withLitContext, html, convertSchemaToProps } from '@bolt/core';
33
import { triggerAnims } from '@bolt/components-animate/utils';
44
import classNames from 'classnames/bind';
5+
import { boltTwoCharacterLayoutIs } from '@bolt/micro-journeys/src/two-character-layout';
56
import styles from './interactive-step.scss';
67
import schema from './interactive-step.schema';
78

@@ -23,10 +24,14 @@ class BoltInteractiveStep extends withLitContext() {
2324
this.triggerUpdate();
2425
}
2526

27+
// @ts-ignore
2628
constructor(self) {
2729
self = super(self);
2830
self._isActiveStep = false;
2931
self._isBecomingActive = false;
32+
// These components are responsible for their own inital animate in.
33+
self.animateInInitialExclusions = [boltTwoCharacterLayoutIs];
34+
self.initializedAnimationExclusions = [];
3035
return self;
3136
}
3237

@@ -61,9 +66,13 @@ class BoltInteractiveStep extends withLitContext() {
6166

6267
connectedCallback() {
6368
super.connectedCallback();
64-
6569
this.addEventListener('bolt:transitionend', this.handleAnimationEnd);
66-
70+
this.animateInInitialExclusions.forEach(exclusionComponent => {
71+
this.addEventListener(
72+
`${exclusionComponent}:animation-initialized`,
73+
this.handleExcludedAnimationInitializedOnChild,
74+
);
75+
});
6776
setTimeout(() => {
6877
this.dispatchEvent(
6978
new CustomEvent(`${BoltInteractiveStep.is}:connected`, {
@@ -78,8 +87,13 @@ class BoltInteractiveStep extends withLitContext() {
7887

7988
disconnectedCallback() {
8089
super.disconnectedCallback();
81-
8290
this.removeEventListener('bolt:transitionend', this.handleAnimationEnd);
91+
this.animateInInitialExclusions.forEach(exclusionComponent => {
92+
this.removeEventListener(
93+
`${exclusionComponent}:animation-initialized`,
94+
this.handleExcludedAnimationInitializedOnChild,
95+
);
96+
});
8397

8498
setTimeout(() => {
8599
this.dispatchEvent(
@@ -93,14 +107,47 @@ class BoltInteractiveStep extends withLitContext() {
93107
}, 0);
94108
}
95109

110+
handleExcludedAnimationInitializedOnChild(e) {
111+
this.initializedAnimationExclusions = [
112+
...this.initializedAnimationExclusions,
113+
...e.target.querySelectorAll('bolt-animate'),
114+
];
115+
}
116+
96117
async triggerAnimOuts(durationOverride = null) {
97118
const anims = this.querySelectorAll('bolt-animate');
98119
return triggerAnims({ animEls: anims, stage: 'OUT', durationOverride });
99120
}
100121

101122
async triggerAnimIns() {
102-
const anims = this.querySelectorAll('bolt-animate');
103-
return triggerAnims({ animEls: anims, stage: 'IN' });
123+
let anims = [...this.querySelectorAll('bolt-animate')];
124+
// Filter bolt-animates inside animateInInitialExclusions.
125+
if (this.animateInInitialExclusions.length) {
126+
const animateInInitialExclusions = [
127+
...this.querySelectorAll(
128+
`${this.animateInInitialExclusions.join(
129+
' bolt-animate',
130+
)} bolt-animate`,
131+
),
132+
];
133+
// Tell the excluded components they can animate themselves.
134+
[
135+
...this.querySelectorAll(this.animateInInitialExclusions.join(' ')),
136+
].forEach(exclusion => {
137+
exclusion.setAttribute('parent-animations-triggered', true);
138+
});
139+
anims = anims.filter(animateEl => {
140+
return !animateInInitialExclusions.find(exclusion =>
141+
animateEl.isSameNode(exclusion),
142+
);
143+
});
144+
}
145+
// Add any excluded components that have finished initializing to trigger list.
146+
const animEls = [...anims, ...this.initializedAnimationExclusions];
147+
return triggerAnims({
148+
animEls,
149+
stage: 'IN',
150+
});
104151
}
105152

106153
attributeChangedCallback(name, oldValue, newValue) {

packages/micro-journeys/src/two-character-layout.js

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,96 @@ import {
33
define,
44
hasNativeShadowDomSupport,
55
equalizeRelativeHeights,
6-
persistentlyAttemptToEqualizeRelativeHeights,
76
} from '@bolt/core/utils';
87
import { withLitHtml, html } from '@bolt/core';
98
import classNames from 'classnames/bind';
109
import {
11-
boltCharacterRootClass,
1210
boltCharacterCenterClass,
13-
boltCharacterConnectionClass,
11+
boltCharacterIs,
1412
} from '@bolt/micro-journeys/src/character';
13+
import { boltConnectionIs } from '@bolt/micro-journeys/src/connection';
14+
import { triggerAnims } from '@bolt/components-animate/utils';
1515
import styles from './two-character-layout.scss';
1616

1717
let cx = classNames.bind(styles);
1818

19+
const boltTwoCharacterLayoutIs = 'bolt-two-character-layout';
20+
1921
@define
2022
class BoltTwoCharacterLayout extends withLitHtml() {
21-
static is = 'bolt-two-character-layout';
23+
static is = boltTwoCharacterLayoutIs;
2224

2325
static props = {
2426
noShadow: {
2527
...props.boolean,
2628
...{ default: false },
2729
},
30+
parentAnimationsTriggered: {
31+
...props.boolean,
32+
...{ default: false },
33+
},
2834
};
2935

3036
// @ts-ignore
3137
constructor(self) {
3238
self = super(self);
3339
self.useShadow = hasNativeShadowDomSupport;
34-
self.addEventListener(
35-
'bolt-character:connected',
36-
this.handleComponentConnect,
37-
);
38-
self.addEventListener(
39-
'bolt-connection:connected',
40-
this.handleComponentConnect,
41-
);
42-
self.connectedComponentCount = 0;
40+
self.hasConnection = !!this.querySelector(boltConnectionIs);
41+
self.requiredComponentCount = self.hasConnection ? 3 : 2;
42+
self.renderedComponentCount = 0;
43+
self.isInitialRender = true;
4344
return self;
4445
}
4546

46-
handleComponentConnect(event) {
47-
this.connectedComponentCount++;
48-
if (this.connectedComponentCount >= this.requiredComponentCount) {
49-
this.componentsHaveConnected();
47+
/**
48+
* Callback for when child components report ready. Figure out if they're
49+
* characters and count them if so. Then attempt to initialize.
50+
*
51+
* @param event
52+
*/
53+
handleChildrenReady(event) {
54+
if (
55+
event.detail.name !== boltCharacterIs &&
56+
event.detail.name !== boltConnectionIs
57+
) {
58+
return;
59+
}
60+
this.renderedComponentCount++;
61+
if (this.isConnected) {
62+
this.attemptCharactersAreReadyInitialization();
63+
}
64+
}
65+
66+
/**
67+
* Make sure that the component is rendering for the first time and `bolt-character`s are ready.
68+
* Both characters have to be on the dom with width and height before this can run.
69+
* The problem is that this component connects and components report as rendered
70+
* before content is slotted. Thus, we have to wait for the parent to tell us
71+
* that it has attempted to animate content as a sigh that we can actually
72+
* perform calculations against slotted elements that have height/width.
73+
* We only do this on initial render after we're told the parent has animated.
74+
* After this, bolt-interactive-step is in charge of animating this in and out.
75+
*/
76+
attemptCharactersAreReadyInitialization() {
77+
const charsAreReady =
78+
this.renderedComponentCount >= this.requiredComponentCount;
79+
if (
80+
this.parentAnimationsTriggered &&
81+
charsAreReady &&
82+
this.isInitialRender
83+
) {
84+
this.charactersAreReadyInitialization();
85+
this.isInitialRender = false;
86+
}
87+
}
88+
89+
connected() {
90+
this.addEventListener('ready', this.handleChildrenReady);
91+
}
92+
93+
disconnecting() {
94+
if (this.parentAnimationsTriggered && !this.isInitialRender) {
95+
this.removeEventListener('ready', this.handleChildrenReady);
5096
}
5197
}
5298

@@ -55,55 +101,60 @@ class BoltTwoCharacterLayout extends withLitHtml() {
55101
* to one another so a connection can be centered between them, while
56102
* preserving document flow by avoiding absolute positioning.
57103
*/
58-
componentsHaveConnected() {
59-
this.boltCharacters = [...this.querySelectorAll('bolt-character')];
60-
const eqHeightArgs = this.boltCharacters.map(el => {
104+
charactersAreReadyInitialization = async () => {
105+
const anims = this.querySelectorAll('bolt-animate');
106+
this.boltCharacters = [...this.querySelectorAll(boltCharacterIs)];
107+
const eqHeightArgs = this.boltCharacters.map(character => {
61108
return {
62-
container: el,
63-
elToEqualize: el.renderRoot.querySelector(
109+
container: character,
110+
elToEqualize: character.renderRoot.querySelector(
64111
`.${boltCharacterCenterClass}`,
65112
),
66-
paddingEqualizationTarget: el.renderRoot.querySelector(
67-
`.${boltCharacterRootClass}`,
68-
),
113+
paddingEqualizationTarget: character,
69114
};
70115
});
71-
persistentlyAttemptToEqualizeRelativeHeights(
116+
equalizeRelativeHeights(
72117
eqHeightArgs,
73-
this.setConnectionWidth,
74-
0,
75-
3,
76-
true,
118+
this.hasConnection ? this.setConnectionWidth : null,
77119
);
78-
}
120+
this.triggerUpdate();
121+
122+
triggerAnims({ animEls: anims, stage: 'IN' });
123+
// Tell the parent step that it can now normally animate this element.
124+
this.dispatchEvent(
125+
new CustomEvent(`${BoltTwoCharacterLayout.is}:animation-initialized`, {
126+
bubbles: true,
127+
}),
128+
);
129+
};
79130

80131
/**
81-
* Set the width of `bolt-connection` so it spans from one `bolt-character` to
132+
* Set the width of `bolt-connection` if present so it spans from one `bolt-character` to
82133
* `bolt-character`. Note: requires page refresh.
83134
*/
84135
setConnectionWidth = () => {
85136
this.boltCharacters.forEach((e, i) => {
86-
const connection = e.querySelector('bolt-connection');
137+
const connection = e.querySelector(boltConnectionIs);
87138
const nextCharacter = this.boltCharacters[i + 1];
88139
if (connection && nextCharacter) {
89140
const nextCharacterCenter = nextCharacter.renderRoot.querySelector(
90141
`.${boltCharacterCenterClass}`,
91142
);
92-
// @TODO figure out why that 50% calculation is off by 10px (getBoundingClientRect().x is not at fault)
93143
connection.style.minWidth = `calc(${nextCharacterCenter.getBoundingClientRect()
94-
.x -
95-
connection.getBoundingClientRect().x -
96-
10}px + 50%)`;
144+
.x - connection.getBoundingClientRect().x}px + 50%)`;
97145
}
98146
});
99147
};
100148

101149
render() {
102-
const classes = cx('c-bolt-two-character-layout');
103-
this.requiredComponentCount =
104-
// @ts-ignore
105-
!!this.slot('character--left') + !!this.slot('character--right');
106-
150+
const props = this.validateProps(this.props);
151+
if (this.isInitialRender) {
152+
this.attemptCharactersAreReadyInitialization();
153+
}
154+
const classes = cx('c-bolt-two-character-layout', {
155+
'c-bolt-two-character-layout__initial':
156+
!props.parentAnimationsTriggered && this.isInitialRender,
157+
});
107158
return html`
108159
${this.addStyles([styles])}
109160
<div class="${classes}">
@@ -124,4 +175,4 @@ class BoltTwoCharacterLayout extends withLitHtml() {
124175
}
125176
}
126177

127-
export { BoltTwoCharacterLayout };
178+
export { BoltTwoCharacterLayout, boltTwoCharacterLayoutIs };

packages/micro-journeys/src/two-character-layout.scss

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
position: relative;
88
}
99

10+
&__initial {
11+
opacity: 0;
12+
}
13+
1014
&__character-row {
1115
display: flex;
1216
flex-wrap: nowrap;
@@ -25,8 +29,8 @@
2529

2630
&__character {
2731
display: flex;
28-
flex: 1 1 50%;
29-
justify-content: center;
32+
flex: 0 1 49%;
33+
justify-content: space-between;
3034
align-items: center;
3135

3236
&--left,

0 commit comments

Comments
 (0)