Skip to content

Commit 1cfb2c8

Browse files
authored
Create reusable FieldsetLegend component (#640)
* 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
1 parent 052921a commit 1cfb2c8

File tree

11 files changed

+499
-337
lines changed

11 files changed

+499
-337
lines changed

docs.md

+331-285
Large diffs are not rendered by default.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@launchpadlab/lp-components",
3-
"version": "10.0.1",
3+
"version": "10.1.0",
44
"engines": {
55
"node": "^18.12 || ^20.0"
66
},

src/controls/tab-bar/tab-bar.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import manageFocus from './focus'
99
* @name TabBar
1010
* @type Function
1111
* @description A control component for navigating among multiple tabs
12-
* @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`)
12+
* @param {Boolean} [vertical=false] - A boolean setting the `className` of the `ul` to 'horizontal' (default), or 'vertical', which determines the alignment of the tabs
1313
* @param {Array} options - An array of tab values (strings or key-value pairs)
1414
* @param {String|Number} value - The value of the current tab
1515
* @param {Function} [onChange] - A function called with the new value when a tab is clicked
16-
* @param {String} [activeClassName] - The class of the active tab, (optional, default `active`)
16+
* @param {String} [activeClassName='active'] - The class of the active tab
1717
* @example
1818
*
1919
* function ShowTabs ({ currentTab, setCurrentTab }) {

src/forms/inputs/checkbox-group.js

+2-24
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,15 @@ import {
66
fieldOptionsType,
77
omitLabelProps,
88
replaceEmptyStringValue,
9-
convertNameToLabel,
109
DropdownSelect,
1110
} from '../helpers'
12-
import { LabeledField } from '../labels'
11+
import { LabeledField, FieldsetLegend } from '../labels'
1312
import {
1413
addToArray,
1514
removeFromArray,
1615
serializeOptions,
1716
compose,
1817
} from '../../utils'
19-
import classnames from 'classnames'
2018

2119
/**
2220
*
@@ -101,26 +99,6 @@ const defaultProps = {
10199
dropdown: false,
102100
}
103101

104-
function CheckboxGroupLegend({
105-
label,
106-
name,
107-
required,
108-
requiredIndicator,
109-
hint,
110-
}) {
111-
return (
112-
<legend className={classnames({ 'visually-hidden': label === false })}>
113-
{label || convertNameToLabel(name)}
114-
{required && requiredIndicator && (
115-
<span className="required-indicator" aria-hidden="true">
116-
{requiredIndicator}
117-
</span>
118-
)}
119-
{hint && <i>{hint}</i>}
120-
</legend>
121-
)
122-
}
123-
124102
function CheckboxOptionsContainer({ children, dropdown, ...rest }) {
125103
if (dropdown)
126104
return (
@@ -159,7 +137,7 @@ function CheckboxGroup(props) {
159137
return (
160138
<LabeledField
161139
className={className}
162-
labelComponent={CheckboxGroupLegend}
140+
labelComponent={FieldsetLegend}
163141
as="fieldset"
164142
{...props}
165143
>

src/forms/inputs/radio-group.js

+2-18
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React from 'react'
22
import PropTypes from 'prop-types'
33
import {
4-
convertNameToLabel,
54
radioGroupPropTypes,
65
fieldOptionsType,
76
omitLabelProps,
87
} from '../helpers'
9-
import { LabeledField } from '../labels'
8+
import { LabeledField, FieldsetLegend } from '../labels'
109
import { serializeOptions, filterInvalidDOMProps } from '../../utils'
11-
import classnames from 'classnames'
1210

1311
/**
1412
*
@@ -90,20 +88,6 @@ const defaultProps = {
9088
radioInputProps: {},
9189
}
9290

93-
function RadioGroupLegend({ label, name, required, requiredIndicator, hint }) {
94-
return (
95-
<legend className={classnames({ 'visually-hidden': label === false })}>
96-
{label || convertNameToLabel(name)}
97-
{required && requiredIndicator && (
98-
<span className="required-indicator" aria-hidden="true">
99-
{requiredIndicator}
100-
</span>
101-
)}
102-
{hint && <i>{hint}</i>}
103-
</legend>
104-
)
105-
}
106-
10791
// This should never be used by itself, so it does not exist as a separate export
10892
function RadioButton(props) {
10993
const {
@@ -146,7 +130,7 @@ function RadioGroup(props) {
146130
return (
147131
<LabeledField
148132
className={className}
149-
labelComponent={RadioGroupLegend}
133+
labelComponent={FieldsetLegend}
150134
as="fieldset"
151135
{...props}
152136
>

src/forms/labels/fieldset-legend.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import classnames from 'classnames'
4+
import { convertNameToLabel } from '../helpers'
5+
6+
/**
7+
*
8+
* A legend representing a caption for the content of its parent field set element
9+
*
10+
* This component must be used as a direct child and the only legend of the <fieldset> element that groups related controls
11+
*
12+
*
13+
* The text of the legend is set using the following rules:
14+
* - 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_
15+
* - Else If the `label` prop is set to a string, the label will display that text
16+
* - Otherwise, the label will be set using the `name` prop.
17+
*
18+
*
19+
* @name FieldsetLegend
20+
* @type Function
21+
* @param {String} name - The name of the associated group
22+
* @param {String} [hint] - A usage hint for the associated input
23+
* @param {String|Boolean} [label] - Custom text for the legend
24+
* @param {Boolean} [required=false] - A boolean value to indicate whether the field is required
25+
* @param {String} [requiredIndicator=''] - Custom character to denote a field is required
26+
27+
* @example
28+
*
29+
*
30+
* function ShippingAddress (props) {
31+
* const name = 'shippingAddress'
32+
* return (
33+
* <fieldset>
34+
* <FieldsetLegend name={name} />
35+
* <Input id={`${name}.name`} input={{name: 'name'}} />
36+
* <Input id={`${name}.street`} input={{name: 'street'}} />
37+
* <Input id={`${name}.city`}" input={{name: 'city'}} />
38+
* <Input id={`${name}.state`} input={{name: 'state'}} />
39+
* </fieldset>
40+
* )
41+
* }
42+
*
43+
*/
44+
45+
const propTypes = {
46+
label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
47+
name: PropTypes.string.isRequired,
48+
required: PropTypes.bool,
49+
requiredIndicator: PropTypes.string,
50+
className: PropTypes.string,
51+
hint: PropTypes.string,
52+
}
53+
54+
const defaultProps = {
55+
children: null,
56+
hint: '',
57+
label: '',
58+
required: false,
59+
requiredIndicator: '',
60+
className: '',
61+
}
62+
63+
function FieldsetLegend({
64+
label,
65+
name,
66+
required,
67+
requiredIndicator,
68+
className,
69+
hint,
70+
}) {
71+
return (
72+
<legend
73+
className={classnames(className, { 'visually-hidden': label === false })}
74+
>
75+
{label || convertNameToLabel(name)}
76+
{required && requiredIndicator && (
77+
<span className="required-indicator" aria-hidden="true">
78+
{requiredIndicator}
79+
</span>
80+
)}
81+
{hint && <i>{hint}</i>}
82+
</legend>
83+
)
84+
}
85+
86+
FieldsetLegend.propTypes = propTypes
87+
FieldsetLegend.defaultProps = defaultProps
88+
89+
export default FieldsetLegend

src/forms/labels/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export ErrorLabel from './error-label'
22
export InputError from './input-error'
33
export InputLabel from './input-label'
44
export LabeledField from './labeled-field'
5+
export FieldsetLegend from './fieldset-legend'

src/forms/labels/input-label.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ import { useToggle } from '../../utils'
2525
* @name InputLabel
2626
* @type Function
2727
* @param {String} name - The name of the associated input
28-
* @param {String} [id=name] - The id of the associated input (defaults to name)
28+
* @param {String} [id=name] - The id of the associated input
2929
* @param {String} [hint] - A usage hint for the associated input
3030
* @param {String|Boolean} [label] - Custom text for the label
3131
* @param {String} [tooltip] - A message to display in a tooltip
32-
* @param {Boolean} [required] - A boolean value to indicate whether the field is required
33-
* @param {String} [requiredIndicator] - Custom character to denote a field is required (optional, default `''`)
32+
* @param {Boolean} [required=false] - A boolean value to indicate whether the field is required
33+
* @param {String} [requiredIndicator=''] - Custom character to denote a field is required
3434
3535
* @example
3636
*
@@ -74,6 +74,7 @@ const defaultProps = {
7474
id: '',
7575
label: '',
7676
tooltip: '',
77+
required: false,
7778
requiredIndicator: '',
7879
className: '',
7980
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react'
2+
import { storiesOf } from '@storybook/react'
3+
import { FieldsetLegend } from 'src'
4+
5+
storiesOf('FieldsetLegend', module)
6+
.add('with default label', () => <FieldsetLegend name="nameOfInput" />)
7+
.add('with custom label', () => (
8+
<FieldsetLegend name="nameOfInput" label="Custom Label" />
9+
))
10+
.add('with no label', () => (
11+
<FieldsetLegend name="nameOfInput" label={false} />
12+
))
13+
.add('with required true custom indicator', () => (
14+
<FieldsetLegend
15+
name="nameOfInput"
16+
label="Custom Label"
17+
required={true}
18+
requiredIndicator={'*'}
19+
/>
20+
))
21+
.add('with hint', () => <FieldsetLegend name="nameOfInput" hint="hint" />)

stories/forms/labels/input-label.story.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@ storiesOf('InputLabel', module)
88
<InputLabel name="nameOfInput" label="Custom Label" />
99
))
1010
.add('with no label', () => <InputLabel name="nameOfInput" label={false} />)
11-
.add('with required true default indicator', () => (
12-
<InputLabel name="nameOfInput" label="Custom Label" required />
13-
))
1411
.add('with required true custom indicator', () => (
1512
<InputLabel
1613
name="nameOfInput"
1714
label="Custom Label"
18-
required
15+
required={true}
1916
requiredIndicator={'*'}
2017
/>
2118
))
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react'
2+
import { render, screen } from '@testing-library/react'
3+
import { FieldsetLegend } from '../../../src/'
4+
5+
const name = 'contactDetails'
6+
const formattedName = 'Contact Details'
7+
8+
describe('FieldsetLegend', () => {
9+
test('renders label correctly when label prop is a string', () => {
10+
render(<FieldsetLegend name={name} label="Your Information" />)
11+
expect(screen.getByText('Your Information')).toBeInTheDocument()
12+
})
13+
14+
test('renders label correctly using name prop when label prop is not provided', () => {
15+
render(<FieldsetLegend name={name} />)
16+
expect(screen.getByText(formattedName)).toBeInTheDocument()
17+
})
18+
19+
test('renders label with class "visually-hidden" when label prop is false', () => {
20+
render(<FieldsetLegend name={name} label={false} />)
21+
expect(screen.getByText(formattedName)).toHaveClass('visually-hidden')
22+
})
23+
24+
test('does not show required indicator when no custom required indicator is provided', () => {
25+
render(<FieldsetLegend name={name} required />)
26+
expect(screen.getByText(formattedName).textContent).toEqual(formattedName)
27+
})
28+
29+
test('shows custom indicator when required is true and custom requiredIndicator is provided', () => {
30+
render(<FieldsetLegend name={name} required requiredIndicator={'*'} />)
31+
expect(screen.getByText('*')).toBeInTheDocument()
32+
})
33+
34+
test('hides custom indicator when required is false and custom requiredIndicator is provided', () => {
35+
render(
36+
<FieldsetLegend name={name} required={false} requiredIndicator={'*'} />
37+
)
38+
expect(screen.queryByText('*')).not.toBeInTheDocument()
39+
})
40+
41+
test('shows hint when hint is provided', () => {
42+
render(<FieldsetLegend name={name} hint="hint" />)
43+
expect(screen.getByText(formattedName)).toHaveTextContent('hint')
44+
})
45+
})

0 commit comments

Comments
 (0)