Skip to content

Commit

Permalink
Fix Form.Check.Label spacing
Browse files Browse the repository at this point in the history
Currently, when using a `Form.Check.Label` component to customize
`Form.Check` rendering, there will be no space between the checkbox and
the label. This is because `Form.Check` is currently relying on the
presence of a `label` prop to apply the `form-check` class name to the
wrapping `<div>`, because checkboxes without labels [don't need the
wrapping element to have the `form-check` class name][1].

This commit adds a utility to check whether an element has a child of a
certain type. It then uses that utility to check if a `Form.Check`
element has a `Form.Check.Label` child and takes that into account when
determining whether the checkbox has a label.

Adding a special property (currently called `typeName`, but that can
certainly change) to components for this utility is necessary because
React minifies the `displayName` property in production.

[1]: #5938 (comment)
  • Loading branch information
TyMick committed Aug 19, 2021
1 parent 74894de commit 3ff951b
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 7 deletions.
21 changes: 20 additions & 1 deletion src/ElementChildren.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
import { BsComponent } from './helpers';

/**
* Iterates through children that are typically specified as `props.children`,
Expand Down Expand Up @@ -35,4 +36,22 @@ function forEach<P = any>(
});
}

export { map, forEach };
/**
* Checks that at least one child is of the specified type (either a string for
* an HTML element or a component for a React element).
*/
function includesType<As extends React.ElementType, P = any>(
children: React.ReactNode,
type: string | BsComponent<As, P>,
) {
const childrenList = React.Children.toArray(children);
return childrenList.some(
(child) =>
React.isValidElement<P>(child) &&
(child.type === type ||
(child.type as BsComponent<As, P>)?.typeName ===
(type as BsComponent<As, P>).typeName),
);
}

export { map, forEach, includesType };
7 changes: 5 additions & 2 deletions src/FormCheck.tsx
Expand Up @@ -8,6 +8,7 @@ import FormCheckLabel from './FormCheckLabel';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';
import { includesType } from './ElementChildren';

export type FormCheckType = 'checkbox' | 'radio' | 'switch';

Expand Down Expand Up @@ -150,7 +151,9 @@ const FormCheck: BsPrefixRefForwardingComponent<'input', FormCheckProps> =
[controlId, id],
);

const hasLabel = label != null && label !== false && !children;
const hasLabel =
(label != null && label !== false) ||
(children && includesType(children, FormCheckLabel));

const input = (
<FormCheckInput
Expand All @@ -170,7 +173,7 @@ const FormCheck: BsPrefixRefForwardingComponent<'input', FormCheckProps> =
style={style}
className={classNames(
className,
label && bsPrefix,
hasLabel && bsPrefix,
inline && `${bsPrefix}-inline`,
type === 'switch' && bsSwitchPrefix,
)}
Expand Down
8 changes: 6 additions & 2 deletions src/FormCheckLabel.tsx
Expand Up @@ -5,7 +5,7 @@ import { useContext } from 'react';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';

import { BsPrefixProps } from './helpers';
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';

export interface FormCheckLabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement>,
Expand All @@ -21,7 +21,10 @@ const propTypes = {
htmlFor: PropTypes.string,
};

const FormCheckLabel = React.forwardRef<HTMLLabelElement, FormCheckLabelProps>(
const FormCheckLabel: BsPrefixRefForwardingComponent<
'label',
FormCheckLabelProps
> = React.forwardRef<HTMLLabelElement, FormCheckLabelProps>(
({ bsPrefix, className, htmlFor, ...props }, ref) => {
const { controlId } = useContext(FormContext);

Expand All @@ -39,6 +42,7 @@ const FormCheckLabel = React.forwardRef<HTMLLabelElement, FormCheckLabelProps>(
);

FormCheckLabel.displayName = 'FormCheckLabel';
FormCheckLabel.typeName = 'FormCheckLabel';
FormCheckLabel.propTypes = propTypes;

export default FormCheckLabel;
14 changes: 12 additions & 2 deletions src/helpers.ts
Expand Up @@ -33,18 +33,28 @@ export interface BsPrefixRefForwardingComponent<
contextTypes?: any;
defaultProps?: Partial<P>;
displayName?: string;
typeName?: string;
}

export class BsPrefixComponent<
As extends React.ElementType,
P = unknown,
> extends React.Component<ReplaceProps<As, BsPrefixProps<As> & P>> {}
> extends React.Component<ReplaceProps<As, BsPrefixProps<As> & P>> {
typeName?: string;
}

// Need to use this instead of typeof Component to get proper type checking.
export type BsPrefixComponentClass<
As extends React.ElementType,
P = unknown,
> = React.ComponentClass<ReplaceProps<As, BsPrefixProps<As> & P>>;
> = React.ComponentClass<ReplaceProps<As, BsPrefixProps<As> & P>> & {
typeName?: string;
};

export type BsComponent<As extends React.ElementType, P = unknown> =
| BsPrefixRefForwardingComponent<As, P>
| BsPrefixComponentClass<As, P>
| BsPrefixComponentClass<As, P>;

export type TransitionType = boolean | TransitionComponent;

Expand Down

0 comments on commit 3ff951b

Please sign in to comment.