diff --git a/.dojorc b/.dojorc index b96e499662..c83ec8a811 100644 --- a/.dojorc +++ b/.dojorc @@ -30,6 +30,7 @@ "src/popup", "src/progress", "src/radio", + "src/radio-group", "src/raised-button", "src/range-slider", "src/select", diff --git a/src/examples/src/config.tsx b/src/examples/src/config.tsx index b5c76a2275..55178ebc62 100644 --- a/src/examples/src/config.tsx +++ b/src/examples/src/config.tsx @@ -75,6 +75,10 @@ import ProgressWithCustomOutput from './widgets/progress/ProgressWithCustomOutpu import ProgressWithMax from './widgets/progress/ProgressWithMax'; import ProgressWithoutOutput from './widgets/progress/ProgressWithoutOutput'; import BasicRadio from './widgets/radio/Basic'; +import BasicRadioGroup from './widgets/radio-group/Basic'; +import CustomLabelRadioGroup from './widgets/radio-group/CustomLabel'; +import CustomRendererRadioGroup from './widgets/radio-group/CustomRenderer'; +import InitialValueRadioGroup from './widgets/radio-group/InitialValue'; import BasicRaisedButton from './widgets/raised-button/Basic'; import RaisedDisabledSubmit from './widgets/raised-button/DisabledSubmit'; import RaisedToggleButton from './widgets/raised-button/ToggleButton'; @@ -620,6 +624,32 @@ export const config = { } } }, + 'radio-group': { + examples: [ + { + filename: 'InitialValue', + module: InitialValueRadioGroup, + title: 'Initial Value' + }, + { + filename: 'CustomLabel', + module: CustomLabelRadioGroup, + title: 'Custom Label' + }, + { + filename: 'CustomRenderer', + module: CustomRendererRadioGroup, + title: 'Custom Renderer' + } + ], + filename: 'index', + overview: { + example: { + filename: 'Basic', + module: BasicRadioGroup + } + } + }, 'raised-button': { examples: [ { diff --git a/src/examples/src/widgets/radio-group/Basic.tsx b/src/examples/src/widgets/radio-group/Basic.tsx new file mode 100644 index 0000000000..8a3fed4c83 --- /dev/null +++ b/src/examples/src/widgets/radio-group/Basic.tsx @@ -0,0 +1,25 @@ +import RadioGroup from '@dojo/widgets/radio-group'; +import { create, tsx } from '@dojo/framework/core/vdom'; +import { icache } from '@dojo/framework/core/middleware/icache'; + +const factory = create({ icache }); + +const App = factory(function({ properties, middleware: { icache } }) { + const { get, set } = icache; + + return ( + + { + set('standard', value); + }} + /> +
{`${get('standard')}`}
+
+ ); +}); + +export default App; diff --git a/src/examples/src/widgets/radio-group/CustomLabel.tsx b/src/examples/src/widgets/radio-group/CustomLabel.tsx new file mode 100644 index 0000000000..1099846017 --- /dev/null +++ b/src/examples/src/widgets/radio-group/CustomLabel.tsx @@ -0,0 +1,29 @@ +import RadioGroup from '@dojo/widgets/radio-group'; +import { create, tsx } from '@dojo/framework/core/vdom'; +import { icache } from '@dojo/framework/core/middleware/icache'; + +const factory = create({ icache }); + +const App = factory(function({ properties, middleware: { icache } }) { + const { get, set } = icache; + + return ( + + { + set('colours', value); + }} + /> +
{`${get('colours')}`}
+
+ ); +}); + +export default App; diff --git a/src/examples/src/widgets/radio-group/CustomRenderer.tsx b/src/examples/src/widgets/radio-group/CustomRenderer.tsx new file mode 100644 index 0000000000..c9866a8ab3 --- /dev/null +++ b/src/examples/src/widgets/radio-group/CustomRenderer.tsx @@ -0,0 +1,44 @@ +import RadioGroup from '@dojo/widgets/radio-group'; +import { Radio } from '@dojo/widgets/radio'; +import { create, tsx } from '@dojo/framework/core/vdom'; +import { icache } from '@dojo/framework/core/middleware/icache'; + +const factory = create({ icache }); + +const App = factory(function({ properties, middleware: { icache } }) { + const { get, set } = icache; + + return ( + + { + set('custom', value); + }} + renderer={(name, radioGroup, options) => { + return options.map(({ value, label }) => { + const { checked } = radioGroup(value); + return ( + + I'm custom! + +
+
+ ); + }); + }} + /> +
{`${get('custom')}`}
+
+ ); +}); + +export default App; diff --git a/src/examples/src/widgets/radio-group/InitialValue.tsx b/src/examples/src/widgets/radio-group/InitialValue.tsx new file mode 100644 index 0000000000..98534271a6 --- /dev/null +++ b/src/examples/src/widgets/radio-group/InitialValue.tsx @@ -0,0 +1,26 @@ +import RadioGroup from '@dojo/widgets/radio-group'; +import { create, tsx } from '@dojo/framework/core/vdom'; +import { icache } from '@dojo/framework/core/middleware/icache'; + +const factory = create({ icache }); + +const App = factory(function({ properties, middleware: { icache } }) { + const { get, set } = icache; + + return ( + + { + set('initial-value', value); + }} + /> +
{`${get('initial-value')}`}
+
+ ); +}); + +export default App; diff --git a/src/radio-group/README.md b/src/radio-group/README.md new file mode 100644 index 0000000000..fd3d14c9bc --- /dev/null +++ b/src/radio-group/README.md @@ -0,0 +1,9 @@ +# @dojo/widgets/radio-group widget + +Dojo's `RadioGroup` widget provides an opinionated way to use a group of check boxes in a form. + +## Features + +- Takes an options property to define the radios to create +- Offers a custom renderer allowing the user to create their own radios +- Provides a middleware for custom use diff --git a/src/radio-group/index.tsx b/src/radio-group/index.tsx new file mode 100644 index 0000000000..5308a04ad2 --- /dev/null +++ b/src/radio-group/index.tsx @@ -0,0 +1,62 @@ +import * as css from '../theme/radio-group.m.css'; +import theme from '@dojo/framework/core/middleware/theme'; +import { Radio } from '../radio/index'; +import { RenderResult } from '@dojo/framework/core/interfaces'; +import { create, tsx } from '@dojo/framework/core/vdom'; +import { radioGroup } from './middleware'; + +type RadioOptions = { value: string; label?: string }[]; + +interface RadioGroupProperties { + /** Initial value of the radio group */ + initialValue?: string; + /** The label to be displayed in the legend */ + label?: string; + /** The name attribute for this form group */ + name: string; + /** Callback for the current value */ + onValue(value: string): void; + /** Object containing the values / labels to create radios for */ + options: RadioOptions; + /** Custom renderer for the radios, receives the radio group middleware and options */ + renderer?( + name: string, + middleware: ReturnType['api']>, + options: RadioOptions + ): RenderResult; +} + +const factory = create({ radioGroup, theme }).properties(); + +export const RadioGroup = factory(function({ properties, middleware: { radioGroup, theme } }) { + const { name, label, options, renderer, onValue, initialValue } = properties(); + const radio = radioGroup(onValue, initialValue || ''); + const { root, legend } = theme.classes(css); + + function renderRadios() { + if (renderer) { + return renderer(name, radio, options); + } + return options.map(({ value, label }) => { + const { checked } = radio(value); + return ( + + ); + }); + } + + return ( +
+ {label && {label}} + {renderRadios()} +
+ ); +}); + +export default RadioGroup; diff --git a/src/radio-group/middleware.ts b/src/radio-group/middleware.ts new file mode 100644 index 0000000000..266c7c2faf --- /dev/null +++ b/src/radio-group/middleware.ts @@ -0,0 +1,34 @@ +import { create } from '@dojo/framework/core/vdom'; +import { createICacheMiddleware } from '@dojo/framework/core/middleware/icache'; + +interface RadioGroupICache { + initial: string; + value: string; +} + +const icache = createICacheMiddleware(); +const factory = create({ icache }); + +export const radioGroup = factory(({ middleware: { icache } }) => { + return (onValue: (value: string) => void, initialValue: string) => { + const existingInitialValue = icache.get('initial'); + + if (existingInitialValue !== initialValue) { + icache.set('value', initialValue); + icache.set('initial', initialValue); + } + + return (key: string) => ({ + checked(checked?: boolean) { + const existingValue = icache.get('value'); + + if (!checked && existingValue === key) { + return existingValue === key && true; + } else if (checked && existingValue !== key) { + icache.set('value', key); + onValue(key); + } + } + }); + }; +}); diff --git a/src/radio-group/tests/RadioGroup.spec.tsx b/src/radio-group/tests/RadioGroup.spec.tsx new file mode 100644 index 0000000000..e474fb2313 --- /dev/null +++ b/src/radio-group/tests/RadioGroup.spec.tsx @@ -0,0 +1,95 @@ +const { describe, it } = intern.getInterface('bdd'); +import * as css from '../../theme/radio-group.m.css'; +import Radio from '../../radio/index'; +import RadioGroup from '../index'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import harness from '@dojo/framework/testing/harness'; +import { tsx } from '@dojo/framework/core/vdom'; + +function noop() {} + +describe('RadioGroup', () => { + const template = assertionTemplate(() => ( +
+ )); + + it('renders with options', () => { + const h = harness(() => ( + + )); + const optionTemplate = template.setChildren('@root', () => [ + , + , + + ]); + h.expect(optionTemplate); + }); + + it('renders with a label', () => { + const h = harness(() => ( + + )); + const labelTemplate = template.setChildren('@root', () => [ + test label, + + ]); + h.expect(labelTemplate); + }); + + it('renders with initial value', () => { + const h = harness(() => ( + + )); + const optionTemplate = template.setChildren('@root', () => [ + , + , + + ]); + h.expect(optionTemplate); + }); + + it('renders with custom renderer', () => { + const h = harness(() => ( + { + return [ + custom label, + , +
+ ]; + }} + /> + )); + const customTemplate = template.setChildren('@root', () => [ + custom render label, + custom label, + , +
+ ]); + h.expect(customTemplate); + }); +}); diff --git a/src/radio-group/tests/middleware.spec.ts b/src/radio-group/tests/middleware.spec.ts new file mode 100644 index 0000000000..9e4f65ba11 --- /dev/null +++ b/src/radio-group/tests/middleware.spec.ts @@ -0,0 +1,55 @@ +const { assert } = intern.getPlugin('chai'); +const { describe, it, afterEach } = intern.getInterface('bdd'); +import cacheMiddleware from '@dojo/framework/core/middleware/cache'; +import icacheMiddleware from '@dojo/framework/core/middleware/icache'; +import { radioGroup as radioGroupMiddleware } from '../middleware'; +import { sandbox } from 'sinon'; + +const sb = sandbox.create(); +const onValueStub = sb.stub(); +const { callback } = radioGroupMiddleware(); + +function cacheFactory() { + return cacheMiddleware().callback({ + id: 'test-cache', + properties: () => ({}), + children: () => [], + middleware: { destroy: sb.stub() } + }); +} + +function icacheFactory() { + return icacheMiddleware().callback({ + id: 'test-cache', + properties: () => ({}), + children: () => [], + middleware: { cache: cacheFactory(), invalidator: sb.stub() } + }); +} + +describe('RadioGroup-middleware', () => { + afterEach(() => { + sb.resetHistory(); + }); + + it('converts checked values to arrays', () => { + const radioGroup = callback({ + id: 'radiogroup-test', + middleware: { icache: icacheFactory() }, + properties: () => ({}), + children: () => [] + })(onValueStub, 'test'); + + const test1Api = radioGroup('test1'); + const test2Api = radioGroup('test2'); + + assert.isUndefined(test1Api.checked()); + test1Api.checked(true); + assert.isTrue(test1Api.checked()); + assert.isTrue(onValueStub.calledWith('test1')); + test2Api.checked(true); + assert.isTrue(onValueStub.calledWith('test2')); + test1Api.checked(false); + assert.isUndefined(test1Api.checked()); + }); +}); diff --git a/src/theme/radio-group.m.css b/src/theme/radio-group.m.css new file mode 100644 index 0000000000..517c8e6690 --- /dev/null +++ b/src/theme/radio-group.m.css @@ -0,0 +1,3 @@ +.root { } + +.legend { } diff --git a/src/theme/radio-group.m.css.d.ts b/src/theme/radio-group.m.css.d.ts new file mode 100644 index 0000000000..30c78d6d83 --- /dev/null +++ b/src/theme/radio-group.m.css.d.ts @@ -0,0 +1,2 @@ +export const root: string; +export const legend: string;