diff --git a/config/jest-config-carbon/package.json b/config/jest-config-carbon/package.json index d09c734edce7..c502eb66561f 100644 --- a/config/jest-config-carbon/package.json +++ b/config/jest-config-carbon/package.json @@ -2,7 +2,7 @@ "name": "jest-config-carbon", "private": true, "description": "Jest configuration and preset for Carbon", - "version": "0.10.0-rc.0", + "version": "0.10.0", "license": "Apache-2.0", "main": "index.js", "repository": { diff --git a/packages/carbon-react/package.json b/packages/carbon-react/package.json index c9a2c13f3c9e..d44c56ce5c86 100644 --- a/packages/carbon-react/package.json +++ b/packages/carbon-react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/react", "description": "React components for the Carbon Design System", - "version": "0.9.0-rc.0", + "version": "0.9.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -43,11 +43,11 @@ }, "dependencies": { "@carbon/feature-flags": "^0.6.0", - "@carbon/icons-react": "^10.43.0-rc.0", - "@carbon/styles": "^0.9.0-rc.0", + "@carbon/icons-react": "^10.43.0", + "@carbon/styles": "^0.9.0", "@carbon/telemetry": "0.0.0-alpha.6", - "carbon-components": "^10.48.0-rc.0", - "carbon-components-react": "^7.48.0-rc.0", + "carbon-components": "^10.48.0", + "carbon-components-react": "^7.48.0", "carbon-icons": "^7.0.7" }, "devDependencies": { @@ -58,7 +58,7 @@ "@babel/plugin-transform-react-constant-elements": "^7.14.5", "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.14.5", - "@carbon/themes": "^10.47.0-rc.0", + "@carbon/themes": "^10.47.0", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-commonjs": "^18.0.0", "@rollup/plugin-node-resolve": "^11.2.1", @@ -91,6 +91,8 @@ "webpack": "^4.41.5" }, "sideEffects": [ + "es/index.js", + "lib/index.js", "es/feature-flags.js", "lib/feature-flags.js", "es/prefix.js", diff --git a/packages/carbon-react/scss/components/stack/_index.scss b/packages/carbon-react/scss/components/stack/_index.scss new file mode 100644 index 000000000000..fb8a6cf6a378 --- /dev/null +++ b/packages/carbon-react/scss/components/stack/_index.scss @@ -0,0 +1,9 @@ +// Code generated by @carbon/react. DO NOT EDIT. +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward '@carbon/styles/scss/components/stack'; diff --git a/packages/carbon-react/tasks/build-styles.js b/packages/carbon-react/tasks/build-styles.js index 97715e8e7e6c..21c0c3dbca5c 100644 --- a/packages/carbon-react/tasks/build-styles.js +++ b/packages/carbon-react/tasks/build-styles.js @@ -367,6 +367,16 @@ async function build() { }, ], }, + { + type: 'directory', + filepath: 'stack', + files: [ + { + type: 'file', + filepath: '_index.scss', + }, + ], + }, { type: 'directory', filepath: 'structured-list', diff --git a/packages/components/package.json b/packages/components/package.json index de9ee7bff26d..5127c7011449 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "carbon-components", "description": "The Carbon Design System is IBM’s open-source design system for products and experiences.", - "version": "10.48.0-rc.0", + "version": "10.48.0", "license": "Apache-2.0", "main": "umd/index.js", "module": "es/index.js", @@ -81,9 +81,9 @@ "@babel/preset-react": "^7.14.5", "@babel/runtime": "^7.14.6", "@carbon/cli": "^10.31.0", - "@carbon/elements": "^10.47.0-rc.0", - "@carbon/icons-handlebars": "^10.43.0-rc.0", - "@carbon/icons-react": "^10.43.0-rc.0", + "@carbon/elements": "^10.47.0", + "@carbon/icons-handlebars": "^10.43.0", + "@carbon/icons-react": "^10.43.0", "@carbon/test-utils": "^10.20.0", "@frctl/fractal": "^1.1.0", "@rollup/plugin-babel": "^5.3.0", diff --git a/packages/elements/package.json b/packages/elements/package.json index d1efabaeba8d..2ea06f63ca13 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/elements", "description": "A collection of design elements in code for the IBM Design Language", - "version": "10.47.0-rc.0", + "version": "10.47.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -37,11 +37,11 @@ "dependencies": { "@carbon/colors": "^10.34.0", "@carbon/grid": "^10.39.0", - "@carbon/icons": "^10.43.0-rc.0", + "@carbon/icons": "^10.43.0", "@carbon/import-once": "^10.6.0", "@carbon/layout": "^10.34.0", "@carbon/motion": "^10.26.0", - "@carbon/themes": "^10.47.0-rc.0", + "@carbon/themes": "^10.47.0", "@carbon/type": "^10.38.0" }, "devDependencies": { diff --git a/packages/icons-handlebars/package.json b/packages/icons-handlebars/package.json index cc0d378f449a..fb79aa672fd5 100644 --- a/packages/icons-handlebars/package.json +++ b/packages/icons-handlebars/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-handlebars", "description": "Handlebars helpers for IBM Design Language icons in digital and software products using the Carbon Design System", - "version": "10.43.0-rc.0", + "version": "10.43.0", "license": "Apache-2.0", "main": "index.js", "repository": { @@ -27,7 +27,7 @@ }, "dependencies": { "@carbon/icon-helpers": "^10.25.0", - "@carbon/icons": "^10.43.0-rc.0" + "@carbon/icons": "^10.43.0" }, "devDependencies": { "handlebars": "^4.0.12" diff --git a/packages/icons-react/package.json b/packages/icons-react/package.json index 77e54ed28c6c..43f0824ce62c 100644 --- a/packages/icons-react/package.json +++ b/packages/icons-react/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-react", "description": "React components for icons in digital and software products using the Carbon Design System", - "version": "10.43.0-rc.0", + "version": "10.43.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -44,7 +44,7 @@ }, "devDependencies": { "@carbon/icon-build-helpers": "^0.30.0", - "@carbon/icons": "^10.43.0-rc.0", + "@carbon/icons": "^10.43.0", "rimraf": "^3.0.2" }, "sideEffects": false diff --git a/packages/icons-vue/package.json b/packages/icons-vue/package.json index 05f9edbdd2f7..8f6cde300309 100644 --- a/packages/icons-vue/package.json +++ b/packages/icons-vue/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons-vue", "description": "Vue components for icons in digital and software products using the Carbon Design System", - "version": "10.43.0-rc.0", + "version": "10.43.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -32,7 +32,7 @@ }, "devDependencies": { "@carbon/cli-reporter": "^10.5.0", - "@carbon/icons": "^10.43.0-rc.0", + "@carbon/icons": "^10.43.0", "fs-extra": "^8.1.0", "prettier": "^2.2.1", "rimraf": "^3.0.0", diff --git a/packages/icons/package.json b/packages/icons/package.json index 21ef1902e201..aa496825b40b 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/icons", "description": "Icons for digital and software products using the Carbon Design System", - "version": "10.43.0-rc.0", + "version": "10.43.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", diff --git a/packages/react/package.json b/packages/react/package.json index 1bd7a0726ce9..a2ba61014041 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "carbon-components-react", "description": "The Carbon Design System is IBM’s open-source design system for products and experiences.", - "version": "7.48.0-rc.0", + "version": "7.48.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", @@ -48,7 +48,7 @@ "dependencies": { "@babel/runtime": "^7.14.6", "@carbon/feature-flags": "^0.6.0", - "@carbon/icons-react": "^10.43.0-rc.0", + "@carbon/icons-react": "^10.43.0", "@carbon/layout": "^10.34.0", "@carbon/telemetry": "0.0.0-alpha.6", "classnames": "2.3.1", @@ -103,7 +103,7 @@ "babel-plugin-react-docgen": "^4.2.1", "babel-plugin-transform-inline-environment-variables": "^0.4.3", "browserslist-config-carbon": "^10.6.1", - "carbon-components": "^10.48.0-rc.0", + "carbon-components": "^10.48.0", "carbon-icons": "^7.0.5", "chalk": "^4.1.1", "cli-table": "^0.3.0", diff --git a/packages/react/src/components/ComposedModal/index.js b/packages/react/src/components/ComposedModal/index.js index c8a5f7b8d15a..ab0019e7c102 100644 --- a/packages/react/src/components/ComposedModal/index.js +++ b/packages/react/src/components/ComposedModal/index.js @@ -7,22 +7,26 @@ import * as FeatureFlags from '@carbon/feature-flags'; import { ModalHeader as ModalHeaderNext } from './next/ModalHeader'; +import { ModalFooter as ModalFooterNext } from './next/ModalFooter'; import { default as ComposedModalNext } from './next/ComposedModal'; import { default as ComposedModalClassic, ModalHeader as ModalHeaderClassic, ModalBody, - ModalFooter, + ModalFooter as ModalFooterClassic, } from './ComposedModal'; export const ModalHeader = FeatureFlags.enabled('enable-v11-release') ? ModalHeaderNext : ModalHeaderClassic; +export const ModalFooter = FeatureFlags.enabled('enable-v11-release') + ? ModalFooterNext + : ModalFooterClassic; + export const ComposedModal = FeatureFlags.enabled('enable-v11-release') ? ComposedModalNext : ComposedModalClassic; -export { ModalBody, ModalFooter }; - +export { ModalBody }; export default from './ComposedModal'; diff --git a/packages/react/src/components/ComposedModal/next/ComposedModal.js b/packages/react/src/components/ComposedModal/next/ComposedModal.js index 7f1c77230bf5..c8a2c9eec095 100644 --- a/packages/react/src/components/ComposedModal/next/ComposedModal.js +++ b/packages/react/src/components/ComposedModal/next/ComposedModal.js @@ -3,19 +3,83 @@ import PropTypes from 'prop-types'; import { ModalHeader } from './ModalHeader'; import { ModalFooter } from '../ComposedModal'; -import classNames from 'classnames'; +import cx from 'classnames'; import toggleClass from '../../../tools/toggleClass'; +import requiredIfGivenPropIsTruthy from '../../../prop-types/requiredIfGivenPropIsTruthy'; import wrapFocus from '../../../internal/wrapFocus'; import { usePrefix } from '../../../internal/usePrefix'; +export function ModalBody({ + className: customClassName, + children, + hasForm, + hasScrollingContent, + ...rest +}) { + const prefix = usePrefix(); + const contentClass = cx({ + [`${prefix}--modal-content`]: true, + [`${prefix}--modal-content--with-form`]: hasForm, + [`${prefix}--modal-scroll-content`]: hasScrollingContent, + [customClassName]: customClassName, + }); + const hasScrollingContentProps = hasScrollingContent + ? { + tabIndex: 0, + role: 'region', + } + : {}; + return ( + <> +
+ {children} +
+ {hasScrollingContent && ( +
+ )} + + ); +} + +ModalBody.propTypes = { + /** + * Required props for the accessibility label of the header + */ + ['aria-label']: requiredIfGivenPropIsTruthy( + 'hasScrollingContent', + PropTypes.string + ), + + /** + * Specify the content to be placed in the ModalBody + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to the Modal Body node + */ + className: PropTypes.string, + + /** + * Provide whether the modal content has a form element. + * If `true` is used here, non-form child content should have `bx--modal-content__regular-content` class. + */ + hasForm: PropTypes.bool, + + /** + * Specify whether the modal contains scrolling content + */ + hasScrollingContent: PropTypes.bool, +}; + const ComposedModal = React.forwardRef(function ComposedModal( { ['aria-labelledby']: ariaLabelledBy, ['aria-label']: ariaLabel, children, - className, + className: customClassName, containerClassName, danger, onClose, @@ -88,14 +152,14 @@ const ComposedModal = React.forwardRef(function ComposedModal( } } - const modalClass = classNames({ + const modalClass = cx({ [`${prefix}--modal`]: true, 'is-visible': isOpen, - [className]: className, + [customClassName]: customClassName, [`${prefix}--modal--danger`]: danger, }); - const containerClass = classNames({ + const containerClass = cx({ [`${prefix}--modal-container`]: true, [`${prefix}--modal-container--${size}`]: size, [containerClassName]: containerClassName, diff --git a/packages/react/src/components/ComposedModal/next/ComposedModal.stories.js b/packages/react/src/components/ComposedModal/next/ComposedModal.stories.js index 089132f59999..b13a38867777 100644 --- a/packages/react/src/components/ComposedModal/next/ComposedModal.stories.js +++ b/packages/react/src/components/ComposedModal/next/ComposedModal.stories.js @@ -16,9 +16,9 @@ import { text, withKnobs, } from '@storybook/addon-knobs'; -import { ModalBody, ModalFooter } from '../ComposedModal'; -import ComposedModal from './ComposedModal'; +import ComposedModal, { ModalBody } from './ComposedModal'; import { ModalHeader } from './ModalHeader'; +import { ModalFooter } from './ModalFooter'; import Select from '../../Select'; import SelectItem from '../../SelectItem'; import TextInput from '../../TextInput'; @@ -120,8 +120,8 @@ const props = { false ), ...secondaryButtons(numberOfButtons), - onRequestClose: action('onRequestClose'), - onRequestSubmit: action('onRequestSubmit'), + onRequestClose: () => action('onRequestClose')(), + onRequestSubmit: () => action('onRequestSubmit')(), }; }, }; diff --git a/packages/react/src/components/ComposedModal/next/ModalFooter-test.js b/packages/react/src/components/ComposedModal/next/ModalFooter-test.js new file mode 100644 index 000000000000..748d66627157 --- /dev/null +++ b/packages/react/src/components/ComposedModal/next/ModalFooter-test.js @@ -0,0 +1,124 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import Button from '../../Button'; +import { ModalFooter } from './ModalFooter'; +import InlineLoading from '../../InlineLoading'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; + +describe('', () => { + describe('Renders as expected', () => { + const wrapper = mount( + +

Test

+
+ ); + + it('renders children as expected', () => { + expect(wrapper.find('p').length).toBe(1); + }); + + it('renders wrapper as expected', () => { + expect(wrapper.length).toBe(1); + }); + + it('renders extra classes passed in via className', () => { + expect(wrapper.hasClass('extra-class')).toEqual(true); + }); + }); + + describe('Should render buttons only if appropriate prop passed in', () => { + const wrapper = shallow( + +

Test

+
+ ); + + const primaryWrapper = shallow(); + const secondaryWrapper = mount(); + const multipleSecondaryWrapper = mount( + , + onClick: jest.fn(), + }, + { + buttonText: 'Cancel', + onClick: jest.fn(), + }, + ]} + /> + ); + + it('does not render primary button if no primary text', () => { + expect(wrapper.find(`.${prefix}--btn--primary`).exists()).toBe(false); + }); + + it('does not render secondary button if no secondary text', () => { + expect(wrapper.find(`.${prefix}--btn--secondary`).exists()).toBe(false); + }); + + it('renders primary button if primary text', () => { + const buttonComponent = primaryWrapper.find(Button); + expect(buttonComponent.exists()).toBe(true); + expect(buttonComponent.props().kind).toBe('primary'); + }); + + it('renders primary button if secondary text', () => { + const buttonComponent = secondaryWrapper.find(Button); + expect(buttonComponent.exists()).toBe(true); + expect(buttonComponent.props().kind).toBe('secondary'); + }); + + it('correctly renders multiple secondary buttons', () => { + const buttonComponents = multipleSecondaryWrapper.find(Button); + expect(buttonComponents.length).toEqual(2); + expect(buttonComponents.at(0).props().kind).toBe('secondary'); + expect(buttonComponents.at(1).props().kind).toBe('secondary'); + }); + }); + + describe('Should render the appropriate buttons when `danger` prop is true', () => { + const primaryWrapper = shallow( + + ); + const secondaryWrapper = mount( + + ); + const multipleSecondaryWrapper = mount( + , + onClick: jest.fn(), + }, + { + buttonText: 'Cancel', + onClick: jest.fn(), + }, + ]} + /> + ); + + it('renders danger button if primary text && danger', () => { + const buttonComponent = primaryWrapper.find(Button); + expect(buttonComponent.exists()).toBe(true); + expect(buttonComponent.props().kind).toBe('danger'); + }); + + it('renders secondary button if secondary text && danger', () => { + const buttonComponent = secondaryWrapper.find(Button); + expect(buttonComponent.exists()).toBe(true); + expect(buttonComponent.prop('kind')).toBe('secondary'); + }); + + it('correctly renders multiple secondary buttons', () => { + const buttonComponents = multipleSecondaryWrapper.find(Button); + expect(buttonComponents.length).toEqual(2); + expect(buttonComponents.at(0).props().kind).toBe('secondary'); + expect(buttonComponents.at(1).props().kind).toBe('secondary'); + }); + }); +}); diff --git a/packages/react/src/components/ComposedModal/next/ModalFooter.js b/packages/react/src/components/ComposedModal/next/ModalFooter.js new file mode 100644 index 000000000000..2665f92069b5 --- /dev/null +++ b/packages/react/src/components/ComposedModal/next/ModalFooter.js @@ -0,0 +1,241 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from '../../Button'; +import ButtonSet from '../../ButtonSet'; +import cx from 'classnames'; +import { usePrefix } from '../../../internal/usePrefix'; + +function SecondaryButtonSet({ + secondaryButtons, + secondaryButtonText, + secondaryClassName, + closeModal, + onRequestClose, +}) { + function handleRequestClose(evt) { + closeModal(evt); + onRequestClose(evt); + } + + if (Array.isArray(secondaryButtons) && secondaryButtons.length <= 2) { + return secondaryButtons.map(({ buttonText, onClick: onButtonClick }, i) => ( + + )); + } + if (secondaryButtonText) { + return ( + + ); + } + return null; +} + +SecondaryButtonSet.propTypes = { + closeModal: PropTypes.func, + onRequestClose: PropTypes.func, + secondaryButtonText: PropTypes.string, + secondaryButtons: (props, propName, componentName) => { + if (props.secondaryButtons) { + if ( + !Array.isArray(props.secondaryButtons) || + props.secondaryButtons.length !== 2 + ) { + return new Error( + `${propName} needs to be an array of two button config objects` + ); + } + + const shape = { + buttonText: PropTypes.node, + onClick: PropTypes.func, + }; + + props[propName].forEach((secondaryButton) => { + PropTypes.checkPropTypes( + shape, + secondaryButton, + propName, + componentName + ); + }); + } + + return null; + }, + secondaryClassName: PropTypes.string, +}; + +export function ModalFooter({ + children, + className: customClassName, + closeModal, + danger, + inputref, + onRequestClose, + onRequestSubmit, + primaryButtonDisabled, + primaryButtonText, + primaryClassName, + secondaryButtonText, + secondaryButtons, + secondaryClassName, + ...rest +}) { + const prefix = usePrefix(); + + const footerClass = cx({ + [`${prefix}--modal-footer`]: true, + [customClassName]: customClassName, + [`${prefix}--modal-footer--three-button`]: + Array.isArray(secondaryButtons) && secondaryButtons.length === 2, + }); + + const secondaryButtonProps = { + closeModal, + secondaryButtons, + secondaryButtonText, + secondaryClassName, + onRequestClose, + }; + + return ( + + + {primaryButtonText && ( + + )} + + {children} + + ); +} + +ModalFooter.propTypes = { + /** + * Pass in content that will be rendered in the Modal Footer + */ + children: PropTypes.node, + + /** + * Specify a custom className to be applied to the Modal Footer container + */ + className: PropTypes.string, + + /** + * Specify an optional function that is called whenever the modal is closed + */ + closeModal: PropTypes.func, + + /** + * Specify whether the primary button should be replaced with danger button. + * Note that this prop is not applied if you render primary/danger button by yourself + */ + danger: PropTypes.bool, + + /** + * The `ref` callback for the primary button. + */ + inputref: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + current: PropTypes.any, + }), + ]), + + /** + * Specify an optional function for when the modal is requesting to be + * closed + */ + onRequestClose: PropTypes.func, + + /** + * Specify an optional function for when the modal is requesting to be + * submitted + */ + onRequestSubmit: PropTypes.func, + + /** + * Specify whether the primary button should be disabled + */ + primaryButtonDisabled: PropTypes.bool, + + /** + * Specify the text for the primary button + */ + primaryButtonText: PropTypes.string, + + /** + * Specify a custom className to be applied to the primary button + */ + primaryClassName: PropTypes.string, + + /** + * Specify the text for the secondary button + */ + secondaryButtonText: PropTypes.string, + + /** + * Specify an array of config objects for secondary buttons + * (`Array<{ + * buttonText: string, + * onClick: function, + * }>`). + */ + secondaryButtons: (props, propName, componentName) => { + if (props.secondaryButtons) { + if ( + !Array.isArray(props.secondaryButtons) || + props.secondaryButtons.length !== 2 + ) { + return new Error( + `${propName} needs to be an array of two button config objects` + ); + } + + const shape = { + buttonText: PropTypes.node, + onClick: PropTypes.func, + }; + + props[propName].forEach((secondaryButton) => { + PropTypes.checkPropTypes( + shape, + secondaryButton, + propName, + componentName + ); + }); + } + + return null; + }, + + /** + * Specify a custom className to be applied to the secondary button + */ + secondaryClassName: PropTypes.string, +}; + +ModalFooter.defaultProps = { + onRequestClose: () => {}, + onRequestSubmit: () => {}, + closeModal: () => {}, +}; diff --git a/packages/react/src/components/ComposedModal/next/ModalHeader.js b/packages/react/src/components/ComposedModal/next/ModalHeader.js index 939d6f7131bb..f5ca4aa5899b 100644 --- a/packages/react/src/components/ComposedModal/next/ModalHeader.js +++ b/packages/react/src/components/ComposedModal/next/ModalHeader.js @@ -12,19 +12,18 @@ import { Close20 } from '@carbon/icons-react'; import { usePrefix } from '../../../internal/usePrefix'; export function ModalHeader({ - className, - labelClassName, - titleClassName, + buttonOnClick, + children, + className: customClassName, closeClassName, closeIconClassName, + closeModal, + iconDescription, label, + labelClassName, title, - children, - iconDescription, - closeModal, // eslint-disable-line - buttonOnClick, // eslint-disable-line - preventCloseOnClickOutside, // eslint-disable-line - ...other + titleClassName, + ...rest }) { const prefix = usePrefix(); @@ -35,7 +34,7 @@ export function ModalHeader({ const headerClass = cx({ [`${prefix}--modal-header`]: true, - [className]: className, + [customClassName]: customClassName, }); const labelClass = cx({ @@ -59,7 +58,7 @@ export function ModalHeader({ }); return ( -
+
{label &&

{label}

} {title &&

{title}

} @@ -140,4 +139,5 @@ ModalHeader.propTypes = { ModalHeader.defaultProps = { iconDescription: 'Close', buttonOnClick: () => {}, + closeModal: () => {}, }; diff --git a/packages/react/src/components/Modal/index.js b/packages/react/src/components/Modal/index.js index b312b576b811..f43cdebd7b8d 100644 --- a/packages/react/src/components/Modal/index.js +++ b/packages/react/src/components/Modal/index.js @@ -4,5 +4,13 @@ * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ +import * as FeatureFlags from '@carbon/feature-flags'; +import { default as ModalNext } from './next/Modal'; -export default from './Modal'; +import { default as ModalClassic } from './Modal'; + +const Modal = FeatureFlags.enabled('enable-v11-release') + ? ModalNext + : ModalClassic; + +export default Modal; diff --git a/packages/react/src/components/Modal/next/Modal-test.js b/packages/react/src/components/Modal/next/Modal-test.js new file mode 100644 index 000000000000..8b205f01eab2 --- /dev/null +++ b/packages/react/src/components/Modal/next/Modal-test.js @@ -0,0 +1,259 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { Close20 } from '@carbon/icons-react'; +import Modal from './Modal'; +import InlineLoading from '../../InlineLoading'; +import { mount } from 'enzyme'; + +const prefix = 'bx'; + +// The modal is the 0th child inside the wrapper on account of focus-trap-react +const getModal = (wrapper) => wrapper.find('.bx--modal'); +const getModalBody = (wrapper) => wrapper.find('.bx--modal-container'); + +describe('Modal', () => { + describe('Renders as expected', () => { + const wrapper = mount(); + const mounted = mount(); + + it('has the expected classes', () => { + expect(getModal(wrapper).hasClass(`${prefix}--modal`)).toEqual(true); + }); + + it('should add extra classes that are passed via className', () => { + expect(getModal(wrapper).hasClass('extra-class')).toEqual(true); + }); + + it('should not be a passive modal by default', () => { + expect(getModal(wrapper).hasClass(`${prefix}--modal-tall`)).toEqual(true); + }); + + it('should be a passive modal when passiveModal is passed', () => { + wrapper.setProps({ passiveModal: true }); + expect(getModal(wrapper).hasClass(`${prefix}--modal-tall`)).toEqual( + false + ); + }); + + it('should set id if one is passed via props', () => { + const modal = mount(); + expect(getModal(modal).props().id).toEqual('modal-1'); + }); + + it('should not place the svg icon in the accessibility tree', () => { + const ariaHidden = mounted.find(Close20).props()['aria-hidden']; + expect(ariaHidden).toEqual('true'); + }); + + it("icon isn't a focusable tab stop", () => { + const icon = mounted.find(Close20).props().tabIndex; + expect(icon).toEqual('-1'); + }); + + it('enables primary button by default', () => { + const primaryButton = mounted + .find(`.${prefix}--btn.${prefix}--btn--primary`) + .at(0); + expect(primaryButton.prop('disabled')).toEqual(false); + }); + + it('disables primary button when disablePrimaryButton prop is passed', () => { + mounted.setProps({ primaryButtonDisabled: true }); + const primaryButton = mounted + .find(`.${prefix}--btn.${prefix}--btn--primary`) + .at(0); + expect(primaryButton.props().disabled).toEqual(true); + }); + + it('Should have node in primary', () => { + mounted.setProps({ primaryButtonText: }); + const primaryButton = mounted + .find(`.${prefix}--btn.${prefix}--btn--primary`) + .at(0); + expect(primaryButton.find('InlineLoading').exists()).toEqual(true); + }); + + it('Should have node in secondary', () => { + mounted.setProps({ secondaryButtonText: }); + const secondaryButton = mounted + .find(`.${prefix}--btn.${prefix}--btn--secondary`) + .at(0); + expect(secondaryButton.find('InlineLoading').exists()).toEqual(true); + }); + }); + + describe('Renders as expected with secondaryButtons prop', () => { + const mounted = mount(); + + it('Should support node in secondary', () => { + mounted.setProps({ + secondaryButtons: [ + { + buttonText: , + onClick: jest.fn(), + }, + { + buttonText: 'Cancel', + onClick: jest.fn(), + }, + ], + }); + + const secondaryButtons = mounted.find( + `.${prefix}--btn.${prefix}--btn--secondary` + ); + expect(secondaryButtons.length).toEqual(2); + expect(secondaryButtons.at(0).find('InlineLoading').exists()).toEqual( + true + ); + expect(secondaryButtons.at(1).text()).toEqual('Cancel'); + }); + }); + + describe('Adds props as expected to the right children', () => { + it('should set label if one is passed via props', () => { + const wrapper = mount(); + const label = wrapper.find(`.${prefix}--modal-header__label`); + expect(label.props().children).toEqual('modal-1'); + }); + + it('should set modal heading if one is passed via props', () => { + const wrapper = mount(); + const heading = wrapper.find(`.${prefix}--modal-header__heading`); + expect(heading.props().children).toEqual('modal-1'); + }); + + it('should set button text if one is passed via props', () => { + const wrapper = mount( + + ); + const modalButtons = wrapper.find(`.${prefix}--modal-footer Button`); + expect(modalButtons.at(0).props().children).toEqual('Cancel'); + expect(modalButtons.at(1).props().children).toEqual('Submit'); + }); + }); + + describe('events', () => { + it('should handle close keyDown events', () => { + const onRequestClose = jest.fn(); + const wrapper = mount( + + ); + wrapper.simulate('keyDown', { which: 26 }); + expect(onRequestClose).not.toHaveBeenCalled(); + wrapper.simulate('keyDown', { which: 27 }); + expect(onRequestClose).toHaveBeenCalled(); + }); + + it('should handle submit keyDown events with shouldSubmitOnEnter enabled', () => { + const onRequestSubmit = jest.fn(); + const wrapper = mount( + + ); + wrapper.simulate('keyDown', { which: 14 }); + expect(onRequestSubmit).not.toHaveBeenCalled(); + wrapper.simulate('keyDown', { which: 13 }); + expect(onRequestSubmit).toHaveBeenCalled(); + }); + + it('should not handle submit keyDown events with shouldSubmitOnEnter not enabled', () => { + const onRequestSubmit = jest.fn(); + const wrapper = mount( + + ); + wrapper.simulate('keyDown', { which: 14 }); + expect(onRequestSubmit).not.toHaveBeenCalled(); + wrapper.simulate('keyDown', { which: 13 }); + expect(onRequestSubmit).not.toHaveBeenCalled(); + }); + + it('should close by default on secondary button click', () => { + const onRequestClose = jest.fn(); + const modal = mount( + + ); + const secondaryBtn = modal.find(`.${prefix}--btn--secondary`); + secondaryBtn.simulate('click'); + expect(onRequestClose).toHaveBeenCalled(); + }); + + it('should handle custom secondary button events', () => { + const onSecondarySubmit = jest.fn(); + const modal = mount( + + ); + const secondaryBtn = modal.find(`.${prefix}--btn--secondary`); + secondaryBtn.simulate('click'); + expect(onSecondarySubmit).toHaveBeenCalled(); + }); + }); +}); + +describe('Danger Modal', () => { + describe('Renders as expected', () => { + const wrapper = mount( + + ); + + it('has the expected classes', () => { + expect(getModal(wrapper).hasClass(`${prefix}--modal--danger`)).toEqual( + true + ); + }); + + it('has correct button combination', () => { + const modalButtons = wrapper.find( + `.${prefix}--modal-footer.${prefix}--btn-set Button` + ); + expect(modalButtons.length).toEqual(2); + expect(modalButtons.at(0).props().kind).toEqual('secondary'); + expect(modalButtons.at(1).props().kind).toEqual('danger'); + }); + }); +}); +describe('Alert Modal', () => { + describe('Renders as expected', () => { + const wrapper = mount(); + + it('has the expected attributes', () => { + expect(getModalBody(wrapper).props()).toEqual( + expect.objectContaining({ + role: 'alertdialog', + 'aria-describedby': expect.any(String), + }) + ); + }); + + it('should be a passive modal when passiveModal is passed', () => { + wrapper.setProps({ passiveModal: true }); + expect(getModalBody(wrapper).props()).toEqual( + expect.objectContaining({ + role: 'alert', + }) + ); + }); + }); +}); diff --git a/packages/react/src/components/Modal/next/Modal.js b/packages/react/src/components/Modal/next/Modal.js new file mode 100644 index 000000000000..1e79723c21f7 --- /dev/null +++ b/packages/react/src/components/Modal/next/Modal.js @@ -0,0 +1,524 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React, { useRef, useEffect } from 'react'; +import classNames from 'classnames'; +import { Close20 } from '@carbon/icons-react'; +import toggleClass from '../../../tools/toggleClass'; +import Button from '../../Button'; +import ButtonSet from '../../ButtonSet'; +import deprecate from '../../../prop-types/deprecate'; +import requiredIfGivenPropIsTruthy from '../../../prop-types/requiredIfGivenPropIsTruthy'; +import wrapFocus, { + elementOrParentIsFloatingMenu, +} from '../../../internal/wrapFocus'; +import setupGetInstanceId from '../../../tools/setupGetInstanceId'; +import { usePrefix } from '../../../internal/usePrefix'; + +const getInstanceId = setupGetInstanceId(); + +const Modal = React.forwardRef(function Modal( + { + children, + className, + modalHeading, + modalLabel, + modalAriaLabel, + passiveModal, + hasForm, + secondaryButtonText, + primaryButtonText, + open, + onRequestClose, + onRequestSubmit, + onSecondarySubmit, + iconDescription, + primaryButtonDisabled, + danger, + alert, + secondaryButtons, + selectorPrimaryFocus, // eslint-disable-line + selectorsFloatingMenus, // eslint-disable-line + shouldSubmitOnEnter, // eslint-disable-line + size, + hasScrollingContent, + closeButtonLabel, + preventCloseOnClickOutside, // eslint-disable-line + ...rest + }, + ref +) { + const prefix = usePrefix(); + const button = useRef(); + const secondaryButton = useRef(); + const innerModal = useRef(); + const startTrap = useRef(); + const endTrap = useRef(); + const modalInstanceId = `modal-${getInstanceId()}`; + const modalLabelId = `${prefix}--modal-header__label--${modalInstanceId}`; + const modalHeadingId = `${prefix}--modal-header__heading--${modalInstanceId}`; + const modalBodyId = `${prefix}--modal-body--${modalInstanceId}`; + const modalCloseButtonClass = `${prefix}--modal-close`; + + function isCloseButton(element) { + return ( + (!onSecondarySubmit && element === secondaryButton.current) || + element.classList.contains(modalCloseButtonClass) + ); + } + + function handleKeyDown(evt) { + if (open) { + if (evt.which === 27) { + onRequestClose(evt); + } + if ( + evt.which === 13 && + shouldSubmitOnEnter && + !isCloseButton(evt.target) + ) { + onRequestSubmit(evt); + } + } + } + + function handleMousedown(evt) { + if ( + innerModal.current && + !innerModal.current.contains(evt.target) && + !elementOrParentIsFloatingMenu(evt.target, selectorsFloatingMenus) && + !preventCloseOnClickOutside + ) { + onRequestClose(evt); + } + } + + function handleBlur({ + target: oldActiveNode, + relatedTarget: currentActiveNode, + }) { + if (open && currentActiveNode && oldActiveNode) { + const { current: bodyNode } = innerModal; + const { current: startTrapNode } = startTrap; + const { current: endTrapNode } = endTrap; + wrapFocus({ + bodyNode, + startTrapNode, + endTrapNode, + currentActiveNode, + oldActiveNode, + selectorsFloatingMenus, + }); + } + } + + const onSecondaryButtonClick = onSecondarySubmit + ? onSecondarySubmit + : onRequestClose; + + const modalClasses = classNames(`${prefix}--modal`, { + [`${prefix}--modal-tall`]: !passiveModal, + 'is-visible': open, + [`${prefix}--modal--danger`]: danger, + [className]: className, + }); + + const containerClasses = classNames(`${prefix}--modal-container`, { + [`${prefix}--modal-container--${size}`]: size, + }); + + const contentClasses = classNames(`${prefix}--modal-content`, { + [`${prefix}--modal-content--with-form`]: hasForm, //TO-DO: deprecate & remove this with v11 + [`${prefix}--modal-scroll-content`]: hasScrollingContent, + }); + + const footerClasses = classNames(`${prefix}--modal-footer`, { + [`${prefix}--modal-footer--three-button`]: + Array.isArray(secondaryButtons) && secondaryButtons.length === 2, + }); + + const modalButton = ( + + ); + + const ariaLabel = + modalLabel || ['aria-label'] || modalAriaLabel || modalHeading; + const getAriaLabelledBy = modalLabel ? modalLabelId : modalHeadingId; + + const hasScrollingContentProps = hasScrollingContent + ? { + tabIndex: 0, + role: 'region', + 'aria-label': ariaLabel, + 'aria-labelledby': getAriaLabelledBy, + } + : {}; + + const alertDialogProps = {}; + if (alert && passiveModal) { + alertDialogProps.role = 'alert'; + } + if (alert && !passiveModal) { + alertDialogProps.role = 'alertdialog'; + alertDialogProps['aria-describedby'] = modalBodyId; + } + + useEffect(() => { + return () => { + toggleClass(document.body, `${prefix}--body--with-modal-open`, false); + }; + }, [prefix]); + + useEffect(() => { + toggleClass(document.body, `${prefix}--body--with-modal-open`, open); + }, [open, prefix]); + + useEffect(() => { + const initialFocus = (focusContainerElement) => { + const containerElement = focusContainerElement || innerModal.current; + const primaryFocusElement = containerElement + ? containerElement.querySelector(selectorPrimaryFocus) + : null; + + if (primaryFocusElement) { + return primaryFocusElement; + } + + return button && button.current; + }; + + const focusButton = (focusContainerElement) => { + const target = initialFocus(focusContainerElement); + if (target) { + target.focus(); + } + }; + + if (open) { + focusButton(innerModal.current); + } + }, [open, selectorPrimaryFocus]); + + const modalBody = ( +
+
+ {passiveModal && modalButton} + {modalLabel && ( +

+ {modalLabel} +

+ )} +

+ {modalHeading} +

+ {!passiveModal && modalButton} +
+
+ {children} +
+ {hasScrollingContent && ( +
+ )} + {!passiveModal && ( + + {Array.isArray(secondaryButtons) && secondaryButtons.length <= 2 + ? secondaryButtons.map( + ({ buttonText, onClick: onButtonClick }, i) => ( + + ) + ) + : secondaryButtonText && ( + + )} + + + )} +
+ ); + + return ( +
+ {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */} + + Focus sentinel + + {modalBody} + {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */} + + Focus sentinel + +
+ ); +}); + +Modal.propTypes = { + /** + * Specify whether the Modal is displaying an alert, error or warning + * Should go hand in hand with the danger prop. + */ + alert: PropTypes.bool, + + /** + * Required props for the accessibility label of the header + */ + ['aria-label']: requiredIfGivenPropIsTruthy( + 'hasScrollingContent', + PropTypes.string + ), + + /** + * Provide the contents of your Modal + */ + children: PropTypes.node, + + /** + * Specify an optional className to be applied to the modal root node + */ + className: PropTypes.string, + + /** + * Specify an label for the close button of the modal; defaults to close + */ + closeButtonLabel: PropTypes.string, + + /** + * Specify whether the Modal is for dangerous actions + */ + danger: PropTypes.bool, + + /** + * Deprecated; Used for advanced focus-wrapping feature using 3rd party library, + * but it's now achieved without a 3rd party library. + */ + focusTrap: deprecate( + PropTypes.bool, + `\nThe prop \`focusTrap\` for Modal has been deprecated, as the feature of \`focusTrap\` runs by default.` + ), + + /** + * Deprecated: Used to determine whether the modal content has a form element to adjust spacing, + * but now spacing styles account for all types of elements + */ + hasForm: deprecate( + PropTypes.bool, + `\nThe prop \`hasForm\` for Modal has been deprecated, as the feature of \`hasForm\` runs by default.` + ), + + /** + * Specify whether the modal contains scrolling content + */ + hasScrollingContent: PropTypes.bool, + + /** + * Provide a description for "close" icon that can be read by screen readers + */ + iconDescription: deprecate( + PropTypes.string, + 'The iconDescription prop is no longer needed and can be safely removed. This prop will be removed in the next major release of Carbon.' + ), + + /** + * Specify the DOM element ID of the top-level node. + */ + id: PropTypes.string, + + /** + * Specify a label to be read by screen readers on the modal root node + */ + modalAriaLabel: PropTypes.string, + + /** + * Specify the content of the modal header title. + */ + modalHeading: PropTypes.node, + + /** + * Specify the content of the modal header label. + */ + modalLabel: PropTypes.node, + + /** + * Specify a handler for keypresses. + */ + onKeyDown: PropTypes.func, + + /** + * Specify a handler for closing modal. + * The handler should care of closing modal, e.g. changing `open` prop. + */ + onRequestClose: PropTypes.func, + + /** + * Specify a handler for "submitting" modal. + * The handler should care of closing modal, e.g. changing `open` prop, if necessary. + */ + onRequestSubmit: PropTypes.func, + + /** + * Specify a handler for the secondary button. + * Useful if separate handler from `onRequestClose` is desirable + */ + onSecondarySubmit: PropTypes.func, + + /** + * Specify whether the Modal is currently open + */ + open: PropTypes.bool, + + /** + * Specify whether the modal should be button-less + */ + passiveModal: PropTypes.bool, + + /** + * Prevent closing on click outside of modal + */ + preventCloseOnClickOutside: PropTypes.bool, + + /** + * Specify whether the Button should be disabled, or not + */ + primaryButtonDisabled: PropTypes.bool, + + /** + * Specify the text for the primary button + */ + primaryButtonText: PropTypes.node, + + /** + * Specify the text for the secondary button + */ + secondaryButtonText: PropTypes.node, + + /** + * Specify an array of config objects for secondary buttons + * (`Array<{ + * buttonText: string, + * onClick: function, + * }>`). + */ + secondaryButtons: (props, propName, componentName) => { + if (props.secondaryButtons) { + if ( + !Array.isArray(props.secondaryButtons) || + props.secondaryButtons.length !== 2 + ) { + return new Error( + `${propName} needs to be an array of two button config objects` + ); + } + + const shape = { + buttonText: PropTypes.node, + onClick: PropTypes.func, + }; + + props[propName].forEach((secondaryButton) => { + PropTypes.checkPropTypes( + shape, + secondaryButton, + propName, + componentName + ); + }); + } + + return null; + }, + + /** + * Specify a CSS selector that matches the DOM element that should + * be focused when the Modal opens + */ + selectorPrimaryFocus: PropTypes.string, + + /** + * Specify CSS selectors that match DOM elements working as floating menus. + * Focusing on those elements won't trigger "focus-wrap" behavior + */ + selectorsFloatingMenus: PropTypes.arrayOf(PropTypes.string), + + /** + * Specify if Enter key should be used as "submit" action + */ + shouldSubmitOnEnter: PropTypes.bool, + + /** + * Specify the size variant. + */ + size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']), +}; + +Modal.defaultProps = { + onRequestClose: () => {}, + onRequestSubmit: () => {}, + primaryButtonDisabled: false, + onKeyDown: () => {}, + passiveModal: false, + modalHeading: '', + modalLabel: '', + preventCloseOnClickOutside: false, + selectorPrimaryFocus: '[data-modal-primary-focus]', + hasScrollingContent: false, +}; + +export default Modal; diff --git a/packages/react/src/components/Modal/next/Modal.stories.js b/packages/react/src/components/Modal/next/Modal.stories.js new file mode 100644 index 000000000000..e21a1ace95fe --- /dev/null +++ b/packages/react/src/components/Modal/next/Modal.stories.js @@ -0,0 +1,300 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from 'react'; +import ReactDOM from 'react-dom'; +import { action } from '@storybook/addon-actions'; +import Modal from './Modal'; +import Button from '../../Button'; +import Select from '../../Select'; +import MultiSelect from '../../MultiSelect'; +import Dropdown from '../../Dropdown'; +import SelectItem from '../../SelectItem'; +import TextInput from '../../TextInput'; +import mdx from '../Modal.mdx'; + +const buttons = { + 'None (0)': '0', + 'One (1)': '1', + 'Two (2)': '2', + 'Three (3)': '3', +}; + +const props = { + modal: () => ({ + numberOfButtons: ('Number of Buttons', buttons, '2'), + className: 'some-class', + open: true, + danger: false, + alert: false, + shouldSubmitOnEnter: false, + hasScrollingContent: false, + hasForm: false, + modalHeading: 'Modal heading', + modalLabel: 'Label', + modalAriaLabel: + 'A label to be read by screen readers on the modal root node', + selectorPrimaryFocus: '[data-modal-primary-focus]', + size: 'md', + onBlur: action('onBlur'), + onClick: action('onClick'), + onFocus: action('onFocus'), + onRequestClose: action('onRequestClose'), + onRequestSubmit: action('onRequestSubmit'), + onSecondarySubmit: action('onSecondarySubmit'), + preventCloseOnClickOutside: true, + primaryButtonDisabled: false, + primaryButtonText: 'Primary button', + }), + modalFooter: (numberOfButtons) => { + const secondaryButtons = () => { + switch (numberOfButtons) { + case '2': + return { + secondaryButtonText: 'Secondary button', + }; + case '3': + return { + secondaryButtons: [ + { + buttonText: 'Keep both', + onClick: action('onClick'), + }, + { + buttonText: 'Rename', + onClick: action('onClick'), + }, + ], + }; + default: + return null; + } + }; + return { + passiveModal: false || numberOfButtons === '0', + ...secondaryButtons(), + }; + }, +}; + +export default { + title: 'Components/Modal', + parameters: { + component: Modal, + docs: { + page: mdx, + }, + }, +}; + +export const Default = () => { + return ( + +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+ + + + (item ? item.text : '')} + /> +
+ ); +}; + +export const Playground = () => { + const { + size, + numberOfButtons, + hasScrollingContent, + ...modalProps + } = props.modal(); + const { passiveModal, ...footerProps } = props.modalFooter(numberOfButtons); + return ( + +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+ + +
+ {hasScrollingContent && ( + <> +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+

Lorem ipsum

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean id + accumsan augue. Phasellus consequat augue vitae tellus tincidunt + posuere. Curabitur justo urna, consectetur vel elit iaculis, + ultrices condimentum risus. Nulla facilisi. Etiam venenatis molestie + tellus. Quisque consectetur non risus eu rutrum.{' '} +

+ + )} +
+ ); +}; + +export const WithStateManager = () => { + /** + * Simple state manager for modals. + */ + const ModalStateManager = ({ + renderLauncher: LauncherContent, + children: ModalContent, + }) => { + const [open, setOpen] = useState(false); + return ( + <> + {!ModalContent || typeof document === 'undefined' + ? null + : ReactDOM.createPortal( + , + document.body + )} + {LauncherContent && } + + ); + }; + return ( + ( + + )}> + {({ open, setOpen }) => ( + setOpen(false)}> +

+ Custom domains direct requests for your apps in this Cloud Foundry + organization to a URL that you own. A custom domain can be a shared + domain, a shared subdomain, or a shared domain and host. +

+ + +
+ )} +
+ ); +}; + +export const PassiveModal = () => { + return ( + + ); +}; diff --git a/packages/react/src/components/ModalWrapper/ModalWrapper-test.js b/packages/react/src/components/ModalWrapper/ModalWrapper-test.js index 472fa386010a..90fb9e7a54e1 100644 --- a/packages/react/src/components/ModalWrapper/ModalWrapper-test.js +++ b/packages/react/src/components/ModalWrapper/ModalWrapper-test.js @@ -78,4 +78,27 @@ describe('ModalWrapper', () => { wrapper.find({ children: mockProps.primaryButtonText }).simulate('click'); expect(wrapper.state('isOpen')).toBe(true); }); + + it('should default to primary button', () => { + const wrapper = mount(); + expect(wrapper.find(`.${prefix}--btn--primary`).length).toEqual(2); + }); + + it('should render ghost button when ghost is passed', () => { + const wrapper = mount(); + wrapper.setProps({ triggerButtonKind: 'ghost' }); + expect(wrapper.find(`.${prefix}--btn--ghost`).length).toEqual(1); + }); + + it('should render danger button when danger is passed', () => { + const wrapper = mount(); + wrapper.setProps({ triggerButtonKind: 'danger' }); + expect(wrapper.find(`.${prefix}--btn--danger`).length).toEqual(1); + }); + + it('should render secondary button when secondary is passed', () => { + const wrapper = mount(); + wrapper.setProps({ triggerButtonKind: 'secondary' }); + expect(wrapper.find(`.${prefix}--btn--secondary`).length).toEqual(2); + }); }); diff --git a/packages/react/src/components/OverflowMenu/next/OverflowMenu.js b/packages/react/src/components/OverflowMenu/next/OverflowMenu.js index 5fc974e887eb..0b0bb248f34d 100644 --- a/packages/react/src/components/OverflowMenu/next/OverflowMenu.js +++ b/packages/react/src/components/OverflowMenu/next/OverflowMenu.js @@ -12,6 +12,7 @@ import { settings } from 'carbon-components'; import { OverflowMenuVertical16 } from '@carbon/icons-react'; import { useId } from '../../../internal/useId'; import Menu from '../../Menu'; +import { keys, matches as keyCodeMatches } from '../../../internal/keyboard'; const { prefix } = settings; @@ -68,6 +69,20 @@ function OverflowMenu({ e.preventDefault(); } + function handleKeyPress(e) { + if ( + open && + keyCodeMatches(e, [ + keys.ArrowUp, + keys.ArrowRight, + keys.ArrowDown, + keys.ArrowLeft, + ]) + ) { + e.preventDefault(); + } + } + const containerClasses = classNames(`${prefix}--overflow-menu__container`); const triggerClasses = classNames( @@ -88,6 +103,7 @@ function OverflowMenu({ className={triggerClasses} onClick={handleClick} onMouseDown={handleMousedown} + onKeyDown={handleKeyPress} ref={triggerRef}> diff --git a/packages/react/src/components/OverflowMenu/next/OverflowMenu-story.js b/packages/react/src/components/OverflowMenu/next/OverflowMenu.stories.js similarity index 100% rename from packages/react/src/components/OverflowMenu/next/OverflowMenu-story.js rename to packages/react/src/components/OverflowMenu/next/OverflowMenu.stories.js diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index 1d8ff0355c3d..8f4d083a586d 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -153,6 +153,15 @@ export default class Tabs extends React.Component { _handleWindowResize = this.handleScroll; + /** + * The debounced version of the `scroll` event handler. + * @type {Function} + * @private + */ + _debouncedHandleScroll = null; + + _handleScroll = this.handleScroll; + componentDidMount() { if (!this._debouncedHandleWindowResize) { this._debouncedHandleWindowResize = debounce( @@ -164,6 +173,10 @@ export default class Tabs extends React.Component { this._handleWindowResize(); window.addEventListener('resize', this._debouncedHandleWindowResize); + if (!this._debouncedHandleScroll) { + this._debouncedHandleScroll = debounce(this._handleScroll, 125); + } + // scroll selected tab into view on mount const { clientWidth: tablistClientWidth, @@ -516,7 +529,7 @@ export default class Tabs extends React.Component { tabIndex={-1} className={classes.tablist} ref={this.tablist} - onScroll={this.handleScroll}> + onScroll={this._debouncedHandleScroll}> {tabsWithProps} {!rightOverflowNavButtonHidden && ( diff --git a/packages/sketch/package.json b/packages/sketch/package.json index 265a4097443c..fccdfef01b73 100644 --- a/packages/sketch/package.json +++ b/packages/sketch/package.json @@ -2,7 +2,7 @@ "name": "@carbon/sketch", "private": true, "description": "Tooling for generating a sketch plugin to bring code to design", - "version": "10.46.0-rc.0", + "version": "10.46.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -30,8 +30,8 @@ "dependencies": { "@carbon/colors": "^10.34.0", "@carbon/icon-helpers": "^10.25.0", - "@carbon/icons": "^10.43.0-rc.0", - "@carbon/themes": "^10.47.0-rc.0", + "@carbon/icons": "^10.43.0", + "@carbon/themes": "^10.47.0", "@carbon/type": "^10.38.0", "@skpm/builder": "^0.7.0", "color-string": "^1.5.3", diff --git a/packages/styles/package.json b/packages/styles/package.json index 2fd0efae6b5b..117de323706a 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/styles", "description": "Styles for the Carbon Design System", - "version": "0.9.0-rc.0", + "version": "0.9.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -25,7 +25,7 @@ "@carbon/grid": "^10.39.0", "@carbon/layout": "^10.34.0", "@carbon/motion": "^10.26.0", - "@carbon/themes": "^10.47.0-rc.0", + "@carbon/themes": "^10.47.0", "@carbon/type": "^10.38.0", "@ibm/plex": "6.0.0-next.6" }, diff --git a/packages/styles/scss/components/overflow-menu/_overflow-menu.scss b/packages/styles/scss/components/overflow-menu/_overflow-menu.scss index 21ac7dd31e99..9cdfd80a3973 100644 --- a/packages/styles/scss/components/overflow-menu/_overflow-menu.scss +++ b/packages/styles/scss/components/overflow-menu/_overflow-menu.scss @@ -16,6 +16,7 @@ @use '../../utilities/focus-outline' as *; @use '../../utilities/high-contrast-mode' as *; @use '../../utilities/convert' as *; +@use '../../utilities/z-index' as *; /// Overflow menu styles /// @access public diff --git a/packages/themes/package.json b/packages/themes/package.json index e68c931c7a66..ec8a69439cc9 100644 --- a/packages/themes/package.json +++ b/packages/themes/package.json @@ -1,7 +1,7 @@ { "name": "@carbon/themes", "description": "Themes for applying color in the Carbon Design System", - "version": "10.47.0-rc.0", + "version": "10.47.0", "license": "Apache-2.0", "main": "lib/index.js", "module": "es/index.js", diff --git a/yarn.lock b/yarn.lock index b5f0846d2847..8bd2128780e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1768,18 +1768,18 @@ __metadata: languageName: unknown linkType: soft -"@carbon/elements@^10.47.0-rc.0, @carbon/elements@workspace:packages/elements": +"@carbon/elements@^10.47.0, @carbon/elements@workspace:packages/elements": version: 0.0.0-use.local resolution: "@carbon/elements@workspace:packages/elements" dependencies: "@carbon/cli": ^10.31.0 "@carbon/colors": ^10.34.0 "@carbon/grid": ^10.39.0 - "@carbon/icons": ^10.43.0-rc.0 + "@carbon/icons": ^10.43.0 "@carbon/import-once": ^10.6.0 "@carbon/layout": ^10.34.0 "@carbon/motion": ^10.26.0 - "@carbon/themes": ^10.47.0-rc.0 + "@carbon/themes": ^10.47.0 "@carbon/type": ^10.38.0 fs-extra: ^8.1.0 klaw-sync: ^6.0.0 @@ -1870,25 +1870,25 @@ __metadata: languageName: node linkType: hard -"@carbon/icons-handlebars@^10.43.0-rc.0, @carbon/icons-handlebars@workspace:packages/icons-handlebars": +"@carbon/icons-handlebars@^10.43.0, @carbon/icons-handlebars@workspace:packages/icons-handlebars": version: 0.0.0-use.local resolution: "@carbon/icons-handlebars@workspace:packages/icons-handlebars" dependencies: "@carbon/icon-helpers": ^10.25.0 - "@carbon/icons": ^10.43.0-rc.0 + "@carbon/icons": ^10.43.0 handlebars: ^4.0.12 peerDependencies: handlebars: ^4.0.12 languageName: unknown linkType: soft -"@carbon/icons-react@^10.43.0-rc.0, @carbon/icons-react@workspace:packages/icons-react": +"@carbon/icons-react@^10.43.0, @carbon/icons-react@workspace:packages/icons-react": version: 0.0.0-use.local resolution: "@carbon/icons-react@workspace:packages/icons-react" dependencies: "@carbon/icon-build-helpers": ^0.30.0 "@carbon/icon-helpers": ^10.25.0 - "@carbon/icons": ^10.43.0-rc.0 + "@carbon/icons": ^10.43.0 "@carbon/telemetry": 0.0.0-alpha.6 prop-types: ^15.7.2 rimraf: ^3.0.2 @@ -1913,7 +1913,7 @@ __metadata: dependencies: "@carbon/cli-reporter": ^10.5.0 "@carbon/icon-helpers": ^10.25.0 - "@carbon/icons": ^10.43.0-rc.0 + "@carbon/icons": ^10.43.0 fs-extra: ^8.1.0 prettier: ^2.2.1 rimraf: ^3.0.0 @@ -1922,7 +1922,7 @@ __metadata: languageName: unknown linkType: soft -"@carbon/icons@^10.43.0-rc.0, @carbon/icons@workspace:packages/icons": +"@carbon/icons@^10.43.0, @carbon/icons@workspace:packages/icons": version: 0.0.0-use.local resolution: "@carbon/icons@workspace:packages/icons" dependencies: @@ -1996,10 +1996,10 @@ __metadata: "@babel/preset-env": ^7.14.7 "@babel/preset-react": ^7.14.5 "@carbon/feature-flags": ^0.6.0 - "@carbon/icons-react": ^10.43.0-rc.0 - "@carbon/styles": ^0.9.0-rc.0 + "@carbon/icons-react": ^10.43.0 + "@carbon/styles": ^0.9.0 "@carbon/telemetry": 0.0.0-alpha.6 - "@carbon/themes": ^10.47.0-rc.0 + "@carbon/themes": ^10.47.0 "@rollup/plugin-babel": ^5.3.0 "@rollup/plugin-commonjs": ^18.0.0 "@rollup/plugin-node-resolve": ^11.2.1 @@ -2013,8 +2013,8 @@ __metadata: babel-plugin-dev-expression: ^0.2.2 babel-preset-carbon: ^0.1.0 browserslist-config-carbon: ^10.6.1 - carbon-components: ^10.48.0-rc.0 - carbon-components-react: ^7.48.0-rc.0 + carbon-components: ^10.48.0 + carbon-components-react: ^7.48.0 carbon-icons: ^7.0.7 css-loader: ^5.2.4 fs-extra: ^10.0.0 @@ -2054,8 +2054,8 @@ __metadata: dependencies: "@carbon/colors": ^10.34.0 "@carbon/icon-helpers": ^10.25.0 - "@carbon/icons": ^10.43.0-rc.0 - "@carbon/themes": ^10.47.0-rc.0 + "@carbon/icons": ^10.43.0 + "@carbon/themes": ^10.47.0 "@carbon/type": ^10.38.0 "@skpm/builder": ^0.7.0 color-string: ^1.5.3 @@ -2067,7 +2067,7 @@ __metadata: languageName: unknown linkType: soft -"@carbon/styles@^0.9.0-rc.0, @carbon/styles@workspace:packages/styles": +"@carbon/styles@^0.9.0, @carbon/styles@workspace:packages/styles": version: 0.0.0-use.local resolution: "@carbon/styles@workspace:packages/styles" dependencies: @@ -2077,7 +2077,7 @@ __metadata: "@carbon/layout": ^10.34.0 "@carbon/motion": ^10.26.0 "@carbon/test-utils": ^10.20.0 - "@carbon/themes": ^10.47.0-rc.0 + "@carbon/themes": ^10.47.0 "@carbon/type": ^10.38.0 "@ibm/plex": 6.0.0-next.6 css: ^3.0.0 @@ -2121,7 +2121,7 @@ __metadata: languageName: unknown linkType: soft -"@carbon/themes@^10.47.0-rc.0, @carbon/themes@workspace:packages/themes": +"@carbon/themes@^10.47.0, @carbon/themes@workspace:packages/themes": version: 0.0.0-use.local resolution: "@carbon/themes@workspace:packages/themes" dependencies: @@ -10299,7 +10299,7 @@ __metadata: languageName: node linkType: hard -"carbon-components-react@^7.48.0-rc.0, carbon-components-react@workspace:packages/react": +"carbon-components-react@^7.48.0, carbon-components-react@workspace:packages/react": version: 0.0.0-use.local resolution: "carbon-components-react@workspace:packages/react" dependencies: @@ -10316,7 +10316,7 @@ __metadata: "@babel/preset-react": ^7.14.5 "@babel/runtime": ^7.14.6 "@carbon/feature-flags": ^0.6.0 - "@carbon/icons-react": ^10.43.0-rc.0 + "@carbon/icons-react": ^10.43.0 "@carbon/layout": ^10.34.0 "@carbon/telemetry": 0.0.0-alpha.6 "@carbon/test-utils": ^10.20.0 @@ -10344,7 +10344,7 @@ __metadata: babel-plugin-react-docgen: ^4.2.1 babel-plugin-transform-inline-environment-variables: ^0.4.3 browserslist-config-carbon: ^10.6.1 - carbon-components: ^10.48.0-rc.0 + carbon-components: ^10.48.0 carbon-icons: ^7.0.5 chalk: ^4.1.1 classnames: 2.3.1 @@ -10429,7 +10429,7 @@ __metadata: languageName: node linkType: hard -"carbon-components@^10.48.0-rc.0, carbon-components@workspace:packages/components": +"carbon-components@^10.48.0, carbon-components@workspace:packages/components": version: 0.0.0-use.local resolution: "carbon-components@workspace:packages/components" dependencies: @@ -10442,9 +10442,9 @@ __metadata: "@babel/preset-react": ^7.14.5 "@babel/runtime": ^7.14.6 "@carbon/cli": ^10.31.0 - "@carbon/elements": ^10.47.0-rc.0 - "@carbon/icons-handlebars": ^10.43.0-rc.0 - "@carbon/icons-react": ^10.43.0-rc.0 + "@carbon/elements": ^10.47.0 + "@carbon/icons-handlebars": ^10.43.0 + "@carbon/icons-react": ^10.43.0 "@carbon/telemetry": 0.0.0-alpha.6 "@carbon/test-utils": ^10.20.0 "@frctl/fractal": ^1.1.0