diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 7c84d3c9..38edc63d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -46,7 +46,7 @@ jobs: -Dsonar.projectKey=TykTechnologies_tyk-ui -Dsonar.sources=./src -Dsonar.coverage.exclusions=cypress/**/*.js,**/*.test.js,src/form/components/Combobox/*.js,src/form/redux-form/**/*.js - -Dsonar.cpd.exclusions=**/*.test.js,src/form/redux-form/**/* + -Dsonar.cpd.exclusions=**/*.test.js,src/form/redux-form/**/*,src/common/fonts -Dsonar.test.inclusions=**/*.test.js -Dsonar.tests=./src -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index a7a8e0d0..ae1c83e6 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -24,7 +24,7 @@ jobs: -Dsonar.projectKey=TykTechnologies_tyk-ui -Dsonar.sources=./src -Dsonar.coverage.exclusions=cypress/**/*.js,**/*.test.js,src/form/components/Combobox/*.js,src/form/redux-form/**/*.js - -Dsonar.cpd.exclusions=**/*.test.js,src/form/redux-form/**/* + -Dsonar.cpd.exclusions=**/*.test.js,src/form/redux-form/**/*,src/common/fonts -Dsonar.test.inclusions=**/*.test.js -Dsonar.tests=./src -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/src/form/components/Toggle/Toggle.test.js b/src/form/components/Toggle/Toggle.test.js index a5460516..fd0fbea3 100644 --- a/src/form/components/Toggle/Toggle.test.js +++ b/src/form/components/Toggle/Toggle.test.js @@ -1,7 +1,201 @@ +import React, { useState } from 'react'; import Toggle from './index'; +// eslint-disable-next-line react/prop-types +function Component({ children, ...rest }) { + const [active, setActive] = useState('option1'); + return ( + + {children || ( + + )} + + ); +} + +const classes = { + disabled: 'tyk-toggle--disabled-true', + labelWidth: 'tyk-form-group--label-has-width', + separated: 'tyk-toggle__list--separated', + onDark: 'tyk-toggle--on-dark', + active: 'tyk-toggle__item--active', +}; + +const selectors = { + component: '.tyk-toggle', + list: '.tyk-toggle__list', + error: '.tyk-form-control__error-message', + note: '.tyk-form-control__help-block', + item: '.tyk-toggle__item', +}; + describe('Toggle', () => { - it('TODO', () => { - expect(true).to.equal(true); + it('renders the component', () => { + cy.mount() + .get(selectors.component) + .should('exist'); + }); + + it('can use className and/or wrapperClassName to pass css classes to the component', () => { + const className = 'my-class'; + const wrapperClassName = 'my-wrapper-class'; + cy.mount() + .get(selectors.component) + .should('have.class', className) + .and('have.class', wrapperClassName); + }); + + it('can be in the disabled state', () => { + cy.mount() + .get(selectors.component) + .should('have.class', classes.disabled); + }); + + it('in readOnly mode the component is disabled', () => { + cy.mount() + .get(selectors.component) + .should('have.class', classes.disabled); + }); + + it('can render with error', () => { + const error = 'my error'; + cy.mount() + .get(selectors.error) + .should('have.text', error); + }); + + it('can set a custom label', () => { + const label = 'my label'; + cy.mount() + .get(selectors.component) + .find('label') + .should('contain', label); + }); + + it('can customize the label width', () => { + const labelWidth = '100px'; + cy.mount( + <> + + + , + ) + .get(selectors.component) + .find('label') + .should('have.css', 'width', labelWidth); + }); + + it('can set a theme', () => { + const theme = 'my-theme'; + cy.mount() + .get(selectors.component) + .should('have.class', `tyk-toggle--${theme}`); + }); + + it('can have multiple items', () => { + cy.mount( + + + + , + ) + .get(selectors.item) + .should('have.length', 2); + }); + + it('can specify a size', () => { + const size = 'lg'; + cy.mount() + .get(selectors.component) + .should('have.class', `tyk-toggle--${size}`); + }); + + it('items can be separated', () => { + cy.mount( + + + + , + ) + .get(selectors.list) + .should('have.class', classes.separated); + }); + + it('can specify a direction', () => { + const direction = 'column'; + cy.mount( + + + + , + ) + .get(selectors.component) + .should('have.class', `tyk-toggle--${direction}`); + }); + + it('can display on dark backgrounds', () => { + cy.mount() + .get(selectors.component) + .should('have.class', classes.onDark); + }); + + it('calls the onChange callback when an item is clicked', () => { + const onChange = cy.stub().as('onChange'); + cy.mount() + .get(selectors.item) + .click() + .get('@onChange') + .should('be.called'); + }); + + it('an item can be selected/active', () => { + cy.mount( + + + + , + ) + .get(selectors.item) + .eq(1) + .click() + .should('have.class', classes.active); }); }); diff --git a/src/form/components/Toggle/index.js b/src/form/components/Toggle/index.js index 9eed5513..b1b932df 100644 --- a/src/form/components/Toggle/index.js +++ b/src/form/components/Toggle/index.js @@ -1,191 +1,152 @@ -import React, { Component, createRef } from 'react'; +import React, { + useCallback, useMemo, useRef, useState, +} from 'react'; import PropTypes from 'prop-types'; import ToggleContext from './js/ToggleContext'; import ToggleItemWrapper from './js/ToggleItemWrapper'; -class Toggle extends Component { - static propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - PropTypes.element, - PropTypes.string, - ]), - className: PropTypes.string, - disabled: PropTypes.bool, - readOnly: PropTypes.bool, - error: PropTypes.string, - onChange: PropTypes.func, - label: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - PropTypes.element, - PropTypes.func, - PropTypes.string, - ]), - labelwidth: PropTypes.string, - theme: PropTypes.string, - type: PropTypes.string, // single || multiple - size: PropTypes.string, - separated: PropTypes.bool, - direction: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), - onDark: PropTypes.bool, - wrapperClassName: PropTypes.string, - }; - - static defaultProps = { - separated: false, - theme: 'primary', - type: 'single', - direction: 'row', - }; - - state = { - selectedRef: null, - } - - constructor(props) { - super(props); - - this.notchRef = createRef(); - this.toggleRef = createRef(); - } - - onItemSelected(value, event) { - const { onChange } = this.props; - - if (onChange) { - onChange(value, event); - } - } - - getCssClasses() { - const { - className, - disabled, - readOnly, - size, - theme, - direction, - onDark, - wrapperClassName = '', - } = this.props; - - let cssClasses = [ - wrapperClassName, - 'tyk-toggle', - `tyk-toggle--disabled-${readOnly || disabled}`, - `tyk-toggle--${size || 'md'}`, - `tyk-toggle--${theme}`, - `tyk-toggle--${direction}`, - ]; - - if (onDark) { - cssClasses.push('tyk-toggle--on-dark'); - } - - if (className) { - cssClasses = cssClasses.concat(className.split(' ')); - } - - return cssClasses.join(' '); - } - - getLabelStyles() { - const { labelwidth } = this.props; - const styles = {}; - - if (labelwidth) { - styles.flexBasis = labelwidth; - } - - return styles; - } - - saveSelectedRef(ref) { - this.setState({ - selectedRef: ref, - }); - } - - positionNotch() { - const { separated } = this.props; - const { selectedRef } = this.state; - +function Toggle({ + className, + disabled, + readOnly, + size, + theme, + direction, + onDark, + wrapperClassName = '', + onChange, + labelwidth, + label, + separated, + children, + type, + value, + error, +}) { + const [selectedRef, setSelectedRef] = useState(null); + const notchRef = useRef(); + const toggleRef = useRef(); + + const classes = [ + wrapperClassName, + className, + 'tyk-toggle', + `tyk-toggle--disabled-${readOnly || disabled}`, + `tyk-toggle--${size || 'md'}`, + `tyk-toggle--${theme}`, + `tyk-toggle--${direction}`, + onDark && 'tyk-toggle--on-dark', + ].filter(Boolean).join(' '); + + const onItemSelected = useCallback((itemValue, event) => { + if (!onChange) return; + onChange(itemValue, event); + }, [onChange]); + + const getLabelStyles = useCallback(() => { + if (labelwidth) return { flexBasis: labelwidth }; + return {}; + }, [labelwidth]); + + const positionNotch = useCallback(() => { if (!selectedRef || separated) { return {}; } const selectedWidth = selectedRef.current.offsetWidth; const selectedOffset = selectedRef.current.getBoundingClientRect().left; - const toggleOffset = this.toggleRef.current.getBoundingClientRect().left; + const toggleOffset = toggleRef.current.getBoundingClientRect().left; const left = selectedOffset - toggleOffset; return { left: `${left + 4}px`, width: `${selectedWidth - 8}px`, }; - } - - render() { - const { - children, - disabled, - readOnly, - label, - type, - separated, - value, - error, - } = this.props; - - return ( - <> -
- + }, [selectedRef, separated]); + + const contextValue = useMemo(() => ({ + disabled, + readOnly, + onItemSelected, + saveSelectedRef: setSelectedRef, + separated, + type, + value, + }), [disabled, readOnly, onItemSelected, separated, type, value]); + + return ( + <> +
+ + { + label + ? + : null + } +
    + { children } { - label - ? + type === 'multiple' && !separated + ?
  • : null } -
      - { children } - { - type === 'multiple' && !separated - ?
    • - : null - } -
    - -
- { - error && ( -

- { error } -

- ) - } - - ); - } + +
+
+ { + error && ( +

+ { error } +

+ ) + } + + ); } +Toggle.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.element, + PropTypes.string, + ]), + className: PropTypes.string, + disabled: PropTypes.bool, + readOnly: PropTypes.bool, + error: PropTypes.string, + onChange: PropTypes.func, + label: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.element, + PropTypes.func, + PropTypes.string, + ]), + labelwidth: PropTypes.string, + theme: PropTypes.string, + type: PropTypes.string, // single || multiple + size: PropTypes.string, + separated: PropTypes.bool, + direction: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + ]), + onDark: PropTypes.bool, + wrapperClassName: PropTypes.string, +}; + +Toggle.defaultProps = { + separated: false, + theme: 'primary', + type: 'single', + direction: 'row', +}; + Toggle.Item = ToggleItemWrapper; + export default Toggle; diff --git a/src/form/components/Toggle/js/ToggleItem.js b/src/form/components/Toggle/js/ToggleItem.js index 3a86f25e..57d5c5ee 100644 --- a/src/form/components/Toggle/js/ToggleItem.js +++ b/src/form/components/Toggle/js/ToggleItem.js @@ -1,24 +1,9 @@ import React, { Component, createRef } from 'react'; import PropTypes from 'prop-types'; -export default class ToggleItem extends Component { - static propTypes = { - context: PropTypes.instanceOf(Object), - label: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - PropTypes.element, - PropTypes.func, - PropTypes.string, - ]), - name: PropTypes.string, - value: PropTypes.string, - }; - +class ToggleItem extends Component { static getNotchCssClasses(context) { - const cssClasses = ['tyk-toggle__item-notch', `tyk-toggle__item-notch--${context.type}`]; - - return cssClasses.join(' '); + return ['tyk-toggle__item-notch', `tyk-toggle__item-notch--${context.type}`].join(' '); } constructor(props) { @@ -83,3 +68,18 @@ export default class ToggleItem extends Component { ); } } + +ToggleItem.propTypes = { + context: PropTypes.instanceOf(Object), + label: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.element, + PropTypes.func, + PropTypes.string, + ]), + name: PropTypes.string, + value: PropTypes.string, +}; + +export default ToggleItem; diff --git a/src/form/components/Toggle/js/ToggleItemWrapper.js b/src/form/components/Toggle/js/ToggleItemWrapper.js index 530a54d3..27340be3 100644 --- a/src/form/components/Toggle/js/ToggleItemWrapper.js +++ b/src/form/components/Toggle/js/ToggleItemWrapper.js @@ -6,12 +6,11 @@ import ToggleItem from './ToggleItem'; const ToggleItemWrapper = React.forwardRef((props, ref) => ( - {context => ( + {(context) => ( {props.children} - ) - } + )} ));