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

[Progress] Split LTI feedback banner into generic reusable component #57933

Merged
merged 4 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
@import 'color';
@import 'font';

.lti-feedback-banner {
.feedback-banner {
$indent: 1rem;

background-color: $light_info_100;
border-color: $light_info_100;
color: $dark_charcoal;
line-height: 2rem;

& .lti-feedback-banner-greeting {
& .feedback-banner-greeting {
display: inline-block;
margin-right: $indent;

Expand All @@ -32,7 +32,7 @@
font-size: 1.3rem;
}

& #lti-feedback-banner-share-more-link {
& #feedback-banner-share-more-link {
font-weight: normal;
}

Expand All @@ -46,7 +46,7 @@
top: 0;
}

& .lti-feedback {
& .feedback {
margin-left: $indent;

& button {
Expand Down
35 changes: 35 additions & 0 deletions apps/src/lib/ui/feedback/FeedbackBanner.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import {StoryFn} from '@storybook/react';
import FeedbackBanner from './FeedbackBanner';

export default {
component: FeedbackBanner,
argTypes: {
answerStatus: {
options: ['', 'unavailable', 'unanswered', 'answered', 'closed'],
control: {type: 'select'},
},
},
};

const Template: StoryFn<typeof FeedbackBanner> = args => (
<FeedbackBanner
{...args}
alertKey="test-feedback-banner"
answer={() => {}}
close={() => {}}
/>
);

export const Default = Template.bind({});
Default.args = {
answerStatus: 'unanswered',
isLoading: false,
closeLabel: 'Close',
question: 'What did you think of this new feature?',
positiveAnswer: 'I liked it',
negativeAnswer: 'I did not like it',
shareMore: 'Would you like to share more?',
shareMoreLink: 'https://example.com',
shareMoreLinkText: 'Share more',
};
133 changes: 133 additions & 0 deletions apps/src/lib/ui/feedback/FeedbackBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import {Alert, Fade} from 'react-bootstrap'; // eslint-disable-line no-restricted-imports
import FontAwesome from '@cdo/apps/templates/FontAwesome';

import './FeedbackBanner.scss';

export const BANNER_STATUS = Object.freeze({
// The initial status of the banner. It means that the status has not been set yet.
UNSET: '',
// The status when the banner is not available. This is typically when the user does not have access to the feature.
// e.g. the teacher is not an LTI teacher.
UNAVAILABLE: 'unavailable',
// The status when the banner is displayed but the user has not yet provided feedback.
UNANSWERED: 'unanswered',
// The status when the user has provided feedback.
ANSWERED: 'answered',
// The status when the banner has been closed by the user.
CLOSED: 'closed',
});

interface FeedbackBannerProps {
alertKey: string;
answerStatus: string;
answer: (satisfied: boolean) => void;
close: () => void;
isLoading: boolean;
closeLabel: string;
question: string;
positiveAnswer: string;
negativeAnswer: string;
shareMore: string;
shareMoreLink: string;
shareMoreLinkText: string;
}

const FeedbackBanner: React.FC<FeedbackBannerProps> = ({
alertKey,
answerStatus,
answer,
close,
isLoading,
closeLabel,
question,
positiveAnswer,
negativeAnswer,
shareMore,
shareMoreLink,
shareMoreLinkText,
}) => {
return (
<Fade
in={(
[BANNER_STATUS.UNANSWERED, BANNER_STATUS.ANSWERED] as string[]
).includes(answerStatus)}
unmountOnExit={true}
>
<Alert
key={alertKey}
bsStyle="info"
className={'feedback-banner'}
aria-labelledby="feedback-banner-title"
closeLabel={closeLabel}
onDismiss={answerStatus === BANNER_STATUS.ANSWERED ? close : undefined}
>
<span className="feedback-banner-greeting">
<FontAwesome
icon="hand-wave"
className="fa-fw"
title=""
aria-hidden="true"
/>
</span>

<Fade in={!isLoading}>
{answerStatus === BANNER_STATUS.UNANSWERED ? (
<span>
<span id="feedback-banner-title" aria-hidden="true">
{question}
</span>

<span className="feedback">
<button
type="button"
title={positiveAnswer}
onClick={() => answer(true)}
>
<FontAwesome
icon="thumbs-o-up"
className="fa-fw"
title=""
aria-hidden="true"
/>
</button>

<button
type="button"
title={negativeAnswer}
onClick={() => answer(false)}
>
<FontAwesome
icon="thumbs-o-down"
className="fa-fw"
title=""
aria-hidden="true"
/>
</button>
</span>
</span>
) : (
<span>
<span id="feedback-banner-title" aria-hidden="true">
{shareMore}
</span>

<span aria-hidden="true"> </span>

<a
id="feedback-banner-share-more-link"
href={shareMoreLink}
target="_blank"
rel="noreferrer"
>
{shareMoreLinkText}
</a>
</span>
)}
</Fade>
</Alert>
</Fade>
);
};

export default FeedbackBanner;
118 changes: 23 additions & 95 deletions apps/src/lib/ui/lti/feedback/LtiFeedbackBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import React, {useState, useEffect, useReducer} from 'react';
import {Alert, Fade} from 'react-bootstrap'; // eslint-disable-line no-restricted-imports
import FontAwesome from '@cdo/apps/templates/FontAwesome';
import {LmsLinks} from '@cdo/apps/util/sharedConstants';
import {trySetLocalStorage, tryGetLocalStorage} from '@cdo/apps/utils';
import {getStore} from '@cdo/apps/redux';
Expand All @@ -11,18 +9,7 @@ import {
} from '@cdo/apps/redux/lti/ltiFeedbackReducer';
import i18n from '@cdo/locale';

import './LtiFeedbackBanner.scss';

// The initial status of the banner. It means that the status has not been set yet.
const UNSET = '';
// The status when the banner is not available. This is typically when the user is not an LTI teacher.
const UNAVAILABLE = 'unavailable';
// The status when the banner is displayed but the user has not yet provided feedback.
const UNANSWERED = 'unanswered';
// The status when the user has provided feedback.
const ANSWERED = 'answered';
// The status when the banner has been closed by the user.
const CLOSED = 'closed';
import FeedbackBanner, {BANNER_STATUS} from '../../feedback/FeedbackBanner';

/**
* LtiFeedbackBanner component
Expand Down Expand Up @@ -51,10 +38,11 @@ const LtiFeedbackBanner: React.FC = () => {
* The status is stored in local storage to persist across sessions.
*/
const [status, setStatus] = useState<string>(() => {
if (!currentUser.isLti || !currentUser.isTeacher) return UNAVAILABLE;
if (!currentUser.isLti || !currentUser.isTeacher)
return BANNER_STATUS.UNAVAILABLE;

let status = tryGetLocalStorage(key, UNSET);
if (status === UNAVAILABLE) status = UNSET;
let status = tryGetLocalStorage(key, BANNER_STATUS.UNSET);
if (status === BANNER_STATUS.UNAVAILABLE) status = BANNER_STATUS.UNSET;

!status && fetchLtiFeedback(ltiFeedbackAction);

Expand All @@ -73,17 +61,17 @@ const LtiFeedbackBanner: React.FC = () => {
*/
useEffect(() => {
if (ltiFeedback === null) {
setStatus(UNANSWERED);
setStatus(BANNER_STATUS.UNANSWERED);
} else if (ltiFeedback) {
setStatus(ANSWERED);
setStatus(BANNER_STATUS.ANSWERED);
}
}, [ltiFeedback]);

/**
* Effect for handling errors.
*/
useEffect(() => {
error && setStatus(UNSET);
error && setStatus(BANNER_STATUS.UNSET);
}, [error]);

/**
Expand All @@ -95,83 +83,23 @@ const LtiFeedbackBanner: React.FC = () => {
/**
* Function for closing the banner.
*/
const close = () => setStatus(CLOSED);
const close = () => setStatus(BANNER_STATUS.CLOSED);

return (
<Fade in={[UNANSWERED, ANSWERED].includes(status)} unmountOnExit={true}>
<Alert
key={key}
bsStyle="info"
className="lti-feedback-banner"
aria-labelledby="lti-feedback-banner-title"
closeLabel={i18n.closeDialog()}
onDismiss={status === ANSWERED ? close : undefined}
>
<span className="lti-feedback-banner-greeting">
<FontAwesome
icon="hand-wave"
className="fa-fw"
title=""
aria-hidden="true"
/>
</span>

<Fade in={!isLoading}>
{status === UNANSWERED ? (
<span>
<span id="lti-feedback-banner-title" aria-hidden="true">
{i18n.lti_feedbackBanner_question()}
</span>

<span className="lti-feedback">
<button
type="button"
title={i18n.lti_feedbackBanner_answer_positive()}
onClick={() => answer(true)}
>
<FontAwesome
icon="thumbs-o-up"
className="fa-fw"
title=""
aria-hidden="true"
/>
</button>

<button
type="button"
title={i18n.lti_feedbackBanner_answer_negative()}
onClick={() => answer(false)}
>
<FontAwesome
icon="thumbs-o-down"
className="fa-fw"
title=""
aria-hidden="true"
/>
</button>
</span>
</span>
) : (
<span>
<span id="lti-feedback-banner-title" aria-hidden="true">
{i18n.lti_feedbackBanner_shareMore_text()}
</span>

<span aria-hidden="true"> </span>

<a
id="lti-feedback-banner-share-more-link"
href={LmsLinks.ADDITIONAL_FEEDBACK_URL}
target="_blank"
rel="noreferrer"
>
{i18n.lti_feedbackBanner_shareMore_link()}
</a>
</span>
)}
</Fade>
</Alert>
</Fade>
<FeedbackBanner
alertKey={key}
answerStatus={status}
answer={answer}
close={close}
isLoading={isLoading}
closeLabel={i18n.closeDialog()}
question={i18n.lti_feedbackBanner_question()}
positiveAnswer={i18n.lti_feedbackBanner_answer_positive()}
negativeAnswer={i18n.lti_feedbackBanner_answer_negative()}
shareMore={i18n.lti_feedbackBanner_shareMore_text()}
shareMoreLink={LmsLinks.ADDITIONAL_FEEDBACK_URL}
shareMoreLinkText={i18n.lti_feedbackBanner_shareMore_link()}
/>
);
};

Expand Down