Repository: mlaursen/react-md
Pull request #831 (2019-05-20 17:00:21)

Added several className props for SelectionControl
I've added several className props to have ability to customize SelectionControl component.

mlaursen: Also, thanks for your continual contributions. I really appreciate it :)
gamtiq: I've updated types.

By the way I've got an error when I try to run `npm run test:watch` on my macOS 10.14.4.
It seems the workaround is described in the following issues but I haven't checked yet:
https://github.com/jest-community/vscode-jest/issues/423
https://github.com/cm-pliser-tdd-by-example/tdd-by-example-js/issues/10
mlaursen: Thanks! I'll try seeing if I can resolve that test issue for you. I'll try to release this by end of day tomorrow for you as well.

Patch:
diff --git a/src/js/SelectionControls/SelectionControl.d.ts b/src/js/SelectionControls/SelectionControl.d.ts
index b67b499ff0..caa0315e33 100644
--- a/src/js/SelectionControls/SelectionControl.d.ts
+++ b/src/js/SelectionControls/SelectionControl.d.ts
@@ -3,7 +3,12 @@ import { IdPropType, Props } from '../index';
 
 type Types = 'checkbox' | 'radio' | 'switch';
 
-export interface BaseSelectionControlProps extends Props {
+export type ClassNameType = string | ((props: SelectionControlProps, selectionControl: React.ComponentClass<SelectionControlProps>) => string);
+
+declare const {className, ...controlProps}: Props;
+type ControlProps = typeof controlProps;
+
+export interface BaseSelectionControlProps extends ControlProps {
   id: IdPropType;
   name: number | string;
   label?: React.ReactNode;
@@ -15,6 +20,11 @@ export interface BaseSelectionControlProps extends Props {
   inline?: boolean;
   'aria-label'?: string;
   'aria-labelledby'?: IdPropType;
+  className?: ClassNameType;
+  controlClassName?: ClassNameType;
+  inputClassName?: ClassNameType;
+  labelClassName?: ClassNameType;
+  labelTextClassName?: ClassNameType;
 }
 
 export interface SelectionControlProps extends BaseSelectionControlProps {
diff --git a/src/js/SelectionControls/SelectionControl.js b/src/js/SelectionControls/SelectionControl.js
index b5c7223c7e..44e40db810 100644
--- a/src/js/SelectionControls/SelectionControl.js
+++ b/src/js/SelectionControls/SelectionControl.js
@@ -71,8 +71,60 @@ export default class SelectionControl extends PureComponent {
 
     /**
      * An optional className to apply to the selection control's container.
+     * When a function is specified as prop value its returned value will be used as className.
+     * The following parameters will be passed into the function:
+     * - component's props (by default value from state is used for `checked` prop)
+     * - reference to component
      */
-    className: PropTypes.string,
+    className: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.string,
+    ]),
+
+    /**
+     * An optional className to apply to the selection control.
+     * When a function is specified as prop value its returned value will be used as className.
+     * The following parameters will be passed into the function:
+     * - component's props (by default value from state is used for `checked` prop)
+     * - reference to component
+     */
+    controlClassName: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.string,
+    ]),
+
+    /**
+     * An optional className to apply to the selection input element.
+     * The following parameters will be passed into the function:
+     * - component's props (by default value from state is used for `checked` prop)
+     * - reference to component
+     */
+    inputClassName: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.string,
+    ]),
+
+    /**
+     * An optional className to apply to the selection label element.
+     * The following parameters will be passed into the function:
+     * - component's props (by default value from state is used for `checked` prop)
+     * - reference to component
+     */
+    labelClassName: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.string,
+    ]),
+
+    /**
+     * An optional className to apply to the selection label's text element.
+     * The following parameters will be passed into the function:
+     * - component's props (by default value from state is used for `checked` prop)
+     * - reference to component
+     */
+    labelTextClassName: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.string,
+    ]),
 
     /**
      * The type of selection control to render.
@@ -265,6 +317,12 @@ export default class SelectionControl extends PureComponent {
     this._container = container;
   };
 
+  _getClassName(className, props) {
+    return typeof className === 'function'
+      ? className(props, this)
+      : className;
+  }
+
   _getIcon = () => {
     const { checkedIcon, uncheckedIcon, type } = this.props;
     const checked = getField(this.props, this.state, 'checked');
@@ -312,6 +370,10 @@ export default class SelectionControl extends PureComponent {
       id,
       style,
       className,
+      controlClassName,
+      inputClassName,
+      labelClassName,
+      labelTextClassName,
       inline,
       type,
       name,
@@ -351,14 +413,22 @@ export default class SelectionControl extends PureComponent {
     } = this.props;
 
     const checked = getField(this.props, this.state, 'checked');
+    const getClassNameProps = Object.assign({}, this.props, { checked });
     const isSwitch = type === 'switch';
     const labelId = this.props.label && `${id}-label`;
-    const label = this.props.label && <span id={labelId}>{this.props.label}</span>;
-
+    const label = this.props.label
+      &&
+      <span
+        id={labelId}
+        className={this._getClassName(labelTextClassName, getClassNameProps)}
+      >{this.props.label}</span>;
+
+    const classValue = this._getClassName(controlClassName, getClassNameProps);
     let control;
     if (isSwitch) {
       control = (
         <SwitchTrack
+          className={classValue}
           disabled={disabled}
           checked={checked}
           aria-label={ariaLabel}
@@ -375,7 +445,7 @@ export default class SelectionControl extends PureComponent {
             disabled,
             hint: !checked,
             secondary: checked,
-          }))}
+          }), classValue)}
           aria-checked={checked}
           aria-label={ariaLabel}
           aria-labelledby={ariaLabelledBy || labelId}
@@ -396,7 +466,7 @@ export default class SelectionControl extends PureComponent {
         className={cn('md-selection-control-container', {
           'md-selection-control-container--inline': inline,
           'md-switch-container': isSwitch,
-        }, className)}
+        }, this._getClassName(className, getClassNameProps))}
         onKeyDown={this._handleKeyDown}
       >
         <input
@@ -406,7 +476,8 @@ export default class SelectionControl extends PureComponent {
           checked={checked}
           onChange={this._handleChange}
           disabled={disabled}
-          className="md-selection-control-input"
+          className={cn('md-selection-control-input',
+            this._getClassName(inputClassName, getClassNameProps))}
           name={name}
           value={value}
           aria-hidden
@@ -415,7 +486,7 @@ export default class SelectionControl extends PureComponent {
           htmlFor={id}
           className={cn('md-selection-control-label', {
             'md-pointer--hover': !disabled,
-          }, themeColors({ disabled, text: !disabled }))}
+          }, themeColors({ disabled, text: !disabled }), this._getClassName(labelClassName, getClassNameProps))}
         >
           {labelBefore && label}
           {control}


Test patch:
diff --git a/src/js/SelectionControls/__tests__/SelectionControl.js b/src/js/SelectionControls/__tests__/SelectionControl.js
index 53dc3a8896..97dda7f464 100644
--- a/src/js/SelectionControls/__tests__/SelectionControl.js
+++ b/src/js/SelectionControls/__tests__/SelectionControl.js
@@ -32,17 +32,42 @@ describe('SelectionControl', () => {
     expect(control.hasClass(props.className)).toBe(true);
   });
 
+  it('should correctly apply classes returned by className function', () => {
+    const checkedClass = 'test-checked';
+    const uncheckedClass = 'test-unchecked';
+    const props = {
+      ...PROPS,
+      className: (propSet) => (propSet.checked ? checkedClass : uncheckedClass),
+      labelClassName: 'test-label-class',
+      labelTextClassName: (propSet, control) => `test-${control.props.label}`,
+    };
+
+    const control = shallow(<SelectionControl {...props} />);
+    expect(control.hasClass('md-selection-control-container')).toBe(true);
+    expect(control.hasClass(checkedClass)).toBe(false);
+    expect(control.hasClass(uncheckedClass)).toBe(true);
+
+    expect(control.find('label').hasClass(props.labelClassName)).toBe(true);
+    expect(control.find(`#${props.id}-label`).hasClass(`test-${props.label}`)).toBe(true);
+
+    control.setProps({ checked: true });
+    expect(control.hasClass('md-selection-control-container')).toBe(true);
+    expect(control.hasClass(checkedClass)).toBe(true);
+    expect(control.hasClass(uncheckedClass)).toBe(false);
+  });
+
   it('renders an input tag with the correct props', () => {
-    const props = { ...PROPS, value: 'hello!' };
+    const props = { ...PROPS, value: 'hello!', inputClassName: 'test-input-class' };
     const control = mount(<SelectionControl {...props} />);
     let input = control.find('input');
     expect(input.length).toBe(1);
+    expect(input.hasClass('md-selection-control-input')).toBe(true);
+    expect(input.hasClass(props.inputClassName)).toBe(true);
     input = input.instance();
     expect(input.id).toBe(props.id);
     expect(input.type).toBe(props.type);
     expect(input.name).toBe(props.name);
     expect(input.checked).toBe(false);
-    expect(input.className).toBe('md-selection-control-input');
     expect(input.value).toBe(props.value);
     expect(input.getAttribute('aria-hidden')).toBe('true');
   });