Repository: mlaursen/react-md
Pull request #855 (2019-12-13 16:02:55)

Added getActiveLabel prop to SelectField
Hello,

I've added `getActiveLabel` prop to `SelectField` from v1 to have ability to customise a label that is displayed for active item.
Also I've fixed a bug that I introduced when I implemented support for `getItemProps`.

mlaursen: I just pulled it down, and it looks good to me. Thanks!

Patch:
diff --git a/docs/src/components/Components/SelectFields/Simple.jsx b/docs/src/components/Components/SelectFields/Simple.jsx
index 984498134c..021d10457f 100644
--- a/docs/src/components/Components/SelectFields/Simple.jsx
+++ b/docs/src/components/Components/SelectFields/Simple.jsx
@@ -25,6 +25,10 @@ const OBJECT_ITEMS = [{
 
 const icon = <SVGIcon use={arrowDropDown.url} />;
 
+function getActiveLabel({ activeLabel, activeIndex }) {
+  return activeIndex > -1 ? `#${activeIndex + 1}: ${activeLabel}` : activeLabel;
+}
+
 const Simple = ({ simplifiedMenu }) => (
   <div className="md-grid">
     <h4 className="md-cell md-cell--12">Normal SelectFields</h4>
@@ -101,6 +105,7 @@ const Simple = ({ simplifiedMenu }) => (
       placeholder="Strings disabled"
       className="md-cell md-cell--bottom"
       menuItems={STRING_ITEMS}
+      getActiveLabel={getActiveLabel}
       disabled
       dropdownIcon={icon}
       simplifiedMenu={simplifiedMenu}
diff --git a/src/js/SelectFields/SelectField.d.ts b/src/js/SelectFields/SelectField.d.ts
index 2d75a4ef16..25960c1a61 100644
--- a/src/js/SelectFields/SelectField.d.ts
+++ b/src/js/SelectFields/SelectField.d.ts
@@ -9,10 +9,36 @@ import {
 } from '../Helpers/Layover';
 import { BaseMenuProps } from '../Menus/Menu';
 
+export type MenuItem = number | string | Object | React.ReactElement<any>;
+export type MenuItemList = Array<MenuItem>;
+
+export type ListValue = number | string;
+
+export interface GetItemPropsParam {
+  index: number;
+  active: boolean;
+  disabled: boolean;
+  itemValue: any;
+  value: ListValue;
+  props: Object;
+  item: number | string;
+  field: SelectFieldComponent;
+}
+
+export interface GetActiveLabelParam {
+  activeItem: MenuItem;
+  activeIndex: number;
+  activeLabel: string;
+  activeValue: any;
+  value: ListValue;
+  menuItems: MenuItemList;
+  field: SelectFieldComponent;
+}
+
 export interface FieldDataProps {
   id: string;
   name: string;
-  value: number | string;
+  value: ListValue;
 }
 
 export interface SharedSelectFieldProps extends BaseMenuProps, SharedTextFieldProps {
@@ -26,16 +52,17 @@ export interface SharedSelectFieldProps extends BaseMenuProps, SharedTextFieldPr
   defaultVisible?: boolean;
   visible?: boolean;
   onVisibilityChange?: (visible: boolean, event: React.MouseEvent<HTMLElement>) => void;
-  menuItems?: Array<number | string | Object | React.ReactElement<any>>;
+  menuItems?: MenuItemList;
   keyboardMatchingTimeout?: number;
   itemLabel?: string;
   itemValue?: string;
   itemProps?: string;
   name?: string;
-  getItemProps?: (data: Object) => Object;
-  defaultValue?: number | string;
-  value?: number | string;
-  onChange?: (value: number | string, selectedIndex: number, event: React.MouseEvent<HTMLElement>, data: FieldDataProps) => void;
+  getItemProps?: (data: GetItemPropsParam) => Object;
+  getActiveLabel?: (data: GetActiveLabelParam) => React.ReactNode;
+  defaultValue?: ListValue;
+  value?: ListValue;
+  onChange?: (value: ListValue, selectedIndex: number, event: React.MouseEvent<HTMLElement>, data: FieldDataProps) => void;
   onClick?: (event: React.MouseEvent<HTMLElement>) => void;
   dropdownIcon?: React.ReactElement<any>;
   toolbar?: boolean;
diff --git a/src/js/SelectFields/SelectField.js b/src/js/SelectFields/SelectField.js
index 197071fb51..8cc949dd34 100644
--- a/src/js/SelectFields/SelectField.js
+++ b/src/js/SelectFields/SelectField.js
@@ -214,6 +214,21 @@ export default class SelectField extends PureComponent {
      */
     getItemProps: PropTypes.func,
 
+    /**
+     * An optional function to get a node that will be rendered as the label of active item.
+     * By default a string will be used that is retrieved from active item's data.
+     *
+     * An object with the following fields will be passed into the function:
+     * - `activeItem` - active item's data
+     * - `activeIndex` - active item's index
+     * - `activeLabel` - active item's label (that is used by default when `getActiveLabel` prop is not specified)
+     * - `activeValue` - active item's value
+     * - `value` - current list value
+     * - `menuItems` - value of `menuItems` prop
+     * - `field` - reference to the component instance
+     */
+    getActiveLabel: PropTypes.func,
+
     /**
      * The default value to use for the select field. If this is set, it should either match
      * one of the `number` or `string` in your `menuItems` list or be the empty string. If
@@ -992,7 +1007,7 @@ export default class SelectField extends PureComponent {
           index: i,
           active,
           disabled,
-          itemValue,
+          itemValue: dataValue,
           value,
           props,
           item,
@@ -1049,6 +1064,7 @@ export default class SelectField extends PureComponent {
       itemValue,
       itemProps,
       getItemProps,
+      getActiveLabel,
       defaultValue,
       defaultVisible,
       onClick,
@@ -1082,12 +1098,26 @@ export default class SelectField extends PureComponent {
       listId = `${menuId}-options`;
     }
 
-    const { listProps, active, activeLabel } = this.state;
+    const { listProps, active, activeIndex } = this.state;
     const below = position === SelectField.Positions.BELOW;
     const visible = typeof isOpen !== 'undefined' ? isOpen : getField(this.props, this.state, 'visible');
     const value = getField(this.props, this.state, 'value');
     const useSameWidth = typeof sameWidth !== 'undefined' ? sameWidth : below;
 
+    let { activeLabel } = this.state;
+    if (typeof getActiveLabel === 'function') {
+      const activeItem = activeIndex > -1 ? menuItems[activeIndex] : null;
+      activeLabel = getActiveLabel({
+        activeItem,
+        activeIndex,
+        activeLabel,
+        activeValue: activeItem ? this._getItemPart(activeItem, itemLabel, itemValue) : '',
+        value,
+        menuItems,
+        field: this,
+      });
+    }
+
     const toggle = (
       <SelectFieldToggle
         {...props}


Test patch:
diff --git a/src/js/SelectFields/__tests__/SelectField.js b/src/js/SelectFields/__tests__/SelectField.js
index 1976cd6797..060a18bf8f 100644
--- a/src/js/SelectFields/__tests__/SelectField.js
+++ b/src/js/SelectFields/__tests__/SelectField.js
@@ -171,6 +171,21 @@ describe('SelectField', () => {
     expect(items.at(2).prop('secondaryText')).toBe(lastText);
   });
 
+  it('should render active item\'s label that is returned by specified function', () => {
+    function getLabel({ activeLabel, activeIndex, menuItems }) {
+      return activeIndex > -1 ? `${activeLabel} (${activeIndex + 1}/${menuItems.length})` : activeLabel;
+    }
+
+    const menuItems = ['a', 'b', 'c', 'd'];
+    const field = mount(<SelectField id="test" menuItems={menuItems} value="c" getActiveLabel={getLabel} onChange={jest.fn()} />);
+    let input = field.find(SelectFieldInput).first();
+    expect(input.text()).toContain(getLabel({ activeLabel: 'c', activeIndex: 2, menuItems }));
+
+    field.setProps({ value: 'a' });
+    input = field.find(SelectFieldInput).first();
+    expect(input.text()).toContain(getLabel({ activeLabel: 'a', activeIndex: 0, menuItems }));
+  });
+
   it('should still have the correct label if the menuItems are defined as a list in the render and the parent component rerenders', () => {
     let renderCount = 0;
     class Test extends React.Component {