diff --git a/src/components/input/OptionButton.tsx b/src/components/input/OptionButton.tsx new file mode 100644 index 00000000..6c887946 --- /dev/null +++ b/src/components/input/OptionButton.tsx @@ -0,0 +1,76 @@ +import classnames from 'classnames'; +import type { ComponentChildren } from 'preact'; + +import { CheckIcon } from '../icons/'; +import Button from './Button'; +import type { ButtonProps } from './Button'; + +export type OptionButtonProps = { + /** Optional content to render at the right side of the button */ + details?: ComponentChildren; + /** alias for `pressed`: this option button is selected **/ + selected?: boolean; +} & Omit; + +/** + * Render a button representing one of a set of options, with optional + * right-aligned `details` content + */ +const OptionButton = function OptionButton({ + children, + details, + selected = false, + + pressed, + ...buttonProps +}: OptionButtonProps) { + const isPressed = selected || pressed; + return ( + + ); +}; + +export default OptionButton; diff --git a/src/components/input/index.ts b/src/components/input/index.ts index 36a53a28..b31b55cc 100644 --- a/src/components/input/index.ts +++ b/src/components/input/index.ts @@ -4,6 +4,7 @@ export { default as Checkbox } from './Checkbox'; export { default as IconButton } from './IconButton'; export { default as Input } from './Input'; export { default as InputGroup } from './InputGroup'; +export { default as OptionButton } from './OptionButton'; export { default as Select } from './Select'; export type { ButtonProps } from './Button'; @@ -12,4 +13,5 @@ export type { CheckboxProps } from './Checkbox'; export type { IconButtonProps } from './IconButton'; export type { InputProps } from './Input'; export type { InputGroupProps } from './InputGroup'; +export type { OptionButtonProps } from './OptionButton'; export type { SelectProps } from './Select'; diff --git a/src/components/input/test/OptionButton-test.js b/src/components/input/test/OptionButton-test.js new file mode 100644 index 00000000..a2dbd41f --- /dev/null +++ b/src/components/input/test/OptionButton-test.js @@ -0,0 +1,42 @@ +import { mount } from 'enzyme'; + +import { testSimpleComponent } from '../../test/common-tests'; +import OptionButton from '../OptionButton'; + +describe('OptionButton', () => { + const createComponent = (props = {}) => { + return mount( + This is content inside of a Button + ); + }; + + testSimpleComponent(OptionButton); + + it('applies appropriate ARIA attributes for button state', () => { + const pressed = createComponent({ pressed: true }); + const selected = createComponent({ selected: true }); + + assert.equal( + pressed.find('button').getDOMNode().getAttribute('aria-pressed'), + 'true' + ); + assert.equal( + selected.find('button').getDOMNode().getAttribute('aria-pressed'), + 'true' + ); + }); + + it('renders optional details content', () => { + const noDetails = createComponent(); + const withDetails = createComponent({ details: 'PDF' }); + + function getDetails(wrapper) { + return wrapper.find('[data-testid="option-button-details"]'); + } + + assert.isNotOk(getDetails(noDetails).exists()); + const details = getDetails(withDetails); + assert.isOk(details); + assert.equal(details.text(), 'PDF'); + }); +}); diff --git a/src/index.ts b/src/index.ts index 90302ec5..5abe7014 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ export { IconButton, Input, InputGroup, + OptionButton, Select, } from './components/input'; export { @@ -97,6 +98,7 @@ export type { IconButtonProps, InputProps, InputGroupProps, + OptionButtonProps, SelectProps, } from './components/input'; diff --git a/src/pattern-library/components/patterns/input/OptionButtonPage.tsx b/src/pattern-library/components/patterns/input/OptionButtonPage.tsx new file mode 100644 index 00000000..0a314786 --- /dev/null +++ b/src/pattern-library/components/patterns/input/OptionButtonPage.tsx @@ -0,0 +1,145 @@ +import { OptionButton } from '../../../../'; +import Library from '../../Library'; + +export default function OptionButtonPage() { + return ( + + OptionButton is a simple component for presenting an + option as part of a set of options. It can be used in places where it + is impractical or undesirable to use an equivalent{' '} + {`input type="radio"`} or select element.{' '} + OptionButton wraps{' '} + + Button + + . +

+ } + > + + + + + +
+ Option Alpha +
+
+
+
+ + + +

+ OptionButton is designed for use in a set, similar to + an option element or a radio button. While not + enforced, a maximum of one OptionButton in a set + should be selected. There is also disabled + styling. +

+

+ To facilitate alignment, OptionButton stretches to + the full width of its container. +

+ +
+ Option Alpha + Option Bravo + Option Charlie-Delta-Echo + Option Foxtrot + Option Golf +
+
+
+
+ + + + + + Optional content to display at right side of button. Be sure to + set a useful title (used for generating{' '} + aria-label) when constructing such buttons. + + + preact.ComponentChildren + + + + +
+ + Google Drive + +
+
+
+ + + + + The option represented by the button is selected. A maximum of + one OptionButton in a set should be selected at any + time. This is an alias for the Button{' '} + pressed prop. + + + boolean + + + false + + + + +
+ + Google Drive + +
+
+
+ + + + + OptionButton accepts and forwards all{' '} + + Button + {' '} + component API props. Styling API props are not forwarded. + + + All Button props except styling API props: +
    +
  • + classes +
  • +
  • + unstyled +
  • +
  • + variant +
  • +
  • + size +
  • +
+
+
+
+
+
+
+ ); +} diff --git a/src/pattern-library/components/patterns/prototype/LMSContentButtonPage.tsx b/src/pattern-library/components/patterns/prototype/LMSContentButtonPage.tsx index 99dc6f35..c91f77a8 100644 --- a/src/pattern-library/components/patterns/prototype/LMSContentButtonPage.tsx +++ b/src/pattern-library/components/patterns/prototype/LMSContentButtonPage.tsx @@ -1,62 +1,8 @@ -import classnames from 'classnames'; import type { ComponentChildren } from 'preact'; -import { Button, Card, CardHeader, CardContent, CheckIcon } from '../../../../'; -import type { ButtonProps } from '../../../../'; +import { Card, CardHeader, CardContent, OptionButton } from '../../../../'; import Library from '../../Library'; -type ContentButtonProps = { contentType: string } & ButtonProps; - -function ContentButton({ - children, - classes, - contentType = 'pdf', - pressed, - ...buttonProps -}: ContentButtonProps) { - return ( - - ); -} - /** * A relatively simplified layout representation of the LMS content-selection * panel @@ -86,15 +32,20 @@ function ContentPanel({ children }: { children: ComponentChildren }) { ); } -export default function LMSContentButtonPage() { +export default function LMSOptionButtonPage() { return ( - The proposed LMS ContentButton encapsulates a new button - design pattern for the assignment content-configuration screen in LMS. - It is based on a selected design approach in the{' '} + The new{' '} + + OptionButton + {' '} + encapsulates a new button design pattern for selecting an option from + a list of options. The assignment content-configuration screen in LMS + will use this new button. It is based on a selected design approach in + the{' '} set of available sketches @@ -103,94 +54,56 @@ export default function LMSContentButtonPage() { } > - - - -
-
- Google Drive -
-
-
- - -
-
- - Google Drive - -
-
-
- - -
-
- - Google Drive - -
-
-
-
- - - -
- - - URL - - Canvas - Google Drive - JSTOR - OneDrive - VitalSource - YouTube - -
-
+ + +
+ + URL + Canvas + Google Drive + JSTOR + OneDrive + VitalSource + YouTube + +
+
- - NB: There is currently no use of disabled or{' '} - pressed states in the LMS content-selection - interface. But the new button provides styling for those states. - + + NB: There is currently no use of disabled or{' '} + pressed states in the LMS content-selection interface. + But the new button provides styling for those states. + - -
- - - URL - - Canvas - - Google Drive - - JSTOR - OneDrive - VitalSource - YouTube - -
-
+ +
+ + URL + Canvas + + Google Drive + + JSTOR + OneDrive + VitalSource + YouTube + +
+
- -
- - - URL - - Canvas - Google Drive - OneDrive - - VitalSource - - -
-
-
+ +
+ + URL + Canvas + Google Drive + OneDrive + + VitalSource + + +
+
diff --git a/src/pattern-library/routes.ts b/src/pattern-library/routes.ts index 75e10575..51cc7de3 100644 --- a/src/pattern-library/routes.ts +++ b/src/pattern-library/routes.ts @@ -18,6 +18,7 @@ import ButtonsPage from './components/patterns/input/ButtonPage'; import CheckboxPage from './components/patterns/input/CheckboxPage'; import InputGroupPage from './components/patterns/input/InputGroupPage'; import InputPage from './components/patterns/input/InputPage'; +import OptionButtonPage from './components/patterns/input/OptionButtonPage'; import SelectPage from './components/patterns/input/SelectPage'; import CardPage from './components/patterns/layout/CardPage'; import OverlayPage from './components/patterns/layout/OverlayPage'; @@ -172,6 +173,12 @@ const routes: PlaygroundRoute[] = [ component: InputGroupPage, route: '/input-input-group', }, + { + title: 'OptionButton', + group: 'input', + component: OptionButtonPage, + route: '/input-option-button', + }, { title: 'Select', group: 'input',