diff --git a/packages/react/__tests__/PublicAPI-test.js b/packages/react/__tests__/PublicAPI-test.js index dc2d28489108..3f688a56813d 100644 --- a/packages/react/__tests__/PublicAPI-test.js +++ b/packages/react/__tests__/PublicAPI-test.js @@ -124,6 +124,8 @@ beforeEach(() => { * the components that we export and their corresponding API. */ test('Public API should only change with a semver change', () => { + jest.mock('../src/internal/deprecateFieldOnObject'); + const CarbonReact = require('../src'); const PublicAPI = new Map(); diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index a78ea9f33be3..5cff6abca646 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -4331,6 +4331,249 @@ Map { }, "render": [Function], }, + "ControlledPasswordInput" => Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "className": "\${prefix}--text__input", + "disabled": false, + "helperText": "", + "invalid": false, + "invalidText": "", + "light": false, + "onChange": [Function], + "onClick": [Function], + "size": "", + }, + "propTypes": Object { + "className": Object { + "type": "string", + }, + "defaultValue": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "number", + }, + ], + ], + "type": "oneOfType", + }, + "disabled": Object { + "type": "bool", + }, + "helperText": Object { + "type": "node", + }, + "hideLabel": Object { + "type": "bool", + }, + "hidePasswordLabel": Object { + "type": "string", + }, + "id": Object { + "isRequired": true, + "type": "string", + }, + "invalid": Object { + "type": "bool", + }, + "invalidText": Object { + "type": "node", + }, + "labelText": Object { + "isRequired": true, + "type": "node", + }, + "light": Object { + "type": "bool", + }, + "onChange": Object { + "type": "func", + }, + "onClick": Object { + "type": "func", + }, + "placeholder": Object { + "type": "string", + }, + "showPasswordLabel": Object { + "type": "string", + }, + "size": Object { + "type": "string", + }, + "tooltipAlignment": Object { + "args": Array [ + Array [ + "start", + "center", + "end", + ], + ], + "type": "oneOf", + }, + "tooltipPosition": Object { + "args": Array [ + Array [ + "top", + "right", + "bottom", + "left", + ], + ], + "type": "oneOf", + }, + "value": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "number", + }, + ], + ], + "type": "oneOfType", + }, + }, + "render": [Function], + }, + "PasswordInput" => Object { + "$$typeof": Symbol(react.forward_ref), + "defaultProps": Object { + "className": "\${prefix}--text__input", + "disabled": false, + "helperText": "", + "invalid": false, + "invalidText": "", + "light": false, + "onChange": [Function], + "onClick": [Function], + "size": "", + }, + "propTypes": Object { + "className": Object { + "type": "string", + }, + "defaultValue": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "number", + }, + ], + ], + "type": "oneOfType", + }, + "disabled": Object { + "type": "bool", + }, + "helperText": Object { + "type": "node", + }, + "hideLabel": Object { + "type": "bool", + }, + "hidePasswordLabel": Object { + "type": "string", + }, + "id": Object { + "isRequired": true, + "type": "string", + }, + "inline": Object { + "type": "bool", + }, + "invalid": Object { + "type": "bool", + }, + "invalidText": Object { + "type": "node", + }, + "labelText": Object { + "isRequired": true, + "type": "node", + }, + "light": Object { + "type": "bool", + }, + "onChange": Object { + "type": "func", + }, + "onClick": Object { + "type": "func", + }, + "onTogglePasswordVisibility": Object { + "type": "func", + }, + "placeholder": Object { + "type": "string", + }, + "showPasswordLabel": Object { + "type": "string", + }, + "size": Object { + "type": "string", + }, + "tooltipAlignment": Object { + "args": Array [ + Array [ + "start", + "center", + "end", + ], + ], + "type": "oneOf", + }, + "tooltipPosition": Object { + "args": Array [ + Array [ + "top", + "right", + "bottom", + "left", + ], + ], + "type": "oneOf", + }, + "type": Object { + "args": Array [ + Array [ + "password", + "text", + ], + ], + "type": "oneOf", + }, + "value": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "number", + }, + ], + ], + "type": "oneOfType", + }, + "warn": Object { + "type": "bool", + }, + "warnText": Object { + "type": "node", + }, + }, + "render": [Function], + }, "PrimaryButton" => Object {}, "ProgressIndicator" => Object { "defaultProps": Object { diff --git a/packages/react/docs/migration/11.x-namespaced-exports.md b/packages/react/docs/migration/11.x-namespaced-exports.md new file mode 100644 index 000000000000..b34408159e58 --- /dev/null +++ b/packages/react/docs/migration/11.x-namespaced-exports.md @@ -0,0 +1,44 @@ +# Namespaced exports + + + + +## Table of Contents + +- [Overview](#overview) +- [Process](#process) +- [Changes:](#changes) + + + +## Overview + +In v10.x there were a few exported components that were only exposed under the +namespace of another component. These have been deprecated, and a new export has +been made available for each so these can be imported directly. In v11.x the +namespaced exports will be removed. + +## Process + +1. Update imports for the components listed in the table below. + +```diff +- import { TextInput } from 'carbon-components-react'; ++ import { PasswordInput } from 'carbon-components-react'; +``` + +2. Update usages of the components listed in the table below, they no longer + need the namespace + +```diff +- ++ +``` + +## Changes: + +| v10.x | v11.x | +| ----------------------------------- | ------------------------- | +| `TextInput.ControlledPasswordInput` | `ControlledPasswordInput` | +| `TextInput.PasswordInput` | `PasswordInput` | +| `MultiSelect.Filterable` | `FilterableMultiSelect` | diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index 4ad9a349281b..8ba8e2097c99 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -35,6 +35,7 @@ describe('Carbon Components React', () => { "ComposedModal", "Content", "ContentSwitcher", + "ControlledPasswordInput", "Copy", "CopyButton", "DangerButton", @@ -96,6 +97,7 @@ describe('Carbon Components React', () => { "Pagination", "PaginationNav", "PaginationSkeleton", + "PasswordInput", "PrimaryButton", "ProgressIndicator", "ProgressIndicatorSkeleton", diff --git a/packages/react/src/components/FilterableMultiSelect/index.js b/packages/react/src/components/FilterableMultiSelect/index.js new file mode 100644 index 000000000000..e24ee932dcc1 --- /dev/null +++ b/packages/react/src/components/FilterableMultiSelect/index.js @@ -0,0 +1 @@ +export FilterableMultiSelect from '../MultiSelect/FilterableMultiSelect'; diff --git a/packages/react/src/components/MultiSelect/MultiSelect-story.js b/packages/react/src/components/MultiSelect/MultiSelect-story.js index b1c532f34572..97303f5660ae 100644 --- a/packages/react/src/components/MultiSelect/MultiSelect-story.js +++ b/packages/react/src/components/MultiSelect/MultiSelect-story.js @@ -17,6 +17,7 @@ import { import { withReadme } from 'storybook-readme'; import readme from './README.md'; import MultiSelect from '../MultiSelect'; +import FilterableMultiSelect from '../MultiSelect/FilterableMultiSelect'; import Checkbox from '../Checkbox'; import mdx from './MultiSelect.mdx'; @@ -116,7 +117,7 @@ export default { page: mdx, }, subcomponents: { - 'MultiSelect.Filterable': MultiSelect.Filterable, + FilterableMultiSelect, }, }, }; @@ -190,7 +191,7 @@ export const _Filterable = withReadme(readme, () => { return (
- (item ? item.text : '')} diff --git a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js index 61bb0707d19d..4c4474840704 100644 --- a/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js +++ b/packages/react/src/components/MultiSelect/__tests__/FilterableMultiSelect-test.js @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import MultiSelect from '../../MultiSelect'; +import FilterableMultiSelect from '../../MultiSelect/FilterableMultiSelect'; import { assertMenuOpen, assertMenuClosed, @@ -21,10 +21,12 @@ const openMenu = (wrapper) => { wrapper.find(`[role="combobox"]`).simulate('click'); }; -describe('MultiSelect.Filterable', () => { +describe('FilterableMultiSelect', () => { let mockProps; beforeEach(() => { + // jest.mock('../../../internal/deprecateFieldOnObject'); + mockProps = { id: 'test-filterable-multiselect', disabled: false, @@ -37,23 +39,23 @@ describe('MultiSelect.Filterable', () => { }); it('should render', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper).toMatchSnapshot(); }); it('should display all items when the menu is open initially', () => { - const wrapper = mount(); + const wrapper = mount(); openMenu(wrapper); expect(wrapper.find(listItemName).length).toBe(mockProps.items.length); }); it('should initially have the menu open when open prop is provided', () => { - const wrapper = mount(); + const wrapper = mount(); assertMenuOpen(wrapper, mockProps); }); it('should open the menu with a down arrow', () => { - const wrapper = mount(); + const wrapper = mount(); const menuIconNode = findMenuIconNode(wrapper); menuIconNode.simulate('keyDown', { key: 'ArrowDown' }); @@ -61,7 +63,7 @@ describe('MultiSelect.Filterable', () => { }); it('should let the user toggle the menu by the menu icon', () => { - const wrapper = mount(); + const wrapper = mount(); findMenuIconNode(wrapper).simulate('click'); assertMenuOpen(wrapper, mockProps); findMenuIconNode(wrapper).simulate('click'); @@ -69,7 +71,7 @@ describe('MultiSelect.Filterable', () => { }); it('should not close the menu after a user makes a selection', () => { - const wrapper = mount(); + const wrapper = mount(); openMenu(wrapper); const firstListItem = wrapper.find(listItemName).at(0); @@ -79,7 +81,7 @@ describe('MultiSelect.Filterable', () => { }); it('should filter a list of items by the input value', () => { - const wrapper = mount(); + const wrapper = mount(); openMenu(wrapper); expect(wrapper.find(listItemName).length).toBe(mockProps.items.length); @@ -93,7 +95,7 @@ describe('MultiSelect.Filterable', () => { it('should call `onChange` with each update to selected items', () => { const wrapper = mount( - + ); openMenu(wrapper); @@ -128,7 +130,7 @@ describe('MultiSelect.Filterable', () => { it('should let items stay at their position after selecting', () => { const wrapper = mount( - + ); openMenu(wrapper); @@ -149,7 +151,7 @@ describe('MultiSelect.Filterable', () => { }); it('should not clear input value after a user makes a selection', () => { - const wrapper = mount(); + const wrapper = mount(); openMenu(wrapper); wrapper diff --git a/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js b/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js index 69274c14bc32..7d512303d656 100644 --- a/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js +++ b/packages/react/src/components/MultiSelect/__tests__/MultiSelect-test.js @@ -14,6 +14,9 @@ import MultiSelect from '../'; import { generateItems, generateGenericItem } from '../../ListBox/test-helpers'; describe('MultiSelect', () => { + beforeEach(() => { + jest.mock('../../../internal/deprecateFieldOnObject'); + }); afterEach(cleanup); describe.skip('automated accessibility tests', () => { diff --git a/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap b/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap index 28d3d3e6b560..92728708ec62 100644 --- a/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap +++ b/packages/react/src/components/MultiSelect/__tests__/__snapshots__/FilterableMultiSelect-test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MultiSelect.Filterable should render 1`] = ` - - + `; diff --git a/packages/react/src/components/MultiSelect/index.js b/packages/react/src/components/MultiSelect/index.js index 112c85925dc6..c61b45aec598 100644 --- a/packages/react/src/components/MultiSelect/index.js +++ b/packages/react/src/components/MultiSelect/index.js @@ -4,6 +4,7 @@ * 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 { deprecateFieldOnObject } from '../../internal/deprecateFieldOnObject'; import MultiSelect from './MultiSelect'; import FilterableMultiSelect from './FilterableMultiSelect'; @@ -11,4 +12,8 @@ import FilterableMultiSelect from './FilterableMultiSelect'; FilterableMultiSelect.displayName = 'MultiSelect.Filterable'; MultiSelect.Filterable = FilterableMultiSelect; +if (__DEV__) { + deprecateFieldOnObject(MultiSelect, 'Filterable', FilterableMultiSelect); +} + export default MultiSelect; diff --git a/packages/react/src/components/PasswordInput/index.js b/packages/react/src/components/PasswordInput/index.js new file mode 100644 index 000000000000..1f07aa31bc41 --- /dev/null +++ b/packages/react/src/components/PasswordInput/index.js @@ -0,0 +1,2 @@ +export ControlledPasswordInput from '../TextInput/ControlledPasswordInput'; +export PasswordInput from '../TextInput/PasswordInput'; diff --git a/packages/react/src/components/TextInput/TextInput-story.js b/packages/react/src/components/TextInput/TextInput-story.js index 10b6f7610129..51b58a98d4b7 100644 --- a/packages/react/src/components/TextInput/TextInput-story.js +++ b/packages/react/src/components/TextInput/TextInput-story.js @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; -import TextInput from '../TextInput'; +import { TextInput } from '../../index'; import TextInputSkeleton from '../TextInput/TextInput.Skeleton'; import FluidForm from '../FluidForm/FluidForm'; import mdx from './TextInput.mdx'; diff --git a/packages/react/src/components/TextInput/index.js b/packages/react/src/components/TextInput/index.js index 58347c19513b..a4bad4443c14 100644 --- a/packages/react/src/components/TextInput/index.js +++ b/packages/react/src/components/TextInput/index.js @@ -4,8 +4,23 @@ * 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 { deprecateFieldOnObject } from '../../internal/deprecateFieldOnObject'; export * from './TextInput.Skeleton'; -export ControlledPasswordInput from './ControlledPasswordInput'; -export PasswordInput from './PasswordInput'; -export default from './TextInput'; +import ControlledPasswordInput from './ControlledPasswordInput'; +import PasswordInput from './PasswordInput'; +import TextInput from './TextInput'; + +TextInput.ControlledPasswordInput = ControlledPasswordInput; +TextInput.PasswordInput = PasswordInput; + +if (__DEV__) { + deprecateFieldOnObject( + TextInput, + 'ControlledPasswordInput', + ControlledPasswordInput + ); + deprecateFieldOnObject(TextInput, 'PasswordInput', PasswordInput); +} + +export default TextInput; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 2f92fb3ff5a7..449904a87a84 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -4,7 +4,6 @@ * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ - export Accordion from './components/Accordion'; export AccordionItem from './components/AccordionItem'; export { AspectRatio } from './components/AspectRatio'; @@ -86,6 +85,10 @@ export OverflowMenu from './components/OverflowMenu'; export OverflowMenuItem from './components/OverflowMenuItem'; export Pagination from './components/Pagination'; export PaginationNav from './components/PaginationNav'; +export { + ControlledPasswordInput, + PasswordInput, +} from './components/PasswordInput'; export PrimaryButton from './components/PrimaryButton'; export { ProgressIndicator, diff --git a/packages/react/src/internal/__mocks__/deprecateFieldOnObject.js b/packages/react/src/internal/__mocks__/deprecateFieldOnObject.js new file mode 100644 index 000000000000..a1888682b4a7 --- /dev/null +++ b/packages/react/src/internal/__mocks__/deprecateFieldOnObject.js @@ -0,0 +1,10 @@ +/** + * 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. + */ + +module.exports = { + deprecateFieldOnObject: jest.fn(), +}; diff --git a/packages/react/src/internal/deprecateFieldOnObject.js b/packages/react/src/internal/deprecateFieldOnObject.js new file mode 100644 index 000000000000..43090160295b --- /dev/null +++ b/packages/react/src/internal/deprecateFieldOnObject.js @@ -0,0 +1,32 @@ +/** + * 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 { warning } from '../internal/warning'; + +const didWarnAboutDeprecation = {}; + +function deprecateFieldOnObject(object, field, Component, message) { + Object.defineProperty(object, field, { + enumerable: true, + get() { + if (!didWarnAboutDeprecation[field]) { + warning( + false, + message || + `The ${field} field has been deprecated on the ${object.displayName} object. ` + + `Please import and use ${ + Component.displayName || Component.name || 'the field' + } directly.` + ); + didWarnAboutDeprecation[field] = true; + } + + return Component; + }, + }); +} + +export { deprecateFieldOnObject };