From 953e52ee679d5d8933271eb3991a5b4c9e5bb923 Mon Sep 17 00:00:00 2001 From: Justin Reynolds Date: Thu, 18 Jan 2018 16:47:33 -0800 Subject: [PATCH] feat(core/forms): Create a react version of the checklist component --- .../src/forms/checklist/Checklist.spec.tsx | 113 ++++++++++++++++++ .../core/src/forms/checklist/Checklist.tsx | 79 ++++++++++++ app/scripts/modules/core/src/forms/index.ts | 1 + app/scripts/modules/core/src/index.ts | 1 + 4 files changed, 194 insertions(+) create mode 100644 app/scripts/modules/core/src/forms/checklist/Checklist.spec.tsx create mode 100644 app/scripts/modules/core/src/forms/checklist/Checklist.tsx create mode 100644 app/scripts/modules/core/src/forms/index.ts diff --git a/app/scripts/modules/core/src/forms/checklist/Checklist.spec.tsx b/app/scripts/modules/core/src/forms/checklist/Checklist.spec.tsx new file mode 100644 index 00000000000..295cd90a14c --- /dev/null +++ b/app/scripts/modules/core/src/forms/checklist/Checklist.spec.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; + +import { noop } from 'core/utils'; +import { Checklist } from './Checklist'; + +describe('', () => { + it('initializes properly with provided values', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(3); + }); + + it('updates items when an item is added externally', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(3); + items.add('e'); + component.setProps({ items }); + expect(component.find('input[type="checkbox"]').length).toBe(5); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(3); + }); + + it('updates items when an item is removed externally', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(3); + items.delete('c'); + component.setProps({ items }); + expect(component.find('input[type="checkbox"]').length).toBe(3); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(2); + }); + + it('updates checked items when an item is checked externally', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(3); + checked.add('d'); + component.setProps({ checked }); + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(4); + }); + + it('updates checked items when an item is unchecked externally', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(3); + checked.delete('c'); + component.setProps({ checked }); + expect(component.find('input[type="checkbox"]').length).toBe(4); + expect(component.find('input[type="checkbox"][checked=true]').length).toBe(2); + }); + + it('shows the select all button when necessary', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + expect(component.find('a').length).toBe(1); + }); + + it('does not show the select all button when necessary', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + expect(component.find('a').length).toBe(0); + }); + + it('shows correct text for the select all button when not all the items are checked', () => { + const checked = new Set(['a', 'b', 'c']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + + expect(component.find('a').text()).toBe('Select All'); + }); + + it('shows correct text for the select all button when all the items are checked', () => { + const checked = new Set(['a', 'b', 'c', 'd']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const component = shallow(); + + expect(component.find('a').text()).toBe('Deselect All'); + }); + + it('passes an empty list to the onChange handler when deselect all clicked', () => { + const checked = new Set(['a', 'b', 'c', 'd']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const onChange = (i: Set): void => { expect(i.size).toBe(0); }; + const component = shallow(); + component.find('a').simulate('click'); + }); + + it('passes a complete list to the onChange handler when select all clicked', () => { + const checked = new Set(['a']); + const items = new Set([ 'a', 'b', 'c', 'd' ]); + const onChange = (i: Set): void => { expect(i.size).toBe(4); }; + const component = shallow(); + component.find('a').simulate('click'); + }); +}); diff --git a/app/scripts/modules/core/src/forms/checklist/Checklist.tsx b/app/scripts/modules/core/src/forms/checklist/Checklist.tsx new file mode 100644 index 00000000000..81975fa020c --- /dev/null +++ b/app/scripts/modules/core/src/forms/checklist/Checklist.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { xor } from 'lodash'; +import { BindAll } from 'lodash-decorators'; + +export interface IChecklistProps { + includeSelectAllButton?: boolean; + inline?: boolean; + items: Set; + checked: Set; + onChange: (checked: Set) => void; +} + +export interface IChecklistState { +} + +@BindAll() +export class Checklist extends React.Component { + constructor(props: IChecklistProps) { + super(props); + } + + private handleChecked(event: React.ChangeEvent): void { + const name = event.target.name; + const isChecked = event.target.checked; + const checked = new Set(this.props.checked); + + isChecked ? checked.add(name) : checked.delete(name); + this.props.onChange(checked); + } + + public render(): React.ReactElement { + const { checked, includeSelectAllButton, inline, items, onChange } = this.props; + + const showSelectAll = includeSelectAllButton && items.size > 1; + const allSelected = xor([...items], [...checked]).length === 0; + const selectAllLabel = allSelected ? 'Deselect All' : 'Select All'; + const handleSelectAll = () => allSelected ? onChange(new Set()) : onChange(new Set(items)); + + if (!inline) { + return ( +
    + {[...items].map((item) => ( +
  • + +
  • + ))} + {showSelectAll && ( +
  • + {selectAllLabel} +
  • + )} +
+ ); + } + + return ( + + {[...items].map((item) => ( + + ))} + {showSelectAll && ( + + {selectAllLabel} + + )} + + ); + } +} diff --git a/app/scripts/modules/core/src/forms/index.ts b/app/scripts/modules/core/src/forms/index.ts new file mode 100644 index 00000000000..b83b862bcdb --- /dev/null +++ b/app/scripts/modules/core/src/forms/index.ts @@ -0,0 +1 @@ +export * from './checklist/Checklist'; diff --git a/app/scripts/modules/core/src/index.ts b/app/scripts/modules/core/src/index.ts index 88e4d8c44f7..3705abe6a02 100644 --- a/app/scripts/modules/core/src/index.ts +++ b/app/scripts/modules/core/src/index.ts @@ -25,6 +25,7 @@ export * from './artifact'; export * from './event/EventBus'; export * from './filterModel'; +export * from './forms'; export * from './healthCounts'; export * from './help';