Skip to content

Commit e98b314

Browse files
authored
Merge pull request #5835 from HSLdevcom/AB#583
AB#583 animated personalization feedback elements
2 parents 08f2527 + c433da9 commit e98b314

7 files changed

Lines changed: 481 additions & 264 deletions

File tree

app/component/itinerary/Feedback.js

Lines changed: 125 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import PropTypes from 'prop-types';
22
import cx from 'classnames';
3-
import React from 'react';
3+
import React, { useEffect, useRef, useState } from 'react';
44
import { FormattedMessage, useIntl } from 'react-intl';
55
import Icon from '../Icon';
66
import { useConfigContext } from '../../configurations/ConfigContext';
77

8-
export default function Feedback({
9-
recommended, // true if ranked as best by personalization algo
10-
feedback, // true=likes, false=dislikes, undefined=no feedback yet
11-
giveFeedback, // callback to submit user's feedback action
12-
}) {
13-
const intl = useIntl();
14-
const { colors } = useConfigContext();
8+
const ANIMATION_MS = 1200;
159

16-
let status;
17-
if (feedback === true) {
18-
status = 'personalization-liked';
19-
} else if (feedback === false) {
20-
status = 'personalization-disliked';
21-
} else {
22-
status = 'personalization-ask';
23-
}
10+
function FeedbackLayer({ recommended, status, giveFeedback, animationClass }) {
11+
const { colors } = useConfigContext();
12+
const intl = useIntl();
2413

2514
const favIcon = recommended
2615
? 'icon_star-with-circle'
@@ -40,48 +29,130 @@ export default function Feedback({
4029
const iconProps = iconMap[status];
4130

4231
return (
43-
<div className="feedback-container">
32+
<div className={cx('feedback-layer', animationClass)}>
33+
<div className="feedback-container">
34+
<div
35+
className={cx('feedback-section', {
36+
'feedback-text-posted': status !== 'personalization-ask',
37+
})}
38+
>
39+
<Icon {...iconProps} height={1.4} width={1.4} />
40+
<span>&nbsp;&nbsp;&nbsp;</span>
41+
<FormattedMessage id={status} />
42+
</div>
43+
{status === 'personalization-ask' && (
44+
<div className="feedback-section">
45+
<button
46+
type="button"
47+
className="thumb-button"
48+
onClick={() => giveFeedback(true)}
49+
aria-label={intl.formatMessage({
50+
id: 'personalization-aria-like',
51+
})}
52+
>
53+
<Icon
54+
img="icon_thumb"
55+
color={colors.primary}
56+
height={1}
57+
width={1}
58+
/>
59+
</button>
60+
<button
61+
type="button"
62+
className="thumb-button"
63+
onClick={() => giveFeedback(false)}
64+
aria-label={intl.formatMessage({
65+
id: 'personalization-aria-dislike',
66+
})}
67+
>
68+
<Icon
69+
img="icon_thumb-down"
70+
color={colors.primary}
71+
height={1}
72+
width={1}
73+
/>
74+
</button>
75+
</div>
76+
)}
77+
</div>
78+
</div>
79+
);
80+
}
81+
82+
FeedbackLayer.propTypes = {
83+
recommended: PropTypes.bool.isRequired,
84+
status: PropTypes.string.isRequired,
85+
giveFeedback: PropTypes.func.isRequired,
86+
animationClass: PropTypes.string.isRequired,
87+
};
88+
89+
export default function Feedback({
90+
recommended, // true if ranked as best by personalization algo
91+
feedback, // true=likes, false=dislikes, undefined=no feedback yet
92+
giveFeedback, // callback to submit user's feedback action
93+
}) {
94+
const [isAnimating, setIsAnimating] = useState(false);
95+
const timerRef = useRef(null);
96+
const panelRef = useRef(null);
97+
98+
useEffect(() => {
99+
return () => {
100+
if (timerRef.current) {
101+
clearTimeout(timerRef.current);
102+
}
103+
};
104+
}, []);
105+
106+
const handleGiveFeedback = value => {
107+
if (isAnimating) {
108+
return;
109+
}
110+
111+
setIsAnimating(true);
112+
timerRef.current = setTimeout(() => {
113+
setIsAnimating(false);
114+
panelRef.current?.focus();
115+
}, ANIMATION_MS);
116+
giveFeedback(value);
117+
};
118+
119+
let status;
120+
if (feedback === true) {
121+
status = 'personalization-liked';
122+
} else if (feedback === false) {
123+
status = 'personalization-disliked';
124+
} else {
125+
status = 'personalization-ask';
126+
}
127+
128+
return (
129+
<div ref={panelRef} className="feedback-panel" tabIndex="-1">
130+
{(status !== 'personalization-ask' || isAnimating) && (
131+
<FeedbackLayer
132+
key="knownstate"
133+
recommended={recommended}
134+
status={status}
135+
giveFeedback={handleGiveFeedback}
136+
animationClass={isAnimating ? 'enter' : ''}
137+
/>
138+
)}
139+
{(status === 'personalization-ask' || isAnimating) && (
140+
<FeedbackLayer
141+
key="askstate"
142+
recommended={recommended}
143+
status="personalization-ask"
144+
giveFeedback={handleGiveFeedback}
145+
animationClass={isAnimating ? 'leave' : ''}
146+
/>
147+
)}
44148
<div
45-
className={cx('feedback-section', {
46-
'feedback-text-posted': status !== 'personalization-ask',
47-
})}
149+
className="sr-only"
150+
aria-live="polite"
151+
aria-atomic="true"
152+
role="status"
48153
>
49-
<Icon {...iconProps} height={1.4} width={1.4} />
50-
<span>&nbsp;&nbsp;&nbsp;</span>
51154
<FormattedMessage id={status} />
52155
</div>
53-
{status === 'personalization-ask' && (
54-
<div className="feedback-section">
55-
<button
56-
type="button"
57-
className="thumb-button"
58-
onClick={() => giveFeedback(true)}
59-
aria-label={intl.formatMessage({ id: 'personalization-aria-like' })}
60-
>
61-
<Icon
62-
img="icon_thumb"
63-
color={colors.primary}
64-
height={1}
65-
width={1}
66-
/>
67-
</button>
68-
<button
69-
type="button"
70-
className="thumb-button"
71-
onClick={() => giveFeedback(false)}
72-
aria-label={intl.formatMessage({
73-
id: 'personalization-aria-dislike',
74-
})}
75-
>
76-
<Icon
77-
img="icon_thumb-down"
78-
color={colors.primary}
79-
height={1}
80-
width={1}
81-
/>
82-
</button>
83-
</div>
84-
)}
85156
</div>
86157
);
87158
}

0 commit comments

Comments
 (0)