Skip to content

Commit 6f8b824

Browse files
committed
feat: make generated ids only client-side to support ssr
1 parent bbe2dba commit 6f8b824

22 files changed

+360
-610
lines changed
Lines changed: 30 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,44 @@
11
import classnames from 'classnames';
2-
import PropTypes from 'prop-types';
32
import React from 'react';
43
import type { InputProps } from 'reactstrap';
4+
import { useUniqueClientId } from '../../hooks/useUniqueClientId';
55
import FormGroup from '../Form/FormGroup';
66
import Input from '../Input/Input';
77
import Label from '../Label/Label';
88

9-
interface CheckboxBooleanInputSpecificProps {
9+
export type CheckboxBooleanInputProps = Omit<InputProps, 'onChange' | 'value'> & {
1010
checkboxLabel?: React.ReactNode;
1111
onChange?: (isChecked: boolean) => void;
1212
value?: boolean;
13+
};
14+
15+
function CheckboxBooleanInput({
16+
checkboxLabel,
17+
className,
18+
onChange,
19+
value,
20+
...inputProps
21+
}: CheckboxBooleanInputProps) {
22+
const id = useUniqueClientId('checkbox-boolean-input-', inputProps.id);
23+
const classNames = classnames('pt-2', className);
24+
return (
25+
<FormGroup check className={classNames}>
26+
<Input
27+
{...inputProps}
28+
id={id}
29+
type="checkbox"
30+
checked={value}
31+
onChange={(e) => onChange && onChange(e.target.checked)}
32+
/>
33+
{checkboxLabel && (
34+
<Label check for={id}>
35+
{checkboxLabel}
36+
</Label>
37+
)}
38+
</FormGroup>
39+
);
1340
}
14-
type ExtendsWithTypeOverrides<T, U> = U & Omit<T, keyof U>;
15-
export type CheckboxBooleanInputProps = ExtendsWithTypeOverrides<
16-
InputProps,
17-
CheckboxBooleanInputSpecificProps
18-
>;
1941

20-
let count = 0;
21-
22-
function getID() {
23-
return `checkbox-boolean-input-${count++}`;
24-
}
25-
26-
class CheckboxBooleanInput extends React.Component<CheckboxBooleanInputProps> {
27-
static propTypes = {
28-
id: PropTypes.string,
29-
checkboxLabel: PropTypes.node,
30-
className: PropTypes.string,
31-
onChange: PropTypes.func,
32-
value: PropTypes.bool,
33-
};
34-
35-
id = getID();
36-
37-
constructor(props: CheckboxBooleanInputProps) {
38-
super(props);
39-
40-
this.id = props.id || this.id;
41-
}
42-
43-
render() {
44-
const { checkboxLabel, className, onChange, value, ...inputProps } = this.props;
45-
const classNames = classnames('pt-2', className);
46-
47-
return (
48-
<FormGroup check className={classNames}>
49-
<Input
50-
id={this.id}
51-
{...inputProps}
52-
type="checkbox"
53-
checked={value}
54-
onChange={(e) => onChange && onChange(e.target.checked)}
55-
/>
56-
{checkboxLabel && (
57-
<Label check for={this.id}>
58-
{checkboxLabel}
59-
</Label>
60-
)}
61-
</FormGroup>
62-
);
63-
}
64-
}
42+
CheckboxBooleanInput.displayName = 'CheckboxBooleanInput';
6543

6644
export default CheckboxBooleanInput;

src/components/Form/FormChoice.d.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/components/Form/FormChoice.js

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/components/Form/FormChoice.spec.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ import FormGroup from './FormGroup';
88

99
describe('<FormChoice />', () => {
1010
describe('unknown type', () => {
11-
it('should generate an id when not set', () => {
12-
const wrapper = shallow(<FormChoice type="radio">A</FormChoice>);
13-
assert(wrapper.find('Input[id^="form-choice-"]').exists());
14-
assert(wrapper.find('Label[for^="form-choice-"]').exists());
15-
});
16-
1711
it('should use id when set', () => {
1812
const wrapper = shallow(
1913
<FormChoice type="radio" id="yowza">

src/components/Form/FormChoice.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import classname from 'classnames';
2+
import React from 'react';
3+
import { useUniqueClientId } from '../../hooks/useUniqueClientId';
4+
import Input from '../Input/Input';
5+
import Label from '../Label/Label';
6+
import FormGroup from './FormGroup';
7+
8+
interface FormChoiceProps extends Omit<React.InputHTMLAttributes<HTMLOptionElement>, 'disabled'> {
9+
inline?: boolean;
10+
disabled?: boolean;
11+
checked?: boolean;
12+
containerClassName?: string;
13+
type?: 'checkbox' | 'radio' | 'select';
14+
value: string;
15+
selected?: boolean;
16+
}
17+
18+
function FormChoice({
19+
inline,
20+
disabled,
21+
children,
22+
containerClassName,
23+
type,
24+
value,
25+
...props
26+
}: FormChoiceProps) {
27+
const id = useUniqueClientId('form-choice-', props.id);
28+
29+
if (type === 'select') {
30+
return (
31+
<option {...props} disabled={disabled} value={value}>
32+
{children}
33+
</option>
34+
);
35+
}
36+
37+
const containerClasses = classname({ 'form-check-inline': inline }, containerClassName);
38+
39+
const computedValue = value || children;
40+
41+
const item = (
42+
<div className={containerClasses}>
43+
<Input
44+
id={id}
45+
type={type}
46+
{...(props as any)}
47+
disabled={disabled}
48+
value={computedValue as string}
49+
style={{ cursor: disabled ? 'not-allowed' : undefined }}
50+
/>
51+
<Label
52+
for={id}
53+
className="form-check-label"
54+
check={!inline}
55+
style={{ cursor: disabled ? 'not-allowed' : undefined }}
56+
>
57+
{children}
58+
</Label>
59+
</div>
60+
);
61+
62+
if (inline) {
63+
return item;
64+
}
65+
66+
return (
67+
<FormGroup check disabled={disabled}>
68+
{item}
69+
</FormGroup>
70+
);
71+
}
72+
73+
FormChoice.displayName = 'FormChoice';
74+
75+
export default FormChoice;

src/components/HasManyFields/HasManyFieldsRow.spec.js

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -70,61 +70,13 @@ describe('<HasManyFieldsRow />', () => {
7070
});
7171
});
7272

73-
it('should have a disabled tooltip when disabled and disabledReason', () => {
74-
component = shallow(
75-
<HasManyFieldsRow onDelete={onDelete} disabled disabledReason="NONE SHALL PASS">
76-
Stuff
77-
</HasManyFieldsRow>
78-
);
79-
const disabledTooltip = component.find(Tooltip);
80-
assert.equal(disabledTooltip.length, 1);
81-
// TODO assert disabledReason text
82-
});
83-
8473
it('should hide the delete button when deletable is false', () => {
8574
component = shallow(<HasManyFieldsRow deletable={false}>Stuff</HasManyFieldsRow>);
8675
assert.equal(component.find(ConfirmationButton).length, 0);
8776
assert.equal(component.find(Button).length, 0);
8877
assert.equal(component.find('.js-delete-col').length, 0);
8978
});
9079

91-
describe('when disabled and disabled reason', () => {
92-
let disabledTooltip;
93-
94-
beforeEach(() => {
95-
component = shallow(
96-
<HasManyFieldsRow onDelete={onDelete} disabled disabledReason="NONE SHALL PASS">
97-
Stuff
98-
</HasManyFieldsRow>
99-
);
100-
disabledTooltip = component.find(Tooltip);
101-
});
102-
103-
it('should have tooltip', () => {
104-
assert.equal(disabledTooltip.length, 1);
105-
// TODO assert disabledReason text
106-
});
107-
108-
it('defaults disabledReasonPlacement to top', () => {
109-
assert.equal(disabledTooltip.prop('placement'), 'top');
110-
});
111-
112-
it('sets tooltip placement based on disabledReasonPlacement', () => {
113-
component = shallow(
114-
<HasManyFieldsRow
115-
onDelete={onDelete}
116-
disabled
117-
disabledReason="NONE SHALL PASS"
118-
disabledReasonPlacement="left"
119-
>
120-
Stuff
121-
</HasManyFieldsRow>
122-
);
123-
disabledTooltip = component.find(Tooltip);
124-
assert.equal(disabledTooltip.prop('placement'), 'left');
125-
});
126-
});
127-
12880
it('should pass down props to delete button', () => {
12981
component = shallow(<HasManyFieldsRow deleteProps={{ tabIndex: -1 }}>Stuff</HasManyFieldsRow>);
13082
assert.strictEqual(component.find(ConfirmationButton).length, 1);

0 commit comments

Comments
 (0)