Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Create Radio Button Component #586

Merged
merged 9 commits into from
Dec 3, 2024
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@department-of-veterans-affairs/mobile-component-library",
"version": "0.27.1",
"version": "0.27.2-alpha.1",
"description": "VA Design System Mobile Component Library",
"main": "src/index.tsx",
"scripts": {
Expand Down
152 changes: 17 additions & 135 deletions packages/components/src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,10 @@
import {
Pressable,
StyleProp,
View,
ViewStyle,
useWindowDimensions,
} from 'react-native'
import { spacing } from '@department-of-veterans-affairs/mobile-tokens'
import { useTranslation } from 'react-i18next'
import React, { FC } from 'react'

import { CheckboxRadioProps, FormElementProps } from '../../types/forms'
import { CheckboxRadio } from '../shared/CheckboxRadio'
import { CheckboxRadioProps } from '../../types/forms'
import { ComponentWrapper } from '../../wrapper'
import {
Description,
Error,
Header,
Hint,
Label,
fontLabel,
} from '../shared/FormText'
import { Icon, IconProps } from '../Icon/Icon'
import { Spacer } from '../Spacer/Spacer'
import { getA11yLabel, useTheme } from '../../utils'

export type CheckboxProps = FormElementProps &
CheckboxRadioProps & {
/** True to make checkbox appear as checked */
checked?: boolean
/** True to apply indeterminate icon to checkbox */
indeterminate?: boolean
}

export const Checkbox: FC<CheckboxProps> = ({
export const Checkbox: FC<CheckboxRadioProps> = ({
a11yListPosition,
checked,
label,
Expand All @@ -45,115 +18,24 @@ export const Checkbox: FC<CheckboxProps> = ({
testID,
tile,
}) => {
const theme = useTheme()
const { t } = useTranslation()
const fontScale = useWindowDimensions().fontScale

/**
* Container styling
*/
let containerStyle: ViewStyle = {
width: '100%',
}

if (error) {
containerStyle = {
...containerStyle,
borderLeftWidth: spacing.vadsSpace2xs,
borderColor: theme.vadsColorFormsBorderError,
paddingLeft: spacing.vadsSpaceMd,
}
const props = {
a11yListPosition,
checked,
description,
error,
header,
hint,
indeterminate,
label,
onPress,
required,
testID,
tile,
}

/**
* Pressable styling
*/
const pressableBaseStyle: StyleProp<ViewStyle> = {
width: '100%',
flexDirection: 'row',
alignItems: 'flex-start',
}

const tileStyle: ViewStyle = {
...pressableBaseStyle,
borderWidth: 2,
borderRadius: 4,
padding: spacing.vadsSpaceSm,
paddingRight: spacing.vadsSpaceMd,
borderColor: checked
? theme.vadsColorFormsBorderActive
: theme.vadsColorFormsBorderSubtle,
backgroundColor: checked
? theme.vadsColorFormsSurfaceActive
: theme.vadsColorSurfaceDefault,
}

/**
* Icon
*/
const iconViewStyle: ViewStyle = {
// Below keeps icon aligned with first row of text, centered, and scalable
alignSelf: 'flex-start',
// TODO: Replace lineHeight with typography token
minHeight: fontLabel.lineHeight * fontScale,
alignItems: 'center',
justifyContent: 'center',
}

const iconProps: IconProps = {
name: indeterminate
? 'IndeterminateCheckBox'
: checked
? 'CheckBox'
: 'CheckBoxOutlineBlank',
fill:
checked || indeterminate
? theme.vadsColorFormsForegroundActive
: theme.vadsColorFormsBorderDefault,
}

const _icon = (
<View style={iconViewStyle}>
<Icon {...iconProps} />
</View>
)

/**
* Combined a11yLabel on Pressable required for Android Talkback
*/
const a11yLabel =
getA11yLabel(label) +
(required ? ', ' + t('required') : '') +
(description ? `, ${getA11yLabel(description)}` : '')

return (
<ComponentWrapper>
<View style={containerStyle} testID={testID}>
<Header text={header} />
{header && <Spacer size="xs" />}

<Hint text={hint} />
{hint && <Spacer size="xs" />}

<Error text={error} />
{error && <Spacer size="xs" />}

<Pressable
onPress={onPress}
style={tile ? tileStyle : pressableBaseStyle}
aria-checked={indeterminate ? 'mixed' : checked}
aria-valuetext={a11yListPosition}
aria-label={a11yLabel}
role="checkbox">
{_icon}
<Spacer size="xs" horizontal />
<View style={{ flexShrink: 1 }}>
<Label text={label} error={error} required={required} />
{description && <Spacer size="xs" />}
<Description text={description} />
</View>
</Pressable>
</View>
<CheckboxRadio {...props} />
</ComponentWrapper>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type CheckboxGroupProps = FormElementProps & {
/**
* ### Managing checked item state
* The state of the selected checkbox items should be provided to CheckboxGroup via the `selectedItems` prop and updated
* using the `onSelectionChange` callback. When a checkbox is tapped, the provided `onSelectionChange` callback
* using the `onSelectionChange` callback. When a checkbox is tapped, the provided `onSelectionChange` callback
* function is fired and passed an array of the newly `selectedItems`, which can be used to update the parent
* component's state, whether that be redux, zustand, useState, or any other state management methods. Here is a basic
* example using the `useState` hook to store the state of the `selectedItems`:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Meta, StoryObj } from '@storybook/react'
import { View } from 'react-native'
import React, { useState } from 'react'

import { RadioButton, RadioButtonProps } from './RadioButton'
import { generateDocs } from '../../utils/storybook'

const meta: Meta<RadioButtonProps> = {
title: 'Radio button',
component: RadioButton,
decorators: [
(Story) => (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
margin: 8,
}}>
{Story()}
</View>
),
],
parameters: {
docs: generateDocs({
name: 'Radio button',
docUrl:
'https://department-of-veterans-affairs.github.io/va-mobile-app/design/Components/Selection%20and%20Input/RadioButton/',
}),
},
}

export default meta

type Story = StoryObj<RadioButtonProps>

const statefulComponentRenderer = (props: RadioButtonProps) => {
const { error, header, hint, items, required, tile } = props

const [selectedItem, setSelectedItem] = useState<string | number>()

return (
<RadioButton
items={items}
selectedItem={selectedItem}
error={error}
header={header}
hint={hint}
onSelectionChange={(selected) => setSelectedItem(selected)}
required={required}
tile={tile}
/>
)
}

const items = [
{ text: 'Option 1', description: 'Description for option 1' },
{
text: 'Option 2',
a11yLabel: 'Accessibility override for option 2',
value: '2',
description: {
text: 'Description for option 2',
a11yLabel: 'Accessibility override for description',
},
},
{ text: 'Option 3' },
{ text: 'Option 4' },
{ text: 'Option 5' },
{ text: 'Option 6' },
]

const simpleItems = ['Option 1', 'Option 2', 'Option 3', 'Option 4']

const header = 'Header'
const hint = { text: 'Hint text', a11yLabel: 'Accessibility override for hint' }
const error = { text: 'Error text' }

export const _Default: Story = {
render: statefulComponentRenderer,
args: {
header,
hint,
items,
required: true,
},
}

export const __Tile: Story = {
render: statefulComponentRenderer,
args: {
header,
hint,
items: simpleItems,
tile: true,
},
}

export const ___Error: Story = {
render: statefulComponentRenderer,
args: {
error,
header,
hint,
items,
required: true,
},
}
Loading
Loading