Skip to content

Commit

Permalink
Create reusable FieldsetLegend component (#640)
Browse files Browse the repository at this point in the history
* Create a component for fieldset legend

* Replace logic in Checkbox and Radio Groups

* Create fieldset legend storybook

* Increase minor v

* Add unit tests

* Provide default value inside bracket

* Add a note re: visually-hidden class

* Remove confusing stories

* Remove commented out code

* fix version

* Rename component + files
  • Loading branch information
jhp0621 authored May 24, 2024
1 parent 052921a commit 1cfb2c8
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 337 deletions.
616 changes: 331 additions & 285 deletions docs.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@launchpadlab/lp-components",
"version": "10.0.1",
"version": "10.1.0",
"engines": {
"node": "^18.12 || ^20.0"
},
Expand Down
4 changes: 2 additions & 2 deletions src/controls/tab-bar/tab-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import manageFocus from './focus'
* @name TabBar
* @type Function
* @description A control component for navigating among multiple tabs
* @param {Boolean} [vertical] - A boolean setting the `className` of the `ul` to 'horizontal' (default), or 'vertical', which determines the alignment of the tabs (optional, default `false`)
* @param {Boolean} [vertical=false] - A boolean setting the `className` of the `ul` to 'horizontal' (default), or 'vertical', which determines the alignment of the tabs
* @param {Array} options - An array of tab values (strings or key-value pairs)
* @param {String|Number} value - The value of the current tab
* @param {Function} [onChange] - A function called with the new value when a tab is clicked
* @param {String} [activeClassName] - The class of the active tab, (optional, default `active`)
* @param {String} [activeClassName='active'] - The class of the active tab
* @example
*
* function ShowTabs ({ currentTab, setCurrentTab }) {
Expand Down
26 changes: 2 additions & 24 deletions src/forms/inputs/checkbox-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ import {
fieldOptionsType,
omitLabelProps,
replaceEmptyStringValue,
convertNameToLabel,
DropdownSelect,
} from '../helpers'
import { LabeledField } from '../labels'
import { LabeledField, FieldsetLegend } from '../labels'
import {
addToArray,
removeFromArray,
serializeOptions,
compose,
} from '../../utils'
import classnames from 'classnames'

/**
*
Expand Down Expand Up @@ -101,26 +99,6 @@ const defaultProps = {
dropdown: false,
}

function CheckboxGroupLegend({
label,
name,
required,
requiredIndicator,
hint,
}) {
return (
<legend className={classnames({ 'visually-hidden': label === false })}>
{label || convertNameToLabel(name)}
{required && requiredIndicator && (
<span className="required-indicator" aria-hidden="true">
{requiredIndicator}
</span>
)}
{hint && <i>{hint}</i>}
</legend>
)
}

function CheckboxOptionsContainer({ children, dropdown, ...rest }) {
if (dropdown)
return (
Expand Down Expand Up @@ -159,7 +137,7 @@ function CheckboxGroup(props) {
return (
<LabeledField
className={className}
labelComponent={CheckboxGroupLegend}
labelComponent={FieldsetLegend}
as="fieldset"
{...props}
>
Expand Down
20 changes: 2 additions & 18 deletions src/forms/inputs/radio-group.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import {
convertNameToLabel,
radioGroupPropTypes,
fieldOptionsType,
omitLabelProps,
} from '../helpers'
import { LabeledField } from '../labels'
import { LabeledField, FieldsetLegend } from '../labels'
import { serializeOptions, filterInvalidDOMProps } from '../../utils'
import classnames from 'classnames'

/**
*
Expand Down Expand Up @@ -90,20 +88,6 @@ const defaultProps = {
radioInputProps: {},
}

function RadioGroupLegend({ label, name, required, requiredIndicator, hint }) {
return (
<legend className={classnames({ 'visually-hidden': label === false })}>
{label || convertNameToLabel(name)}
{required && requiredIndicator && (
<span className="required-indicator" aria-hidden="true">
{requiredIndicator}
</span>
)}
{hint && <i>{hint}</i>}
</legend>
)
}

// This should never be used by itself, so it does not exist as a separate export
function RadioButton(props) {
const {
Expand Down Expand Up @@ -146,7 +130,7 @@ function RadioGroup(props) {
return (
<LabeledField
className={className}
labelComponent={RadioGroupLegend}
labelComponent={FieldsetLegend}
as="fieldset"
{...props}
>
Expand Down
89 changes: 89 additions & 0 deletions src/forms/labels/fieldset-legend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { convertNameToLabel } from '../helpers'

/**
*
* A legend representing a caption for the content of its parent field set element
*
* This component must be used as a direct child and the only legend of the <fieldset> element that groups related controls
*
*
* The text of the legend is set using the following rules:
* - If the `label` prop is set to `false`, the legend is hidden visually via a class. _Note: It's your responsibility to make sure your styling rules respect the `visually-hidden` class_
* - Else If the `label` prop is set to a string, the label will display that text
* - Otherwise, the label will be set using the `name` prop.
*
*
* @name FieldsetLegend
* @type Function
* @param {String} name - The name of the associated group
* @param {String} [hint] - A usage hint for the associated input
* @param {String|Boolean} [label] - Custom text for the legend
* @param {Boolean} [required=false] - A boolean value to indicate whether the field is required
* @param {String} [requiredIndicator=''] - Custom character to denote a field is required
* @example
*
*
* function ShippingAddress (props) {
* const name = 'shippingAddress'
* return (
* <fieldset>
* <FieldsetLegend name={name} />
* <Input id={`${name}.name`} input={{name: 'name'}} />
* <Input id={`${name}.street`} input={{name: 'street'}} />
* <Input id={`${name}.city`}" input={{name: 'city'}} />
* <Input id={`${name}.state`} input={{name: 'state'}} />
* </fieldset>
* )
* }
*
*/

const propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
name: PropTypes.string.isRequired,
required: PropTypes.bool,
requiredIndicator: PropTypes.string,
className: PropTypes.string,
hint: PropTypes.string,
}

const defaultProps = {
children: null,
hint: '',
label: '',
required: false,
requiredIndicator: '',
className: '',
}

function FieldsetLegend({
label,
name,
required,
requiredIndicator,
className,
hint,
}) {
return (
<legend
className={classnames(className, { 'visually-hidden': label === false })}
>
{label || convertNameToLabel(name)}
{required && requiredIndicator && (
<span className="required-indicator" aria-hidden="true">
{requiredIndicator}
</span>
)}
{hint && <i>{hint}</i>}
</legend>
)
}

FieldsetLegend.propTypes = propTypes
FieldsetLegend.defaultProps = defaultProps

export default FieldsetLegend
1 change: 1 addition & 0 deletions src/forms/labels/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export ErrorLabel from './error-label'
export InputError from './input-error'
export InputLabel from './input-label'
export LabeledField from './labeled-field'
export FieldsetLegend from './fieldset-legend'
7 changes: 4 additions & 3 deletions src/forms/labels/input-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import { useToggle } from '../../utils'
* @name InputLabel
* @type Function
* @param {String} name - The name of the associated input
* @param {String} [id=name] - The id of the associated input (defaults to name)
* @param {String} [id=name] - The id of the associated input
* @param {String} [hint] - A usage hint for the associated input
* @param {String|Boolean} [label] - Custom text for the label
* @param {String} [tooltip] - A message to display in a tooltip
* @param {Boolean} [required] - A boolean value to indicate whether the field is required
* @param {String} [requiredIndicator] - Custom character to denote a field is required (optional, default `''`)
* @param {Boolean} [required=false] - A boolean value to indicate whether the field is required
* @param {String} [requiredIndicator=''] - Custom character to denote a field is required
* @example
*
Expand Down Expand Up @@ -74,6 +74,7 @@ const defaultProps = {
id: '',
label: '',
tooltip: '',
required: false,
requiredIndicator: '',
className: '',
}
Expand Down
21 changes: 21 additions & 0 deletions stories/forms/labels/fieldset-legend.story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { FieldsetLegend } from 'src'

storiesOf('FieldsetLegend', module)
.add('with default label', () => <FieldsetLegend name="nameOfInput" />)
.add('with custom label', () => (
<FieldsetLegend name="nameOfInput" label="Custom Label" />
))
.add('with no label', () => (
<FieldsetLegend name="nameOfInput" label={false} />
))
.add('with required true custom indicator', () => (
<FieldsetLegend
name="nameOfInput"
label="Custom Label"
required={true}
requiredIndicator={'*'}
/>
))
.add('with hint', () => <FieldsetLegend name="nameOfInput" hint="hint" />)
5 changes: 1 addition & 4 deletions stories/forms/labels/input-label.story.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ storiesOf('InputLabel', module)
<InputLabel name="nameOfInput" label="Custom Label" />
))
.add('with no label', () => <InputLabel name="nameOfInput" label={false} />)
.add('with required true default indicator', () => (
<InputLabel name="nameOfInput" label="Custom Label" required />
))
.add('with required true custom indicator', () => (
<InputLabel
name="nameOfInput"
label="Custom Label"
required
required={true}
requiredIndicator={'*'}
/>
))
Expand Down
45 changes: 45 additions & 0 deletions test/forms/labels/fieldset-legend.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { FieldsetLegend } from '../../../src/'

const name = 'contactDetails'
const formattedName = 'Contact Details'

describe('FieldsetLegend', () => {
test('renders label correctly when label prop is a string', () => {
render(<FieldsetLegend name={name} label="Your Information" />)
expect(screen.getByText('Your Information')).toBeInTheDocument()
})

test('renders label correctly using name prop when label prop is not provided', () => {
render(<FieldsetLegend name={name} />)
expect(screen.getByText(formattedName)).toBeInTheDocument()
})

test('renders label with class "visually-hidden" when label prop is false', () => {
render(<FieldsetLegend name={name} label={false} />)
expect(screen.getByText(formattedName)).toHaveClass('visually-hidden')
})

test('does not show required indicator when no custom required indicator is provided', () => {
render(<FieldsetLegend name={name} required />)
expect(screen.getByText(formattedName).textContent).toEqual(formattedName)
})

test('shows custom indicator when required is true and custom requiredIndicator is provided', () => {
render(<FieldsetLegend name={name} required requiredIndicator={'*'} />)
expect(screen.getByText('*')).toBeInTheDocument()
})

test('hides custom indicator when required is false and custom requiredIndicator is provided', () => {
render(
<FieldsetLegend name={name} required={false} requiredIndicator={'*'} />
)
expect(screen.queryByText('*')).not.toBeInTheDocument()
})

test('shows hint when hint is provided', () => {
render(<FieldsetLegend name={name} hint="hint" />)
expect(screen.getByText(formattedName)).toHaveTextContent('hint')
})
})

0 comments on commit 1cfb2c8

Please sign in to comment.