Skip to content

Commit

Permalink
Create new chip component to represent grade status (#6658)
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Sep 10, 2024
1 parent 96de0c2 commit 08c5d13
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 6 deletions.
12 changes: 7 additions & 5 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ gulp.task('build-css', () =>
'./lms/static/styles/lms.scss',
'./lms/static/styles/ui-playground.scss',
],
{ tailwindConfig }
)
{ tailwindConfig },
),
);

gulp.task('watch-css', () => {
Expand All @@ -33,9 +33,11 @@ gulp.task('watch-css', () => {
'./lms/static/scripts/frontend_apps/**/*.ts',
'./lms/static/scripts/frontend_apps/**/*.tsx',
'./lms/static/scripts/ui-playground/**/*.js',
'./lms/static/scripts/ui-playground/**/*.ts',
'./lms/static/scripts/ui-playground/**/*.tsx',
],
{ ignoreInitial: false },
gulp.series('build-css')
gulp.series('build-css'),
);
});

Expand All @@ -58,6 +60,6 @@ gulp.task(
karmaConfig: 'lms/static/scripts/karma.config.cjs',
rollupConfig: 'rollup-tests.config.js',
testsPattern: 'lms/static/scripts/**/*-test.js',
})
)
}),
),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import classnames from 'classnames';

export type GradeStatusChipProps = {
/**
* A grade, from 0 to 100, that will be used to render the corresponding
* color combination.
*/
grade: number;
};

/**
* A badge where the corresponding color combination is calculated from a grade
* from 0 to 100, following the next table:
*
* 100 - bright green
* 80-99 - light green
* 50-79 - yellow
* 1-49 - light red
* 0 - bright red
* other - grey
*/
export default function GradeStatusChip({ grade }: GradeStatusChipProps) {
const gradeIsInvalid = grade < 0 || grade > 100;

return (
<div
className={classnames('rounded font-bold inline-block px-2 py-0.5', {
// We would usually use our standard `green-success` and `red-error`
// colors here, but they don't have enough contrast when used with
// white text and a small font.
// Instead, we use slightly darker shades of green and red.
'bg-[#008558] text-white': grade === 100,
'bg-[#D7373A] text-white': grade === 0,
'bg-green-200 text-green-900': grade >= 80 && grade < 100,
'bg-amber-100 text-amber-900': grade >= 50 && grade < 80,
'bg-red-200 text-red-900': grade >= 1 && grade < 50,
'bg-grey-3 text-grey-7': gradeIsInvalid,
})}
>
{grade}
{!gradeIsInvalid && '%'}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { checkAccessibility } from '@hypothesis/frontend-testing';
import { mount } from 'enzyme';

import GradeStatusChip from '../GradeStatusChip';

describe('GradeStatusChip', () => {
function renderComponent(grade) {
return mount(<GradeStatusChip grade={grade} />);
}

[0, 20, 48, 77, 92, 100].forEach(grade => {
it('renders valid grades as percentage', () => {
const wrapper = renderComponent(grade);
assert.equal(wrapper.text(), `${grade}%`);
});
});

[-20, 150].forEach(grade => {
it('renders invalid grades verbatim', () => {
const wrapper = renderComponent(grade);
assert.equal(wrapper.text(), `${grade}`);
});
});

it(
'should pass a11y checks',
checkAccessibility([
{
name: '100',
content: () => renderComponent(100),
},
{
name: '80',
content: () => renderComponent(80),
},
{
name: '68',
content: () => renderComponent(68),
},
{
name: '38',
content: () => renderComponent(38),
},
{
name: '0',
content: () => renderComponent(0),
},
{
name: '-20',
content: () => renderComponent(-20),
},
{
name: '150',
content: () => renderComponent(150),
},
]),
);
});
103 changes: 103 additions & 0 deletions lms/static/scripts/ui-playground/components/GradeStatusChipPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { DataTable } from '@hypothesis/frontend-shared';
import Library from '@hypothesis/frontend-shared/lib/pattern-library/components/Library';

import GradeStatusChip from '../../frontend_apps/components/dashboard/GradeStatusChip';

export default function GradeStatusChipPage() {
return (
<Library.Page title="Grade status chip">
<Library.Section title="GradeStatusChip">
<p>
It renders a badge with an automatically calculating color
combination, based on a grade from 0 to 100.
</p>
<Library.Demo withSource>
<GradeStatusChip grade={100} />
<GradeStatusChip grade={80} />
<GradeStatusChip grade={68} />
<GradeStatusChip grade={38} />
<GradeStatusChip grade={0} />
<GradeStatusChip grade={120} />
<GradeStatusChip grade={-25} />
</Library.Demo>
</Library.Section>

<Library.Section title="GradeStatusChip in DataTable">
<p>
We plan to use the <code>GradeStatusChip</code> inside the dashboard
metrics tables. This is how it will look like.
</p>
<Library.Demo withSource>
<DataTable
grid
striped={false}
rows={[
{
name: 'Bethany VonRueden',
grade: 100,
annotations: 4,
replies: 1,
},
{
name: 'Grace Feet',
grade: 92,
annotations: 2,
replies: 1,
},
{
name: 'Hannah Rohan',
grade: 0,
annotations: 0,
replies: 0,
},
{
name: 'Jeremiah Kassuke',
grade: 68,
annotations: 1,
replies: 2,
},
{
name: 'Julio Mertz',
grade: 75,
annotations: 2,
replies: 1,
},
{
name: 'Martha Russel',
grade: 48,
annotations: 1,
replies: 0,
},
]}
columns={[
{
field: 'name',
label: 'Student',
},
{
field: 'grade',
label: 'Grade',
},
{
field: 'annotations',
label: 'Annotations',
},
{
field: 'replies',
label: 'Replies',
},
]}
title="Students"
renderItem={(row, field) => {
if (field === 'grade') {
return <GradeStatusChip grade={row.grade} />;
}

return row[field];
}}
/>
</Library.Demo>
</Library.Section>
</Library.Page>
);
}
10 changes: 9 additions & 1 deletion lms/static/scripts/ui-playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@
import { startApp } from '@hypothesis/frontend-shared/lib/pattern-library';
import type { CustomPlaygroundRoute } from '@hypothesis/frontend-shared/lib/pattern-library/routes';

import GradeStatusChipPage from './components/GradeStatusChipPage';

// LMS prototype pages should be defined here
const extraRoutes: CustomPlaygroundRoute[] = [];
const extraRoutes: CustomPlaygroundRoute[] = [
{
component: GradeStatusChipPage,
route: '/grade-status-chip',
title: 'Grade status chip',
},
];

startApp({
baseURL: '/ui-playground',
Expand Down

0 comments on commit 08c5d13

Please sign in to comment.