diff --git a/package-lock.json b/package-lock.json index a1eeb7a..26b84ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@robotsandpencils/react-robits", - "version": "1.0.3", + "version": "1.1.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5681,6 +5681,7 @@ "version": "7.29.6", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.6.tgz", "integrity": "sha512-vzTsAXa439ptdvav/4lsKRcGpAQX7b6wBIqia7+iNzqGJ5zjswApxA6jDAsexrc6ue9krWcbh8o+LYkBXW+GCQ==", + "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5696,6 +5697,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5707,6 +5709,7 @@ "version": "5.11.9", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz", "integrity": "sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2", "@types/testing-library__jest-dom": "^5.9.1", @@ -5722,6 +5725,7 @@ "version": "11.2.5", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.5.tgz", "integrity": "sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ==", + "dev": true, "requires": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^7.28.1" @@ -5731,6 +5735,7 @@ "version": "12.7.3", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.7.3.tgz", "integrity": "sha512-IdSHkWfbeSSJRFlldvHDWfVX0U18TbXIvLSGII+JbqkJrsflFr4OWlQIua0TvcVVJNna3BNrNvRSvpQ0yvSXlA==", + "dev": true, "requires": { "@babel/runtime": "^7.12.5" } @@ -5743,7 +5748,8 @@ "@types/aria-query": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz", - "integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==" + "integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==", + "dev": true }, "@types/babel__core": { "version": "7.1.12", @@ -5858,6 +5864,7 @@ "version": "26.0.20", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz", "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==", + "dev": true, "requires": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" @@ -6013,6 +6020,7 @@ "version": "5.9.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", "integrity": "sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==", + "dev": true, "requires": { "@types/jest": "*" } @@ -8625,6 +8633,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9456,6 +9465,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, "requires": { "inherits": "^2.0.4", "source-map": "^0.6.1", @@ -9580,7 +9590,8 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=" + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true }, "cssdb": { "version": "4.4.0", @@ -10111,7 +10122,8 @@ "dom-accessibility-api": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz", - "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==" + "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", + "dev": true }, "dom-converter": { "version": "0.2.0", @@ -16146,7 +16158,8 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=" + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true }, "magic-string": { "version": "0.25.7", @@ -16612,7 +16625,8 @@ "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true }, "mini-css-extract-plugin": { "version": "0.11.3", @@ -17766,7 +17780,6 @@ "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", - "dev": true, "requires": { "process": "^0.11.1", "util": "^0.10.3" @@ -17775,14 +17788,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, "requires": { "inherits": "2.0.3" } @@ -21473,6 +21484,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "requires": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -23339,6 +23351,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, "requires": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0" @@ -23863,6 +23876,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, "requires": { "min-indent": "^1.0.0" } diff --git a/src/core/components/tabs/Tabs.js b/src/core/components/tabs/Tabs.js index f853ee0..12731d8 100644 --- a/src/core/components/tabs/Tabs.js +++ b/src/core/components/tabs/Tabs.js @@ -1,9 +1,42 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useCallback } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' import { KEYCODES } from '../../constants/constants' import ThemeWrapper from '../../utils/ThemeWrapper' +const Tab = ({ styling, isActive, item, onTabChange, ...rest }) => { + const tabStyle = classNames( + styling.tab, + isActive && styling.active, + !item.enabled && styling.disabled + ) + + const handleClick = useCallback(() => { + onTabChange(item) + }, [onTabChange]) + + const handleKeyPress = useCallback( + event => { + if ([KEYCODES.SPACE, KEYCODES.ENTER].includes(event.charCode)) { + event.preventDefault() + onTabChange(item) + } + }, + [onTabChange] + ) + + return ( +
  • + {item.label} +
  • + ) +} + export const Tabs = ({ activeTab, className = '', @@ -13,55 +46,47 @@ export const Tabs = ({ styling, ...rest }) => { + // The initial selected tab is either `activeTab`, `defaultActiveTab`, or undefined (unset), evaluated in that order const [selectedTab, setSelectedTab] = useState(activeTab || defaultActiveTab) + /* When `activeTab` prop changes, update the selected tab */ useEffect(() => { - const selectedTab = options.find(i => i.label === (activeTab || defaultActiveTab)) - if (selectedTab) { - setSelectedTab(selectedTab.label) + if (activeTab) { + setSelectedTab(activeTab) } - }, [activeTab, defaultActiveTab, options]) + }, [activeTab]) const containerStyle = classNames(className, styling.container) - const handleKeyPress = (e, item) => { - if ([KEYCODES.SPACE, KEYCODES.ENTER].includes(e.charCode)) { - e.preventDefault() - setSelectedTab(item) - } - } - - const Tab = ({ item }) => { - const tabStyle = classNames( - styling.tab, - selectedTab === item.label && styling.active, - !item.enabled && styling.disabled - ) - - return ( -
  • { - if (item.enabled) { - setSelectedTab(activeTab || item.label) - if (onChangeCallback) { - onChangeCallback(activeTab || item.label) - } - } - }} - onKeyPress={e => handleKeyPress(e, item.label)} - {...rest}> - {item.label} -
  • - ) - } + const handleTabChange = useCallback( + item => { + if (item.enabled) { + /* When `activeTab` prop is provided, it is a controlled component; let the parent + component handle the change. Otherwise, update the active tab internally */ + if (!activeTab) { + setSelectedTab(item?.label) + } + + if (onChangeCallback) { + onChangeCallback(item?.label) + } + } + }, + [activeTab, onChangeCallback] + ) return (
    @@ -70,7 +95,7 @@ export const Tabs = ({ Tabs.propTypes = { /** - * Optional property that makes this a controlled component + * Optional tab option label property that makes this a controlled component */ activeTab: PropTypes.string, @@ -80,7 +105,7 @@ Tabs.propTypes = { className: PropTypes.string, /** - * Optional property that sets the initial default value, but leaves the component uncontrolled + * Optional tab option label property that sets the initial default value, but leaves the component uncontrolled */ defaultActiveTab: PropTypes.string, diff --git a/src/stories/Tabs.stories.js b/src/stories/Tabs.stories.js index 2989750..d519c45 100644 --- a/src/stories/Tabs.stories.js +++ b/src/stories/Tabs.stories.js @@ -1,18 +1,26 @@ import React from 'react' -import { useState } from '@storybook/client-api' +import { useState, useEffect } from '@storybook/client-api' import Tabs, { Tabs as TabsComponent } from '../core/components/tabs/Tabs' +import FormCheckbox from '../core/components/formCheckbox/FormCheckbox' export default { title: 'Robits/Tabs', component: TabsComponent } +const tabOptions = [ + { label: 'Tab 1', enabled: true }, + { label: 'Tab 2', enabled: true } +] + export const Normal = ({ themeName }) => { - const tabOptions = [ - { label: 'Tab 1', enabled: true }, - { label: 'Tab 2', enabled: true } - ] const [activeTab, setActiveTab] = useState(tabOptions[0].label) + const [isControlled, setIsControlled] = useState(false) + + // Reinitialize tab state + useEffect(() => { + setActiveTab(tabOptions[0].label) + }, [isControlled]) const handleTabChange = tab => { if (tab) { @@ -22,12 +30,33 @@ export const Normal = ({ themeName }) => { return ( <> - + id='is-controlled-checkbox' + toggle + onChange={() => { + setIsControlled(!isControlled) + }} + checked={isControlled}> + Is Controlled + + {isControlled ? ( + + ) : ( + + )} {activeTab === tabOptions[0].label ? (

    {tabOptions[0].label} Content

    ) : (