Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added: Add FloatingActionButton and UserFeedback components #692

Merged
merged 19 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9e32707
chore: Remove port 6006 from docker-compose.yaml as it's unused
drikusroor Dec 27, 2023
76693ad
story: Add Final and UserFeedback stories
drikusroor Dec 27, 2023
e90a4d4
style(UserFeedback): Improve borders between input and button
drikusroor Dec 27, 2023
46d54fc
test(UserFeedback): Add UserFeedback component tests
drikusroor Dec 27, 2023
53ac963
chore(deps): Update sass version in package.json and yarn.lock
drikusroor Dec 28, 2023
a83b5ed
feat: Add FloatingActionButton component
drikusroor Dec 28, 2023
9a3d24b
style: Add breakpoints to variables.scss
drikusroor Dec 28, 2023
97c830f
style: Refactor UserFeedback component and styles to include vertical…
drikusroor Dec 28, 2023
0d5d45e
style: Update background and color variables in FloatingActionButton.…
drikusroor Dec 28, 2023
8e80f1d
feat: Add overlay to close floating action button
drikusroor Dec 28, 2023
70454a0
feat: Add FloatingActionButton and UserFeedback components
drikusroor Dec 28, 2023
9febc76
feat: Add floating action button for user feedback
drikusroor Dec 28, 2023
8ee2489
style: Update FloatingActionButton background color
drikusroor Dec 28, 2023
849ed31
test: Refactor FloatingActionButton component and add unit tests
drikusroor Dec 28, 2023
61efb62
feat: Show floating feedback button for gold-msi
drikusroor Dec 28, 2023
64c6d32
story: Add FloatingActionButton position stories
drikusroor Jan 8, 2024
98ddbc1
fix: Fix FloatingActionButton.scss position and border-radius for eve…
drikusroor Jan 8, 2024
3152658
docs: Explain FloatingActionButton position options
drikusroor Jan 8, 2024
b5e46fc
refactor: Remove show_float_button from feedback_info
drikusroor Jan 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions backend/experiment/rules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,20 @@ def __init__(self):
def feedback_info(self):
feedback_body = render_to_string('feedback/user_feedback.html', {'email': self.contact_email})
return {
# Header above the feedback form
'header': _("Do you have any remarks or questions?"),

# Button text
'button': _("Submit"),

# Body of the feedback form, can be HTML. Shown under the button
'contact_body': feedback_body,
'thank_you': _("We appreciate your feedback!")

# Thank you message after submitting feedback
'thank_you': _("We appreciate your feedback!"),

# Show a floating button on the right side of the screen to open the feedback form
'show_float_button': False,
}

def calculate_score(self, result, data):
Expand Down Expand Up @@ -112,7 +122,7 @@ def get_single_question(self, session, randomize=False):
feedback_form=Form([question], is_skippable=question.is_skippable))
except StopIteration:
return None

def get_questionnaire(self, session, randomize=False, cutoff_index=None):
''' Get a list of questions to be asked in succession '''

Expand All @@ -127,7 +137,7 @@ def get_questionnaire(self, session, randomize=False, cutoff_index=None):
feedback_form=Form([question], is_skippable=question.is_skippable)
))
return trials

def social_media_info(self, experiment, score):
current_url = "{}/{}".format(settings.RELOAD_PARTICIPANT_TARGET,
experiment.slug
Expand Down
5 changes: 5 additions & 0 deletions backend/experiment/rules/gold_msi.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ def next_round(self, session, request_session=None):
return questions
else:
return final_action_with_optional_button(session, '', request_session)

def feedback_info(self):
info = super().feedback_info()
info['show_float_button'] = True
return info
drikusroor marked this conversation as resolved.
Show resolved Hide resolved
1 change: 0 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ services:
- REACT_APP_SENTRY_DSN=${REACT_APP_SENTRY_DSN}
ports:
- 3000:3000
- 6006:6006
command: sh -c "yarn scss && yarn start"
volumes:
db_data:
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"react-scripts": "5.0.0",
"react-select": "^5.4.0",
"react-transition-group": "^4.4.5",
"sass": "^1.50"
"sass": "^1.69.5"
},
"scripts": {
"start": "react-scripts start",
Expand Down
22 changes: 18 additions & 4 deletions frontend/src/components/Experiment/Experiment.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import Trial from "../Trial/Trial";
import useResultHandler from "../../hooks/useResultHandler";
import Info from "../Info/Info";
import classNames from "classnames";
import FloatingActionButton from "components/FloatingActionButton/FloatingActionButton";
import UserFeedback from "components/UserFeedback/UserFeedback";

// Experiment handles the main experiment flow:
// - Loads the experiment and participant
Expand Down Expand Up @@ -55,7 +57,7 @@ const Experiment = ({ match, location }) => {
[loadState]
);

const updateActions = useCallback( (currentActions) => {
const updateActions = useCallback((currentActions) => {
let newActions = currentActions;
const newState = newActions.shift();
loadState(newState);
Expand Down Expand Up @@ -213,21 +215,33 @@ const Experiment = ({ match, location }) => {
title={state.title}
logoClickConfirm={
["FINAL", "ERROR", "TOONTJEHOGER"].includes(key) ||
// Info pages at end of experiment
(key === "INFO" &&
(!state.next_round || !state.next_round.length))
// Info pages at end of experiment
(key === "INFO" &&
(!state.next_round || !state.next_round.length))
? null
: "Are you sure you want to stop this experiment?"
}
className={className}
>
{render(state.view)}

{experiment?.feedback_info?.show_float_button && (
<FloatingActionButton>
<UserFeedback
experimentSlug={experiment.slug}
participant={participant}
feedbackInfo={experiment.feedback_info}
inline={false} />
</FloatingActionButton>
)}
</DefaultPage>
) : (
<div className="loader-container">
<Loading />
</div>
)}


</CSSTransition>
</TransitionGroup>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

import React from 'react';
import classNames from 'util/classNames';

const FloatingActionButton = ({
children,
icon = 'fa-comment',
position = 'center-right',
className,
}) => {

const [expanded, setExpanded] = React.useState(false);

/**
* @param {string} position
* @returns {string}
*/
const getPositionClassNames = (position) => {
const [vertical, horizontal] = position.split('-');
return `floating-action-button--${vertical} ${horizontal ? `floating-action-button--${horizontal}` : ''}`;
drikusroor marked this conversation as resolved.
Show resolved Hide resolved
}

return (<>
<div
data-testid="floating-action-button"
className={
classNames("floating-action-button",
getPositionClassNames(position),
expanded && 'floating-action-button--expanded',
className
)}
>
<button
data-testid="floating-action-button__toggle-button"
className='floating-action-button__toggle-button'
onClick={() => setExpanded(!expanded)}
>
<i
data-testid="floating-action-button__icon"
className={`floating-action-button__icon fa ${expanded ? 'fa-times' : icon}`}
/>
</button>
<div
data-testid="floating-action-button__content"
className='floating-action-button__content'
>
{children}
</div>
</div>
<div
data-testid="floating-action-button__overlay"
className={
classNames(
'floating-action-button__overlay',
expanded && 'floating-action-button__overlay--expanded'
)}
onClick={() => setExpanded(false)}
aria-hidden={expanded ? 'false' : 'true'}
role="presentation"
>
</div>
</>
);
};

export default FloatingActionButton;

113 changes: 113 additions & 0 deletions frontend/src/components/FloatingActionButton/FloatingActionButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
@import "../../scss/variables.scss";

.floating-action-button {
$self: &;

display: flex;
flex-direction: row;
position: fixed;
z-index: 100;
cursor: pointer;
transition-property: transform, left, right, top, bottom, filter;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
filter: drop-shadow(0, 0, 0, rgba(0, 0, 0, 0));

&--expanded {
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.2));
}

&--left {
right: auto;
left: 0px;

&#{$self}--expanded {
left: 200px;
}
}

&--right {
left: auto;
right: -200px;

&#{$self}--expanded {
right: 0px;
}
}

&--top {
align-items: flex-start;
bottom: auto;
top: 0px;
}

&--bottom {
align-items: flex-end;
top: auto;
bottom: 0px;
}

// vertically centered
&--center {
align-items: center;
top: 50%;
transform: translateY(-50%);
}

&__toggle-button {
background: $gray;
border: none;
padding: .5rem;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}

&__icon {
width: 24px;
height: 24px;
color: $black;
}

&__content {
background: $gray;
border: none;
padding: .5rem;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
width: 200px;
min-height: 100px;
max-height: 100vh;

#{$self}--left & {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}

#{$self}--right & {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
}

&__overlay {
position: fixed;
z-index: 99;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,.2);
opacity: 0;
pointer-events: none;
transition: opacity .2s ease-in-out;

&--expanded {
opacity: 1;
pointer-events: all;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import FloatingActionButton from './FloatingActionButton';

describe('FloatingActionButton', () => {
it('renders the button with the initial icon', () => {
render(<FloatingActionButton icon="fa-comment" />);
const icon = screen.getByTestId('floating-action-button__icon');
expect(icon).toBeInTheDocument();
expect(icon).toHaveClass('fa-comment');
});

it('toggles the content on click', () => {
const { getByTestId } = render(<FloatingActionButton icon="fa-comment"><div>Test Content</div></FloatingActionButton>);

const toggleButton = getByTestId('floating-action-button__toggle-button');
fireEvent.click(toggleButton);

const content = getByTestId('floating-action-button');
expect(content).toHaveClass('floating-action-button--expanded');

fireEvent.click(toggleButton);
expect(content).not.toHaveClass('floating-action-button--expanded');
});

it('displays the correct icon when expanded', () => {
const { getByTestId } = render(<FloatingActionButton icon="fa-comment" />);

const toggleButton = getByTestId('floating-action-button__toggle-button');
fireEvent.click(toggleButton);

const icon = getByTestId('floating-action-button__icon');
expect(icon).toBeInTheDocument();
expect(icon).toHaveClass('fa-times');
});

it('closes the expanded content when the overlay is clicked', () => {
const { getByTestId } = render(<FloatingActionButton icon="fa-comment"><div>Test Content</div></FloatingActionButton>);

const toggleButton = getByTestId('floating-action-button__toggle-button');
fireEvent.click(toggleButton);

const overlay = getByTestId('floating-action-button__overlay');
fireEvent.click(overlay);

const content = getByTestId('floating-action-button__content');
expect(content).not.toHaveClass('floating-action-button--expanded');
});

it('initially renders in a collapsed state', () => {
render(<FloatingActionButton icon="fa-comment" />);
expect(screen.getByTestId('floating-action-button')).not.toHaveClass('floating-action-button--expanded');
});

it('correctly applies position classes', () => {
render(<FloatingActionButton position="bottom-left" />);
expect(screen.getByTestId('floating-action-button')).toHaveClass('floating-action-button--bottom');
expect(screen.getByTestId('floating-action-button')).toHaveClass('floating-action-button--left');
});

it('applies custom class name', () => {
render(<FloatingActionButton className="custom-class" />);
expect(screen.getByTestId('floating-action-button')).toHaveClass('custom-class');
});

it('updates aria-hidden attribute of overlay correctly', () => {
render(<FloatingActionButton />);
const overlay = screen.getByTestId('floating-action-button__overlay');
expect(overlay).toHaveAttribute('aria-hidden', 'true');
fireEvent.click(screen.getByTestId('floating-action-button__toggle-button'));
expect(overlay).toHaveAttribute('aria-hidden', 'false');
});
});
Loading
Loading