Skip to content

Commit

Permalink
feedback component v2 (#5778)
Browse files Browse the repository at this point in the history
* remove cloudscape designs

* remove unused import

* update menu tests

* update feedback icons

* remove extra feedback links

* change visibility of feedback component on load

* updated sizing and color, added close button

* add end of content feedback component

* remove pill for phase 1

* fix tests

* feedback pill changes

* fixing animation on feedback component

* animation for bottom feedback and logic for hiding

* fix tests

* changes from alee

* put back removed package

* remove floating feedback component

---------

Co-authored-by: Jacob Logan <lognjc@amazon.com>
Co-authored-by: katiegoines <katiegoines@gmail.com>
  • Loading branch information
3 people authored Aug 23, 2023
1 parent 5c551ed commit a1673a8
Show file tree
Hide file tree
Showing 26 changed files with 622 additions and 867 deletions.
6 changes: 5 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
// Setup file to extend jest-dom, referenced in packages.json under "jest"
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom/extend-expect';

if (typeof window.URL.createObjectURL === 'undefined') {
window.URL.createObjectURL = jest.fn();
}
7 changes: 1 addition & 6 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,7 @@ export default async (phase, { defaultConfig }) => {
},
exportPathMap,
trailingSlash: true,
transpilePackages: [
'@algolia/autocomplete-shared',
'@cloudscape-design/components',
'@cloudscape-design/component-toolkit',
'next-image-export-optimizer'
],
transpilePackages: ['@algolia/autocomplete-shared', 'next-image-export-optimizer'],
// eslint-disable-next-line @typescript-eslint/require-await
async headers() {
return [
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"unist-util-visit": "^4.1.0"
},
"devDependencies": {
"@cloudscape-design/jest-preset": "^2.0.0",
"@mdx-js/loader": "^1.6.22",
"@next/mdx": "^10.1.3",
"@stencil/core": "^1.17.0",
Expand Down
3 changes: 1 addition & 2 deletions preset.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const tsPreset = require('ts-jest/presets/js-with-babel/jest-preset');
const cloudscapePreset = require('@cloudscape-design/jest-preset');

module.exports = Object.assign(tsPreset, cloudscapePreset);
module.exports = Object.assign(tsPreset);
10 changes: 7 additions & 3 deletions src/components/ExternalLink/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from "react";
import {ExternalLinkGraphic} from "./styles";
import {trackExternalLink} from "../../utils/track";
import React from 'react';
import { ExternalLinkGraphic } from './styles';
import { trackExternalLink } from '../../utils/track';
import { ExternalLinkIcon } from '../Icons';

type ExternalLinkProps = {
graphic?: string;
href: string;
anchorTitle?: string;
icon?: boolean;
};

const ExternalLink: React.FC<ExternalLinkProps> = ({
children,
graphic,
href,
anchorTitle,
icon
}) => {
return (
<a
Expand All @@ -31,6 +34,7 @@ const ExternalLink: React.FC<ExternalLinkProps> = ({
src={`/assets/external-link-${graphic}.svg`}
/>
)}
{icon && <ExternalLinkIcon />}
</a>
);
};
Expand Down
11 changes: 5 additions & 6 deletions src/components/Feedback/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ describe('Feedback', () => {

render(component);

const thumbsUp = screen.getByText('Yes');
const thumbsDown = screen.getByText('No');
const thumbsUp = screen.getByLabelText('Yes');
const thumbsDown = screen.getByLabelText('No');

expect(thumbsUp).toBeInTheDocument();
expect(thumbsDown).toBeInTheDocument();
Expand All @@ -36,8 +36,8 @@ describe('Feedback', () => {

render(component);

const thumbsUp = screen.getByText('Yes');
const thumbsDown = screen.getByText('No');
const thumbsUp = screen.getByLabelText('Yes');
const thumbsDown = screen.getByLabelText('No');

expect(thumbsUp).toBeInTheDocument();
expect(thumbsDown).toBeInTheDocument();
Expand All @@ -52,12 +52,11 @@ describe('Feedback', () => {

it('should call trackFeedbackSubmission request when either button is clicked', async () => {
jest.spyOn(trackModule, 'trackFeedbackSubmission');

const component = <Feedback />;

render(component);

const thumbsDown = screen.getByText('No');
const thumbsDown = screen.getByLabelText('No');

userEvent.click(thumbsDown);

Expand Down
258 changes: 170 additions & 88 deletions src/components/Feedback/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { useCallback, useRef, useState } from 'react';
import { forwardRef, useCallback, useState } from 'react';
import {
FeedbackContainer,
VoteButton,
VoteButtonAfter,
VoteButtonsContainer,
Toggle,
FeedbackMobileContainer,
ThankYouContainer
FeedbackText,
FeedbackTextAfter,
ButtonStyles
} from './styles';
import { useEffect } from 'react';
import { trackFeedbackSubmission } from '../../utils/track';
import {
ThumbsUpIcon,
ThumbsDownIcon,
ThumbsUpFilledIcon,
ThumbsDownFilledIcon
} from '../Icons';
import ExternalLink from '../ExternalLink';

enum FeedbackState {
START = 'START',
END = 'END',
UP = 'UP',
DOWN = 'DOWN',
HIDDEN = 'HIDDEN'
}

Expand All @@ -23,99 +31,173 @@ type Feedback = {
comment?: string;
};

export default function Feedback() {
const [state, setState] = useState<FeedbackState>(FeedbackState.START);
const feedbackQuestion = 'Was this page helpful?';
const feedbackAppreciation = 'Thank you for your feedback!';
// eslint-disable-next-line no-empty-pattern
const Feedback = forwardRef(function Feedback({}, ref) {
const [state, setState] = useState(FeedbackState.START);

// Feedback Component Customizations
const c = {
feedbackQuestion: 'Was this page helpful?',
yesVoteResponse: 'Thanks for your feedback!',
noVoteResponse: 'Thanks for your feedback!',
noVoteCTA: 'Can you provide more details?',
noVoteCTAButton: 'File an issue on GitHub',
ctaIcon: 'external',
iconPosition: 'right',
buttonLink: 'https://github.com/aws-amplify/docs/issues/new/choose'
};

const onYesVote = useCallback(() => {
setState(FeedbackState.END);
let currentState = state;

const onYesVote = useCallback((e) => {
trackFeedbackSubmission(true);
}, []);
const yesButton = e.currentTarget;
const noButton = yesButton.nextSibling;
const feedbackComponent = yesButton.parentElement.parentElement;
const feedbackText = feedbackComponent.getElementsByTagName('p')[0];
const feedbackTextWidth = feedbackText.offsetWidth;

const transitionUpButton = [
{
maxWidth: yesButton.offsetWidth + 'px',
overflow: 'visible'
},
{
maxWidth: '40px',
overflow: 'hidden',
color: 'green',
border: '1px solid green',
transform: `translateX(-${feedbackTextWidth}px)`,
marginLeft: '0px'
}
];

const transitionDownButton = [
{
maxWidth: noButton.offsetWidth + 'px',
overflow: 'visible'
},
{
maxWidth: 0,
overflow: 'hidden',
margin: 0,
padding: 0,
border: 'none'
}
];

const transitionFeedbackText = [
{ transform: 'translateX(-40px)', opacity: 0 }
];

const animationTiming = {
duration: 300,
iterations: 1,
fill: 'forwards'
};

yesButton.animate(transitionUpButton, animationTiming);
noButton.animate(transitionDownButton, animationTiming);
feedbackText.animate(transitionFeedbackText, animationTiming);

const onNoVote = useCallback(() => {
setState(FeedbackState.END);
setTimeout(function() {
currentState = FeedbackState.UP;
setState(currentState);
}, 300);
}, []);

const onNoVote = useCallback((e) => {
trackFeedbackSubmission(false);
const feedbackContent = e.currentTarget.parentNode.parentNode;

feedbackContent.classList.add('fadeOut');

setTimeout(function() {
currentState = FeedbackState.DOWN;
feedbackContent.classList.remove('fadeOut');
feedbackContent.classList.add('fadeIn');
setState(currentState);
}, 300);
}, []);

return (
<FeedbackContainer
style={state === FeedbackState.HIDDEN ? { display: 'none' } : {}}
id="feedback-container"
ref={ref}
aria-hidden={state == FeedbackState.UP ? true : false}
>
{state == FeedbackState.START ? (
<>
<p>{feedbackQuestion}</p>
<VoteButtonsContainer>
<VoteButton onClick={onYesVote}>
<img src="/assets/thumbs-up.svg" alt="Thumbs up" />
Yes
</VoteButton>
<VoteButton onClick={onNoVote}>
<img src="/assets/thumbs-down.svg" alt="Thumbs down" />
No
</VoteButton>
</VoteButtonsContainer>
</>
) : (
<ThankYouContainer>
<p>{feedbackAppreciation}</p>
</ThankYouContainer>
)}
{(() => {
switch (state) {
case 'START':
return (
<div
id="start-state"
aria-label={c.feedbackQuestion}
tabIndex={0}
>
<FeedbackText>{c.feedbackQuestion}</FeedbackText>
<VoteButtonsContainer>
<VoteButton
onClick={onYesVote}
aria-label="Yes"
role="button"
tabIndex={0}
>
<ThumbsUpIcon />
<FeedbackText>Yes</FeedbackText>
</VoteButton>
<VoteButton
onClick={onNoVote}
aria-label="No"
role="button"
tabIndex={0}
>
<ThumbsDownIcon />
<FeedbackText>No</FeedbackText>
</VoteButton>
</VoteButtonsContainer>
</div>
);
case 'UP':
return (
<div className="up">
<VoteButtonsContainer className="up-response">
<VoteButtonAfter className="up-response">
<ThumbsUpFilledIcon />
</VoteButtonAfter>
<FeedbackTextAfter className="up-response">
{c.yesVoteResponse}
</FeedbackTextAfter>
</VoteButtonsContainer>
</div>
);
case 'DOWN':
return (
<div className="down">
<VoteButtonsContainer className="down-response">
<VoteButtonAfter className="down-response">
<ThumbsDownFilledIcon />
</VoteButtonAfter>
<FeedbackTextAfter className="down-response">
{c.noVoteResponse}
</FeedbackTextAfter>
</VoteButtonsContainer>
<FeedbackTextAfter className="cta">
{c.noVoteCTA}
</FeedbackTextAfter>
<ButtonStyles>
<ExternalLink href={c.buttonLink} icon={true}>
{c.noVoteCTAButton}
</ExternalLink>
</ButtonStyles>
</div>
);
default:
return <div></div>;
}
})()}
</FeedbackContainer>
);
}
});

export function FeedbackToggle() {
const [inView, setInView] = useState(false);
const feedbackContainer = useRef(null);

function toggleView() {
if (inView) {
setInView(false);
} else {
setInView(true);
}
}

function handleClickOutside(e) {
if (
feedbackContainer.current &&
feedbackContainer.current.contains(e.target)
) {
// inside click
return;
}
// outside click
setInView(false);
}

useEffect(() => {
if (inView) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [inView]);

return (
<div ref={feedbackContainer}>
<FeedbackMobileContainer style={inView ? {} : { display: 'none' }}>
<Feedback></Feedback>
</FeedbackMobileContainer>
<Toggle
onClick={() => {
toggleView();
}}
>
<img src="/assets/thumbs-up.svg" alt="Thumbs up" />
<img src="/assets/thumbs-down.svg" alt="Thumbs down" />
</Toggle>
</div>
);
}
export default Feedback;
Loading

0 comments on commit a1673a8

Please sign in to comment.