0

I'm having a strange problem that I have not encountered before while using React.

I'm creating a Button component that needs to be backwards compatible with a legacy Button component. This is requiring me to do some unfortunate things in my implementation...

One of the issues with the legacy Button is that there is a typo for passing in a tabIndex. The legacy Button's property for tabIndex is tapIndex.

This should be easy to map using something like...

<Button tabIndex={this.props.tabIndex || this.props.tapIndex || 0}></Button>

But for some reason this is not the case...

If the tabIndex attribute of the child is mapped to this.props.tapIndex the rendered element will not have a tabIndex set. In short:

This works:

<Button
  ...
  tabIndex={this.props.tabIndex}
  ...
>Hello</Button>

This does not:

<Button
  ...
  tabIndex={this.props.tapIndex}
  ...
>Hello</Button>

I have debugged the execution of my component and this.props.tapIndex is indeed set correctly and equal to the intended value. But again, the tabIndex attribute does not exist on the rendered element in this case.

Button.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import {
  stylesToStyled,
  cleanClassNames,
  extractProps
} from '@ns/utils';

import getKind, {
  KINDS,
  KIND_DICT,
  PRESENTATION_DICT,
  BOOLEAN_PROPS
} from './utils';
import PrimaryButton from './components/PrimaryButton';
import SecondaryButton from './components/SecondaryButton';
import TertiaryButton from './components/TertiaryButton';


const TYPES = ['button', 'submit', 'reset'];
const COMPONENT_DICT = {
  [KIND_DICT.PRIMARY]: PrimaryButton,
  [KIND_DICT.SECONDARY]: SecondaryButton,
  [KIND_DICT.TERTIARY]: TertiaryButton,
};

class Button extends Component {
  componentDidMount() {
    this.setAnalyticsMessageOnDOM();
  }

  componentDidUpdate() {
    this.setAnalyticsMessageOnDOM();
  }

  get StyledButton() {
    // Get the appropriate Component
    const kind = getKind(this.props, extractProps(
      this.props.className,
      BOOLEAN_PROPS
    ));

    // Add passed in styles
    const styles = stylesToStyled(this.props.style);
    return styled(COMPONENT_DICT[kind])(
      styles.properties,
      ...styles.values
    );
  }

  get tabIndex() {
    if (Number.isInteger(this.props.tabIndex)) {
      return this.props.tabIndex;
    }

    if (Number.isInteger(this.props.tapIndex)) {
      return this.props.tapIndex;
    }

    return 0;
  }

  setAnalyticsMessageOnDOM() {
    // Terrible Hack
    this.buttonRef.setAttribute(
      'analyticstrack',
      this.props.analyticstrackmessage
    );
  }

  render () {
    const {
      className,
      children,
      ...props
    } = this.props;
    const { StyledButton } = this;

    return (
      <StyledButton
        className={cleanClassNames(className, BOOLEAN_PROPS)}
        tabIndex={this.tabIndex}
        innerRef={el => {this.buttonRef = el}}
        {...props}
      >
        {children}
      </StyledButton>
    );
  }
}

Button.propTypes = {
  [KIND_DICT.PRIMARY]: PropTypes.bool,
  [KIND_DICT.SECONDARY]: PropTypes.bool,
  [KIND_DICT.TERTIARY]: PropTypes.bool,
  [PRESENTATION_DICT.DISABLED]: PropTypes.bool,
  [PRESENTATION_DICT.INVERTED]: PropTypes.bool,
  children: PropTypes.node.isRequired,
  className: PropTypes.string,
  style: PropTypes.object,
  onClick: PropTypes.func,
  tabIndex: PropTypes.number,
  // Deprectation Warning: Typo that is being used in production, use tabindex.
  tapIndex: PropTypes.number,
  type: PropTypes.oneOf(TYPES),
  // Deprectation Warning: Use tabindex.
  kind: PropTypes.oneOf(KINDS),
  analyticstrackmessage: PropTypes.string,
};

Button.defaultProps = {
  [KIND_DICT.PRIMARY]: false,
  [KIND_DICT.SECONDARY]: false,
  [KIND_DICT.TERTIARY]: false,
  [PRESENTATION_DICT.DISABLED]: false,
  [PRESENTATION_DICT.INVERTED]: false,
  className: undefined,
  style: {},
  onClick: () => {},
  tabIndex: undefined,
  tapIndex: undefined,
  type: TYPES[0],
  kind: undefined,
  analyticstrackmessage: undefined,
};

export { TYPES };
export default Button;

If anyone has any ideas as to what may be causing this please feel free to comment or answer. Anything will be helpful.

Thank you in advance.

0

2 Answers 2

1

Create an object for tabIndex and then assign it to Button using spread operator.

const tabIndex = {tabIndex:this.props.tabIndex || this.props.tapIndex }

Button

<Button {...tabIndex}
  ...

  ...
>Hello</Button>
Sign up to request clarification or add additional context in comments.

Comments

0

I have found the issue after trying @RIYAJ KHAN`s suggestion.

I changed from:

  render () {
    const {
      className,
      children,
      ...props
    } = this.props;
    const { StyledButton } = this;

    return (
      <StyledButton
        className={cleanClassNames(className, BOOLEAN_PROPS)}
        tabIndex={this.tabIndex}
        innerRef={el => {this.buttonRef = el}}
        {...props}
      >
        {children}
      </StyledButton>
    );
  }

To:

  render () {
    const {
      className,
      children,
      tabIndex,
      tapIndex,
      ...props
    } = this.props;
    const { StyledButton } = this;
    const tabIdx = { tabIndex: tapIndex || tabIndex || 0 };

    return (
      <StyledButton
        className={cleanClassNames(className, BOOLEAN_PROPS)}
        innerRef={el => {this.buttonRef = el}}
        {...tabIdx}
        {...props}
      >
        {children}
      </StyledButton>
    );
  }
}

Then slowly worked my way back to broken render method.

What I noticed is that in the latest example tapIndex and tabIndex are excluded in the ...props rest expression which is added to StyledButton after my tabIndex={...}.

What I have determined is that when these properties are not excluded they somehow end up overwriting the tabIndex that I'm adding. So what is being passed to React.createElement is:

React.createElement(
  StyledButton,
  { ..., tabIndex: undefined, tapIndex: N },
  children
)

Not the tabIndex that I'm setting on the returned component.

My final solution is—render method only:

  render () {
    const {
      className,
      children,
      tabIndex,
      tapIndex,
      ...props
    } = this.props;
    const { StyledButton } = this;

    return (
      <StyledButton
        className={cleanClassNames(className, BOOLEAN_PROPS)}
        tabIndex={tabIndex || tapIndex || 0}
        innerRef={el => {this.buttonRef = el}}
        {...props}
      >
        {children}
      </StyledButton>
    );
  }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.