Skip to content

Commit 5284503

Browse files
authored
Merge pull request #454 from acelaya-forks/feature/labelled-help-text
Add support to pass help text to labelled form control variants
2 parents 44a0836 + b1b69a9 commit 5284503

File tree

9 files changed

+419
-404
lines changed

9 files changed

+419
-404
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
66

7-
## [Unreleased]
7+
## [0.8.13] - 2025-04-27
88
### Added
9-
* *Nothing*
9+
* Allow help text to be passed to labelled form control tailwind-based components.
1010

1111
### Changed
1212
* Define bootstrap breakpoints in tailwind preset, so that migration can be done progressively.

dev/tailwind/form/InputsPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export const InputsPage: FC = () => {
4848
<Input placeholder="Error input" feedback="error" />
4949
<Input placeholder="Disabled input" disabled />
5050
<Input placeholder="Readonly input" readOnly />
51-
<LabelledInput label="Labelled input" />
51+
<LabelledInput label="Labelled input" helpText="This is the help text under the input" />
52+
<LabelledInput label="Error labelled input" error="This input is invalid!" />
5253
<Input placeholder="Large input" size="lg" />
5354
<Input placeholder="Small input" size="sm" />
5455
<LabelledRevealablePasswordInput label="Revealable password input" defaultValue="some_password" />
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { FC, PropsWithChildren, ReactNode } from 'react';
2+
3+
export type FormControlWithFeedbackProps = PropsWithChildren<{
4+
/** An error message to display under the input. Will cause the input to be set with `error` feedback. */
5+
error?: ReactNode;
6+
/** Informative help to be display under the input */
7+
helpText?: ReactNode;
8+
9+
'data-testid'?: string;
10+
}>;
11+
12+
/**
13+
* This component should not be exported from the module. It's designed to reuse as a helper wrapper
14+
*/
15+
export const FormControlWithFeedback: FC<FormControlWithFeedbackProps> = (
16+
{ children, helpText, error, 'data-testid': testId },
17+
) => (
18+
<div className="tw:flex tw:flex-col tw:gap-1" data-testid={testId}>
19+
{children}
20+
{helpText && (
21+
<small
22+
data-testid={testId ? `${testId}-help-text` : 'help-text'}
23+
className="tw:text-gray-500 tw:dark:text-gray-400"
24+
>
25+
{helpText}
26+
</small>
27+
)}
28+
{error && <span data-testid={testId ? `${testId}-error` : 'error'} className="tw:text-danger">{error}</span>}
29+
</div>
30+
);
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
import { forwardRef , useId } from 'react';
1+
import { forwardRef, useId } from 'react';
22
import type { RequiredReactNode } from '../types';
3+
import type { FormControlWithFeedbackProps } from './FormControlWithFeedback';
4+
import { FormControlWithFeedback } from './FormControlWithFeedback';
35
import type { InputProps } from './Input';
46
import { Input } from './Input';
57
import { Label } from './Label';
68

7-
export type LabelledInputProps = Omit<InputProps, 'className' | 'id' | 'feedback'> & {
9+
export type LabelledInputProps = Omit<InputProps, 'className' | 'id' | 'feedback'> & FormControlWithFeedbackProps & {
810
label: RequiredReactNode;
911
inputClassName?: string;
10-
error?: string;
1112

1213
/** Alternative to `required`. Causes the input to be required, without displaying an asterisk */
1314
hiddenRequired?: boolean;
1415
};
1516

1617
export const LabelledInput = forwardRef<HTMLInputElement, LabelledInputProps>((
17-
{ label, inputClassName, required, hiddenRequired, error, ...rest },
18+
{ label, inputClassName, required, hiddenRequired, error, helpText, 'data-testid': testId, ...rest },
1819
ref,
1920
) => {
2021
const id = useId();
2122
return (
22-
<div className="tw:flex tw:flex-col tw:gap-1">
23+
<FormControlWithFeedback error={error} helpText={helpText} data-testid={testId}>
2324
<Label htmlFor={id} required={required}>{label}</Label>
2425
<Input
2526
ref={ref}
@@ -29,7 +30,6 @@ export const LabelledInput = forwardRef<HTMLInputElement, LabelledInputProps>((
2930
feedback={error ? 'error' : undefined}
3031
{...rest}
3132
/>
32-
{error && <span className="tw:text-danger">{error}</span>}
33-
</div>
33+
</FormControlWithFeedback>
3434
);
3535
});
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import { forwardRef , useId } from 'react';
22
import type { RequiredReactNode } from '../types';
3+
import type { FormControlWithFeedbackProps } from './FormControlWithFeedback';
4+
import { FormControlWithFeedback } from './FormControlWithFeedback';
35
import { Label } from './Label';
46
import type { RevealablePasswordInputProps } from './RevealablePasswordInput';
57
import { RevealablePasswordInput } from './RevealablePasswordInput';
68

79
export type LabelledRevealablePasswordInputProps =
8-
Omit<RevealablePasswordInputProps, 'className' | 'id' | 'feedback'> & {
10+
Omit<RevealablePasswordInputProps, 'className' | 'id' | 'feedback'> & FormControlWithFeedbackProps & {
911
label: RequiredReactNode;
1012
inputClassName?: string;
11-
error?: string;
1213

1314
/** Alternative to `required`. Causes the input to be required, without displaying an asterisk */
1415
hiddenRequired?: boolean;
1516
};
1617

1718
export const LabelledRevealablePasswordInput = forwardRef<HTMLInputElement, LabelledRevealablePasswordInputProps>((
18-
{ label, inputClassName, required, hiddenRequired, error, ...rest },
19+
{ label, inputClassName, required, hiddenRequired, error, helpText, 'data-testid': testId, ...rest },
1920
ref,
2021
) => {
2122
const id = useId();
2223
return (
23-
<div className="tw:flex tw:flex-col tw:gap-1">
24+
<FormControlWithFeedback error={error} helpText={helpText} data-testid={testId}>
2425
<Label htmlFor={id} required={required}>{label}</Label>
2526
<RevealablePasswordInput
2627
ref={ref}
@@ -30,7 +31,6 @@ export const LabelledRevealablePasswordInput = forwardRef<HTMLInputElement, Labe
3031
feedback={error ? 'error' : undefined}
3132
{...rest}
3233
/>
33-
{error && <span className="tw:text-danger">{error}</span>}
34-
</div>
34+
</FormControlWithFeedback>
3535
);
3636
});
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { forwardRef , useId } from 'react';
1+
import { forwardRef, useId } from 'react';
22
import type { RequiredReactNode } from '../types';
3+
import type { FormControlWithFeedbackProps } from './FormControlWithFeedback';
4+
import { FormControlWithFeedback } from './FormControlWithFeedback';
35
import { Label } from './Label';
46
import type { SelectProps } from './Select';
57
import { Select } from './Select';
68

7-
export type LabelledSelectProps = Omit<SelectProps, 'className' | 'id'> & {
9+
export type LabelledSelectProps = Omit<SelectProps, 'className' | 'id'> & FormControlWithFeedbackProps & {
810
label: RequiredReactNode;
911
selectClassName?: string;
1012

@@ -13,14 +15,21 @@ export type LabelledSelectProps = Omit<SelectProps, 'className' | 'id'> & {
1315
};
1416

1517
export const LabelledSelect = forwardRef<HTMLSelectElement, LabelledSelectProps>((
16-
{ selectClassName, label, required, hiddenRequired, ...rest },
18+
{ selectClassName, label, error, helpText, required, hiddenRequired, 'data-testid': testId, ...rest },
1719
ref,
1820
) => {
1921
const id = useId();
2022
return (
21-
<div className="tw:flex tw:flex-col tw:gap-1">
23+
<FormControlWithFeedback error={error} helpText={helpText} data-testid={testId}>
2224
<Label htmlFor={id} required={required}>{label}</Label>
23-
<Select ref={ref} id={id} className={selectClassName} required={required || hiddenRequired} {...rest} />
24-
</div>
25+
<Select
26+
ref={ref}
27+
id={id}
28+
className={selectClassName}
29+
required={required || hiddenRequired}
30+
feedback={error ? 'error' : undefined}
31+
{...rest}
32+
/>
33+
</FormControlWithFeedback>
2534
);
2635
});

test/tailwind/feedback/CardModal.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('<CardModal />', () => {
6868
])('renders expected size', (size) => {
6969
const { container } = setUp({ size });
7070
// We need to match against the container's parent (the body) since dialogs are rendered there via portals
71-
expect(container.parentNode).toMatchSnapshot();
71+
expect(container.parentNode?.querySelector('dialog')).toMatchSnapshot();
7272
});
7373

7474
it.each([
@@ -78,7 +78,7 @@ describe('<CardModal />', () => {
7878
])('renders expected variant', (props) => {
7979
const { container } = setUp(props);
8080
// We need to match against the container's parent (the body) since dialogs are rendered there via portals
81-
expect(container.parentNode).toMatchSnapshot();
81+
expect(container.parentNode?.querySelector('dialog')).toMatchSnapshot();
8282
});
8383

8484
it('defers closing the modal until the transition has finished', async () => {

0 commit comments

Comments
 (0)