chore: responsive course card (#64)

This commit is contained in:
leangseu-edx
2022-11-07 15:06:09 -05:00
committed by GitHub
parent dde8d45df3
commit 2a72a85efd
31 changed files with 164 additions and 129 deletions

View File

@@ -15,8 +15,17 @@
.pgn__card-header-content {
margin-top: 1.5rem;
}
.pgn__card-footer .pgn__action-row {
white-space: nowrap;
.pgn__card-footer {
flex-wrap: nowrap;
&.vertical {
flex-direction: column;
}
.pgn__action-row {
align-self: flex-end;
white-space: nowrap;
}
}
.course-card-verify-ribbon-container {

View File

@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActionButton snapshot is collapsed 1`] = `
<Button
arbitary="props"
size="sm"
/>
`;
exports[`ActionButton snapshot is not collapsed 1`] = `
<Button
arbitary="props"
/>
`;

View File

@@ -0,0 +1,8 @@
import { useWindowSize, breakpoints } from '@edx/paragon';
export const useIsCollapsed = () => {
const { width } = useWindowSize();
return width < breakpoints.medium.maxWidth && width > breakpoints.small.maxWidth;
};
export default useIsCollapsed;

View File

@@ -0,0 +1,21 @@
import { useWindowSize, breakpoints } from '@edx/paragon';
import useIsCollapsed from './hooks';
describe('useIsCollapsed', () => {
it('returns true only when it is between medium and small', () => {
// make sure all three breakpoints gap is large enough for test
expect(
(breakpoints.large.maxWidth - 1)
> (breakpoints.medium.maxWidth - 1)
&& (breakpoints.medium.maxWidth - 1)
> (breakpoints.small.maxWidth - 1),
).toBe(true);
useWindowSize.mockReturnValue({ width: breakpoints.large.maxWidth - 1 });
expect(useIsCollapsed()).toEqual(false);
useWindowSize.mockReturnValue({ width: breakpoints.medium.maxWidth - 1 });
expect(useIsCollapsed()).toEqual(true);
useWindowSize.mockReturnValue({ width: breakpoints.small.maxWidth - 1 });
expect(useIsCollapsed()).toEqual(false);
});
});

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { Button } from '@edx/paragon';
import useIsCollapsed from './hooks';
export const ActionButton = (props) => {
const isSmall = useIsCollapsed();
return (
<Button
{...props}
{...isSmall && { size: 'sm' }}
/>
);
};
export default ActionButton;

View File

@@ -0,0 +1,25 @@
import { shallow } from 'enzyme';
import ActionButton from '.';
import useIsCollapsed from './hooks';
jest.mock('./hooks', () => jest.fn());
describe('ActionButton', () => {
const props = {
arbitary: 'props',
};
describe('snapshot', () => {
test('is collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('is not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import ActionButton from './ActionButton';
import messages from './messages';
export const BeginCourseButton = ({ cardId }) => {
@@ -13,13 +13,13 @@ export const BeginCourseButton = ({ cardId }) => {
const { isMasquerading } = hooks.useMasqueradeData();
const { formatMessage } = useIntl();
return (
<Button
<ActionButton
disabled={isMasquerading || !hasAccess}
as="a"
href={homeUrl}
>
{formatMessage(messages.beginCourse)}
</Button>
</ActionButton>
);
};
BeginCourseButton.propTypes = {

View File

@@ -11,6 +11,7 @@ jest.mock('data/redux', () => ({
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
},
}));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
const { homeUrl } = hooks.useCardCourseRunData();

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import ActionButton from './ActionButton';
import messages from './messages';
export const ResumeButton = ({ cardId }) => {
@@ -13,13 +13,13 @@ export const ResumeButton = ({ cardId }) => {
const { isMasquerading } = hooks.useMasqueradeData();
const { formatMessage } = useIntl();
return (
<Button
<ActionButton
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
as="a"
href={resumeUrl}
>
{formatMessage(messages.resume)}
</Button>
</ActionButton>
);
};
ResumeButton.propTypes = {

View File

@@ -15,6 +15,7 @@ jest.mock('data/redux', () => ({
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
},
}));
jest.mock('./ActionButton', () => 'ActionButton');
const { resumeUrl } = hooks.useCardCourseRunData();

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import ActionButton from './ActionButton';
import messages from './messages';
export const SelectSessionButton = ({ cardId }) => {
@@ -14,12 +14,12 @@ export const SelectSessionButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const openSessionModal = hooks.useUpdateSelectSessionModalCallback(cardId);
return (
<Button
<ActionButton
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
onClick={openSessionModal}
>
{formatMessage(messages.selectSession)}
</Button>
</ActionButton>
);
};
SelectSessionButton.propTypes = {

View File

@@ -12,6 +12,7 @@ jest.mock('data/redux', () => ({
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
},
}));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;

View File

@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import ActionButton from './ActionButton';
import messages from './messages';
export const UpgradeButton = ({ cardId }) => {
@@ -15,14 +15,14 @@ export const UpgradeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const isEnabled = (!isMasquerading && canUpgrade);
return (
<Button
<ActionButton
iconBefore={Locked}
variant="outline-primary"
disabled={!isEnabled}
{...isEnabled && { as: 'a', href: upgradeUrl }}
>
{formatMessage(messages.upgrade)}
</Button>
</ActionButton>
);
};
UpgradeButton.propTypes = {

View File

@@ -11,6 +11,7 @@ jest.mock('data/redux', () => ({
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
},
}));
jest.mock('./ActionButton', () => 'ActionButton');
describe('UpgradeButton', () => {
const props = {

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks } from 'data/redux';
import ActionButton from './ActionButton';
import messages from './messages';
export const ViewCourseButton = ({ cardId }) => {
@@ -12,13 +12,13 @@ export const ViewCourseButton = ({ cardId }) => {
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
const { formatMessage } = useIntl();
return (
<Button
<ActionButton
disabled={!hasAccess}
as="a"
href={homeUrl}
>
{formatMessage(messages.viewCourse)}
</Button>
</ActionButton>
);
};
ViewCourseButton.propTypes = {

View File

@@ -11,20 +11,22 @@ jest.mock('data/redux', () => ({
useCardEntitlementData: jest.fn(),
},
}));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
const props = { cardId: 'cardId' };
const defaultProps = { cardId: 'cardId' };
const homeUrl = 'homeUrl';
const createWrapper = ({
hasAccess = false,
isEntitlement = false,
isExpired = false,
propsOveride = {},
}) => {
hooks.useCardCourseRunData.mockReturnValue({ homeUrl });
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
return shallow(<ViewCourseButton {...props} />);
return shallow(<ViewCourseButton {...defaultProps} {...propsOveride} />);
};
describe('ViewCourseButton', () => {

View File

@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
<Button
<ActionButton
as="a"
disabled={false}
href="home-url"
>
Begin Course
</Button>
</ActionButton>
`;

View File

@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
<Button
<ActionButton
as="a"
disabled={false}
href="resumeUrl"
>
Resume
</Button>
</ActionButton>
`;

View File

@@ -1,28 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionButton snapshot renders default button 1`] = `
<Button
<ActionButton
disabled={false}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</Button>
</ActionButton>
`;
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
<Button
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</Button>
</ActionButton>
`;
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
<Button
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</Button>
</ActionButton>
`;

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UpgradeButton snapshot can upgrade 1`] = `
<Button
<ActionButton
as="a"
disabled={false}
href="upgradeUrl"
@@ -9,25 +9,25 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
variant="outline-primary"
>
Upgrade
</Button>
</ActionButton>
`;
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
<Button
<ActionButton
disabled={true}
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>
Upgrade
</Button>
</ActionButton>
`;
exports[`UpgradeButton snapshot masquerading 1`] = `
<Button
<ActionButton
disabled={true}
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>
Upgrade
</Button>
</ActionButton>
`;

View File

@@ -1,21 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewCourseButton learner does not have access to course snapshot 1`] = `
<Button
<ActionButton
as="a"
disabled={true}
href="homeUrl"
>
View Course
</Button>
</ActionButton>
`;
exports[`ViewCourseButton learner has access to course snapshot 1`] = `
<Button
<ActionButton
as="a"
disabled={false}
href="homeUrl"
>
View Course
</Button>
</ActionButton>
`;

View File

@@ -1,6 +1,7 @@
import { shallow } from 'enzyme';
import { hooks } from 'data/redux';
import CourseCardActions from '.';
jest.mock('data/redux', () => ({

View File

@@ -55,22 +55,10 @@ export const CourseCardContent = ({ cardId, orientation }) => {
<Card.Section className="pt-0">
<CourseCardDetails cardId={cardId} />
</Card.Section>
{orientation === 'vertical'
? (
<>
<RelatedProgramsBadge cardId={cardId} />
<Card.Footer orientation="horizontal">
<CourseCardActions cardId={cardId} />
</Card.Footer>
</>
) : (
<Card.Footer
orientation="vertical"
textElement={<RelatedProgramsBadge cardId={cardId} />}
>
<CourseCardActions cardId={cardId} />
</Card.Footer>
)}
<Card.Footer orientation={orientation}>
<RelatedProgramsBadge cardId={cardId} />
<CourseCardActions cardId={cardId} />
</Card.Footer>
</Card.Body>
</>
);

View File

@@ -3,7 +3,7 @@
exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `
<Fragment>
<Button
className="pl-0 mr-0"
className="pl-0 mr-0 justify-content-start align-self-start flex-shrink-1"
data-testid="RelatedProgramsBadge"
onClick={[MockFunction useRelatedProgramsBadge.openModal]}
size="sm"

View File

@@ -20,7 +20,7 @@ export const RelatedProgramsBadge = ({ cardId }) => {
<>
<Button
data-testid="RelatedProgramsBadge"
className="pl-0 mr-0"
className="pl-0 mr-0 justify-content-start align-self-start flex-shrink-1"
variant="tertiary"
size="sm"
onClick={openModal}

View File

@@ -38,12 +38,12 @@ exports[`CourseCardContent snapshot not verified 1`] = `
cardId="test-card-id"
/>
</Card.Section>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<Card.Footer
orientation="horizontal"
orientation="vertical"
>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<CourseCardActions
cardId="test-card-id"
/>
@@ -107,13 +107,11 @@ exports[`CourseCardContent snapshot orientation horizontal 1`] = `
/>
</Card.Section>
<Card.Footer
orientation="vertical"
textElement={
<RelatedProgramsBadge
cardId="test-card-id"
/>
}
orientation="horizontal"
>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<CourseCardActions
cardId="test-card-id"
/>
@@ -176,12 +174,12 @@ exports[`CourseCardContent snapshot orientation vertical 1`] = `
cardId="test-card-id"
/>
</Card.Section>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<Card.Footer
orientation="horizontal"
orientation="vertical"
>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<CourseCardActions
cardId="test-card-id"
/>

View File

@@ -7,14 +7,10 @@ import hooks from './hooks';
export const columnConfig = {
courseList: {
sm: { span: 12, offset: 0 },
md: { span: 10, offset: 1 },
lg: { span: 12, offset: 0 },
xl: { span: 8, offset: 0 },
},
sidebar: {
sm: { span: 12, offset: 0 },
md: { span: 10, offset: 1 },
lg: { span: 12, offset: 0 },
xl: { span: 4, offset: 0 },
},

View File

@@ -14,18 +14,6 @@ exports[`DashboardLayout collapsed snapshot 1`] = `
"span": 12,
}
}
md={
Object {
"offset": 1,
"span": 10,
}
}
sm={
Object {
"offset": 0,
"span": 12,
}
}
xl={
Object {
"offset": 0,
@@ -43,18 +31,6 @@ exports[`DashboardLayout collapsed snapshot 1`] = `
"span": 12,
}
}
md={
Object {
"offset": 1,
"span": 10,
}
}
sm={
Object {
"offset": 0,
"span": 12,
}
}
xl={
Object {
"offset": 0,
@@ -82,18 +58,6 @@ exports[`DashboardLayout not collapsed snapshot 1`] = `
"span": 12,
}
}
md={
Object {
"offset": 1,
"span": 10,
}
}
sm={
Object {
"offset": 0,
"span": 12,
}
}
xl={
Object {
"offset": 0,
@@ -111,18 +75,6 @@ exports[`DashboardLayout not collapsed snapshot 1`] = `
"span": 12,
}
}
md={
Object {
"offset": 1,
"span": 10,
}
}
sm={
Object {
"offset": 0,
"span": 12,
}
}
xl={
Object {
"offset": 0,

View File

@@ -5,7 +5,7 @@ exports[`MasqueradeBar snapshot can masquerade 1`] = `
className="w-100 shadow-sm px-2"
>
<Form
className="masquerade-bar col-sm-12 col-md-10 col-12"
className="masquerade-bar w-100"
>
<FormLabel
className="masquerade-form-label"
@@ -47,7 +47,7 @@ exports[`MasqueradeBar snapshot can masquerade with input 1`] = `
className="w-100 shadow-sm px-2"
>
<Form
className="masquerade-bar col-sm-12 col-md-10 col-12"
className="masquerade-bar w-100"
>
<FormLabel
className="masquerade-form-label"
@@ -91,7 +91,7 @@ exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = `
className="w-100 shadow-sm px-2"
>
<Form
className="masquerade-bar col-sm-12 col-md-10 col-12"
className="masquerade-bar w-100"
>
<FormLabel
className="masquerade-form-label"
@@ -137,7 +137,7 @@ exports[`MasqueradeBar snapshot is masquerading pending 1`] = `
className="w-100 shadow-sm px-2"
>
<Form
className="masquerade-bar col-sm-12 col-md-10 col-12"
className="masquerade-bar w-100"
>
<FormLabel
className="masquerade-form-label"
@@ -179,7 +179,7 @@ exports[`MasqueradeBar snapshot is masquerading with input 1`] = `
className="w-100 shadow-sm px-2"
>
<Form
className="masquerade-bar col-sm-12 col-md-10 col-12"
className="masquerade-bar w-100"
>
<FormLabel
className="masquerade-form-label"

View File

@@ -37,7 +37,7 @@ export const MasqueradeBar = () => {
return (
<div className="w-100 shadow-sm px-2">
<Form className="masquerade-bar col-sm-12 col-md-10 col-12">
<Form className="masquerade-bar w-100">
{isMasquerading ? (
<>
<FormLabel inline className="masquerade-form-label">

View File

@@ -12,6 +12,7 @@ export const htmlProps = StrictDict({
onClick: 'onClick',
onChange: 'onChange',
onBlur: 'onBlur',
size: 'size',
});
export default {