Skip to content

Commit

Permalink
Create new chip components to represent status and grade status
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Sep 5, 2024
1 parent ffc2af3 commit 2b72bcd
Show file tree
Hide file tree
Showing 7 changed files with 291 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',
})
)
}),
),
);
35 changes: 35 additions & 0 deletions lms/static/scripts/frontend_apps/components/StatusChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import classnames from 'classnames';
import type { ComponentChildren } from 'preact';

export type StatusChipProps = {
/**
* Determines the aspect of the status chip, being "top" the one representing
* the most successful status, and "bottom" the one representing a total
* failure.
*/
variant: 'success' | 'semi-success' | 'warning' | 'error' | 'fatal';

children?: ComponentChildren;
};

/**
* A status chip (AKA label or tag) that visually represents the success level
* on a scale of 1 to 5
*/
export default function StatusChip({ variant, children }: StatusChipProps) {
return (
<div
className={classnames('rounded font-bold inline-block px-2 py-0.5', {
'bg-green-success text-white': variant === 'success',
// 'bg-green-700 text-white': variant === 'success',
'bg-green-200 text-green-900': variant === 'semi-success',
'bg-amber-100 text-amber-900': variant === 'warning',
'bg-red-200 text-red-900': variant === 'error',
'bg-red-error text-white': variant === 'fatal',
// 'bg-red-600 text-white': variant === 'fatal',
})}
>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMemo } from 'preact/hooks';

import type { StatusChipProps } from '../StatusChip';
import StatusChip from '../StatusChip';

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

/**
* A StatusChip where the corresponding variant is calculated from a grade
* from 0 to 100, following the next table:
*
* 100 - success
* 80-99 - semi-success
* 50-79 - warning
* 1-49 - error
* 0 - fatal
*
* See {@link StatusChip}
*/
export default function GradeStatusChip({ grade }: GradeStatusChipProps) {
const variant = useMemo((): StatusChipProps['variant'] => {
if (grade === 100) {
return 'success';
} else if (grade >= 80 && grade < 100) {
return 'semi-success';
} else if (grade >= 50 && grade < 80) {
return 'warning';
} else if (grade >= 1 && grade < 50) {
return 'error';
}

return 'fatal';
}, [grade]);

return <StatusChip variant={variant}>{grade}%</StatusChip>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 grade as percentage', () => {
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),
},
]),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { checkAccessibility } from '@hypothesis/frontend-testing';
import { mount } from 'enzyme';

import StatusChip from '../StatusChip';

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

// This is a presentational component. We just need to test that the color
// combinations for all variants are accessible

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

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

export default function GradeStatusChipPage() {
return (
<Library.Page title="Grade status chip">
<Library.Section title="Standard StatusChip">
<p>
It can render any kind of content. The color can be customized via{' '}
<code>variant</code> prop.
</p>
<Library.Demo withSource>
<StatusChip variant="success">Success</StatusChip>
<StatusChip variant="semi-success">Semi-success</StatusChip>
<StatusChip variant="warning">Warning</StatusChip>
<StatusChip variant="error">Error</StatusChip>
<StatusChip variant="fatal">Fatal</StatusChip>
</Library.Demo>
</Library.Section>

<Library.Section title="GradeStatusChip">
<p>
It renders a <code>StatusChip</code> by automatically calculating the
right variant, 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} />
</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 2b72bcd

Please sign in to comment.