diff --git a/README.md b/README.md index 9443290610c1..a4582bfc2d56 100644 --- a/README.md +++ b/README.md @@ -297,13 +297,13 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
nandininarayanofficial

💻
Andrea DG

💻
Luis

💻 -
Ali Al Dobyan

💻
Luke Harrison

💻
Ahmed Alsinan

💻 +
Ali Al Dobyan

💻
Adam Shea

💻 -
Md Nabeel Ayubee

💻 +
Md Nabeel Ayubee

💻
Patan Amrulla Khan

💻 📖 diff --git a/docs/experimental-code.md b/docs/experimental-code.md index 3c938083869d..30b92124a901 100644 --- a/docs/experimental-code.md +++ b/docs/experimental-code.md @@ -38,20 +38,20 @@ const unstable_meta = { // An unstable component will retain its name, specifically for things like // the rules of hooks plugin which depend on the correct casing of the name -function StaticNotification(props) { +function ComponentName(props) { // ... } // However, when we export the component we will export it with the `unstable_` // prefix. (Similar to React.unstable_Suspense, React.unstable_Profiler) -export { default as unstable_StaticNotification } from './components/StaticNotification'; +export { default as unstable_ComponentName } from './components/ComponentName'; ``` For teams using these features, they will need to import the functionality by using the `unstable_` prefix. For example: ```jsx -import { unstable_StaticNotification as StaticNotification } from '@carbon/react'; +import { unstable_ComponentName as ComponentName } from '@carbon/react'; ``` ### Documenting components and exports prefixed with `unstable_` diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 692d20549529..77143e4e58b4 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -569,6 +569,50 @@ Map { "2": "bottom", "3": "left", }, + "Callout" => Object { + "propTypes": Object { + "actionButtonLabel": Object { + "type": "string", + }, + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + "kind": Object { + "args": Array [ + Array [ + "error", + "info", + "info-square", + "success", + "warning", + "warning-alt", + ], + ], + "type": "oneOf", + }, + "lowContrast": Object { + "type": "bool", + }, + "onActionButtonClick": Object { + "type": "func", + }, + "statusIconDescription": Object { + "type": "string", + }, + "subtitle": Object { + "type": "node", + }, + "title": Object { + "type": "string", + }, + "titleId": Object { + "type": "string", + }, + }, + }, "Checkbox" => Object { "$$typeof": Symbol(react.forward_ref), "propTypes": Object { @@ -7455,50 +7499,7 @@ Map { }, "render": [Function], }, - "StaticNotification" => Object { - "propTypes": Object { - "actionButtonLabel": Object { - "type": "string", - }, - "children": Object { - "type": "node", - }, - "className": Object { - "type": "string", - }, - "kind": Object { - "args": Array [ - Array [ - "error", - "info", - "info-square", - "success", - "warning", - "warning-alt", - ], - ], - "type": "oneOf", - }, - "lowContrast": Object { - "type": "bool", - }, - "onActionButtonClick": Object { - "type": "func", - }, - "statusIconDescription": Object { - "type": "string", - }, - "subtitle": Object { - "type": "node", - }, - "title": Object { - "type": "string", - }, - "titleId": Object { - "type": "string", - }, - }, - }, + "StaticNotification" => Object {}, "StructuredListBody" => Object { "propTypes": Object { "children": Object { diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index f83f5635a36a..3fc8a6767449 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -36,6 +36,7 @@ describe('Carbon Components React', () => { "ButtonSkeleton", "ButtonTooltipAlignments", "ButtonTooltipPositions", + "Callout", "Checkbox", "CheckboxGroup", "CheckboxSkeleton", diff --git a/packages/react/src/components/Notification/Notification-test.js b/packages/react/src/components/Notification/Notification-test.js index 33f482a43a64..8e26e4aa6a88 100644 --- a/packages/react/src/components/Notification/Notification-test.js +++ b/packages/react/src/components/Notification/Notification-test.js @@ -12,6 +12,8 @@ import { ToastNotification, InlineNotification, ActionableNotification, + StaticNotification, + Callout, } from './Notification'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -337,3 +339,41 @@ describe('ActionableNotification', () => { }); }); }); + +// TODO: Remove StaticNotification tests when Callout moves to stable OR in +// v12, whichever is first. Ensure test parity on Callout. +describe('StaticNotification', () => { + it('logs a deprecation notice when used', () => { + const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + expect(() => { + render(); + }).not.toThrow(); + + expect(spy).toHaveBeenCalledWith( + 'Warning: `StaticNotification` has been renamed to `Callout`.' + + 'Run the following codemod to automatically update usages in your' + + 'project: `npx @carbon/upgrade migrate refactor-to-callout --write`' + ); + spy.mockRestore(); + }); +}); + +describe('Callout', () => { + it('enforces aria-describedby on interactive children elements', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => { + render( + + + + ); + }).not.toThrow(); + + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); +}); diff --git a/packages/react/src/components/Notification/Notification.mdx b/packages/react/src/components/Notification/Notification.mdx index c44833e75c0f..1e08c67932d6 100644 --- a/packages/react/src/components/Notification/Notification.mdx +++ b/packages/react/src/components/Notification/Notification.mdx @@ -22,7 +22,7 @@ import { Canvas, ArgTypes, Meta } from '@storybook/blocks'; There are 4 different types of notification components: `ActionableNotification`, `InlineNotification`, `ToastNotification`, and -`unstable__StaticNotification`. +`unstable__Callout`. ### ActionableNotification @@ -39,19 +39,18 @@ focus until the action is acted upon or the notification is dismissed. elements or rich text. These are announced by screenreaders when rendered. They don't grab focus. Use them to provide the user with an alert, status, or log. -### unstable\_\_StaticNotification +### unstable\_\_Callout (previously StaticNotification) -`unstable__StaticNotification` is non-modal and should only be used inline with -content on the initial render of the page or modal because it will not be -announced to screenreader users like the other notification components. +`unstable__Callout` is non-modal and should only be used inline with content on +the initial render of the page or modal because it will not be announced to +screenreader users like the other notification components. As such, this should not be used for real-time notifications or notifications responding to user input (unless the page is completely refreshing and bumping -the users focus back to the first element in the dom/tab order). If a -StaticNotification is rendered after the initial render, screenreader users' -focus may have already passed this portion of the DOM and they will not know -that the notification has been rendered until they circle back to the beginning -of the page. +the users focus back to the first element in the dom/tab order). If a Callout is +rendered after the initial render, screenreader users' focus may have already +passed this portion of the DOM and they will not know that the notification has +been rendered until they circle back to the beginning of the page. This is the most passive notification component and is essentially just a styled div. If you place actions or interactive elements within this component, place diff --git a/packages/react/src/components/Notification/Notification.tsx b/packages/react/src/components/Notification/Notification.tsx index f1b1d25f01e7..0c82454f7608 100644 --- a/packages/react/src/components/Notification/Notification.tsx +++ b/packages/react/src/components/Notification/Notification.tsx @@ -42,6 +42,7 @@ import { useId } from '../../internal/useId'; import { noopFn } from '../../internal/noopFn'; import wrapFocus, { wrapFocusWithoutSentinels } from '../../internal/wrapFocus'; import { useFeatureFlag } from '../FeatureFlags'; +import { warning } from '../../internal/warning'; /** * Conditionally call a callback when the escape key is pressed @@ -851,7 +852,7 @@ export interface ActionableNotificationProps /** * @deprecated This prop will be removed in the next major version, v12. - * Specify if focus should be moved to the component on render. To meet the spec for alertdialog, this must always be true. If you're setting this to false, explore using StaticNotification instead. https://github.com/carbon-design-system/carbon/pull/15532 + * Specify if focus should be moved to the component on render. To meet the spec for alertdialog, this must always be true. If you're setting this to false, explore using Callout instead. https://github.com/carbon-design-system/carbon/pull/15532 */ hasFocus?: boolean; @@ -1128,10 +1129,14 @@ ActionableNotification.propTypes = { closeOnEscape: PropTypes.bool, /** - * Deprecated, please use StaticNotification once it's available. Issue #15532 * Specify if focus should be moved to the component when the notification contains actions */ - hasFocus: deprecate(PropTypes.bool), + hasFocus: deprecate( + PropTypes.bool, + 'hasFocus is deprecated. To conform to accessibility requirements hasFocus ' + + 'should always be `true` for ActionableNotification. If you were ' + + 'setting this prop to `false`, consider using the Callout component instead.' + ), /** * Specify the close button should be disabled, or not @@ -1198,12 +1203,11 @@ ActionableNotification.propTypes = { }; /** - * StaticNotification + * Callout * ================== */ -export interface StaticNotificationProps - extends HTMLAttributes { +export interface CalloutProps extends HTMLAttributes { /** * Pass in the action button label that will be rendered within the ActionableNotification. */ @@ -1231,7 +1235,7 @@ export interface StaticNotificationProps | 'warning-alt'; /** - * Specify whether you are using the low contrast variant of the StaticNotification. + * Specify whether you are using the low contrast variant of the Callout. */ lowContrast?: boolean; @@ -1261,7 +1265,7 @@ export interface StaticNotificationProps titleId?: string; } -export function StaticNotification({ +export function Callout({ actionButtonLabel, children, onActionButtonClick, @@ -1273,7 +1277,7 @@ export function StaticNotification({ kind = 'error', lowContrast, ...rest -}: StaticNotificationProps) { +}: CalloutProps) { const prefix = usePrefix(); const containerClassName = cx(className, { [`${prefix}--actionable-notification`]: true, @@ -1329,7 +1333,7 @@ export function StaticNotification({ ); } -StaticNotification.propTypes = { +Callout.propTypes = { /** * Pass in the action button label that will be rendered within the ActionableNotification. */ @@ -1358,7 +1362,7 @@ StaticNotification.propTypes = { ]), /** - * Specify whether you are using the low contrast variant of the StaticNotification. + * Specify whether you are using the low contrast variant of the Callout. */ lowContrast: PropTypes.bool, @@ -1387,3 +1391,30 @@ StaticNotification.propTypes = { */ titleId: PropTypes.string, }; + +// In renaming StaticNotification to Callout, the legacy StaticNotification +// export and it's types should remain usable until Callout is moved to stable. +// The StaticNotification component below forwards props to Callout and inherits +// CalloutProps to ensure consumer usage is not impacted, while providing them +// a deprecation warning. +// TODO: remove this when Callout moves to stable OR in v12, whichever is first +/** + * @deprecated Use `CalloutProps` instead. + */ +export interface StaticNotificationProps extends CalloutProps {} +let didWarnAboutDeprecation = false; +export const StaticNotification: React.FC = ( + props +) => { + if (__DEV__) { + warning( + didWarnAboutDeprecation, + '`StaticNotification` has been renamed to `Callout`.' + + 'Run the following codemod to automatically update usages in your' + + 'project: `npx @carbon/upgrade migrate refactor-to-callout --write`' + ); + didWarnAboutDeprecation = true; + } + + return ; +}; diff --git a/packages/react/src/components/Notification/index.tsx b/packages/react/src/components/Notification/index.tsx index a28475e11150..37432616086e 100644 --- a/packages/react/src/components/Notification/index.tsx +++ b/packages/react/src/components/Notification/index.tsx @@ -18,4 +18,6 @@ export { type ActionableNotificationProps, StaticNotification, type StaticNotificationProps, + Callout, + type CalloutProps, } from './Notification'; diff --git a/packages/react/src/components/Notification/stories/Callout.stories.js b/packages/react/src/components/Notification/stories/Callout.stories.js new file mode 100644 index 000000000000..530c3f3a3aac --- /dev/null +++ b/packages/react/src/components/Notification/stories/Callout.stories.js @@ -0,0 +1,98 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * 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 { Callout } from '../../Notification'; +import { Link } from '../../Link'; +import mdx from '../Notification.mdx'; + +export default { + title: 'Experimental/unstable__Callout', + component: Callout, + parameters: { + docs: { + page: mdx, + }, + }, + args: { + kind: 'error', + lowContrast: false, + statusIconDescription: 'notification', + }, +}; + +export const Default = () => ( + +); + +export const WithInteractiveElements = () => ( + +
+ Additional text can describe the notification, or a link to{' '} + + learn more + +
+
+); + +export const WithActionButtonOnly = () => ( + +
+ Here is some important info you might want to know.{' '} +
+
+); + +export const WithActionButtonAndLinks = () => ( + +
+ + Create + {' '} + or{' '} + + register + {' '} + a cluster before creating a Configuration. Some additional info could go + here to show that this notification subtitle goes below the title. +
+
+); + +export const Playground = (args) => ; + +Playground.argTypes = { + children: { + table: { + disable: true, + }, + }, + className: { + table: { + disable: true, + }, + }, +}; +Playground.args = { + title: 'Notification title', + subtitle: 'Subtitle text goes here', +}; diff --git a/packages/react/src/components/Notification/stories/StaticNotification.mdx b/packages/react/src/components/Notification/stories/StaticNotification.mdx new file mode 100644 index 000000000000..d9d578b7d060 --- /dev/null +++ b/packages/react/src/components/Notification/stories/StaticNotification.mdx @@ -0,0 +1,7 @@ +# StaticNotification has been renamed to Callout + +Run the following codemod to automatically update usages in your project: + +``` +npx @carbon/upgrade migrate refactor-to-callout --write +``` diff --git a/packages/react/src/components/Notification/stories/StaticNotification.stories.js b/packages/react/src/components/Notification/stories/StaticNotification.stories.js index c396f85d90e5..88c15127f01d 100644 --- a/packages/react/src/components/Notification/stories/StaticNotification.stories.js +++ b/packages/react/src/components/Notification/stories/StaticNotification.stories.js @@ -8,95 +8,30 @@ import React from 'react'; import { StaticNotification } from '../../Notification'; import { Link } from '../../Link'; -import mdx from '../Notification.mdx'; +import { CodeSnippet } from '../../CodeSnippet'; +import mdx from './StaticNotification.mdx'; -// eslint-disable-next-line storybook/csf-component export default { title: 'Experimental/unstable__StaticNotification', - component: StaticNotification, parameters: { docs: { page: mdx, }, }, - args: { - kind: 'error', - lowContrast: false, - statusIconDescription: 'notification', - }, }; export const Default = () => ( - -); - -export const WithInteractiveElements = () => ( - -
- Additional text can describe the notification, or a link to{' '} - - learn more - -
-
-); - -export const WithActionButtonOnly = () => ( - -
- Here is some important info you might want to know.{' '} -
-
-); - -export const WithActionButtonAndLinks = () => ( - -
- - Create - {' '} - or{' '} - - register - {' '} - a cluster before creating a Configuration. Some additional info could go - here to show that this notification subtitle goes below the title. + <> + + +
+

+ Run the following codemod to automatically update usages in your + project: +

+ + npx @carbon/upgrade migrate refactor-to-callout --write +
-
+ ); - -export const Playground = (args) => ; - -Playground.argTypes = { - children: { - table: { - disable: true, - }, - }, - className: { - table: { - disable: true, - }, - }, -}; -Playground.args = { - title: 'Notification title', - subtitle: 'Subtitle text goes here', -}; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index e8d501b2f2cb..8700ebd8142e 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -106,7 +106,8 @@ export { InlineNotification, NotificationActionButton, NotificationButton, - StaticNotification as unstable__StaticNotification, + Callout as unstable__Callout, + Callout as unstable__StaticNotification, } from './components/Notification'; export { NumberInput, NumberInputSkeleton } from './components/NumberInput'; export { OrderedList } from './components/OrderedList'; diff --git a/packages/upgrade/src/upgrades.js b/packages/upgrade/src/upgrades.js index 18a2699573d3..01376c13746a 100644 --- a/packages/upgrade/src/upgrades.js +++ b/packages/upgrade/src/upgrades.js @@ -331,6 +331,34 @@ export const upgrades = [ }); }, }, + { + name: 'refactor-to-callout', + description: + 'Rewrites imports and usages of StaticNotification to Callout', + migrate: async (options) => { + const transform = path.join(TRANSFORM_DIR, 'refactor-to-callout.js'); + const paths = + Array.isArray(options.paths) && options.paths.length > 0 + ? options.paths + : await glob(['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], { + cwd: options.workspaceDir, + ignore: [ + '**/es/**', + '**/lib/**', + '**/umd/**', + '**/node_modules/**', + '**/storybook-static/**', + ], + }); + + await run({ + dry: !options.write, + transform, + paths, + verbose: options.verbose, + }); + }, + }, ], }, { diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout.input.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout.input.js new file mode 100644 index 000000000000..241aaadc5ba5 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout.input.js @@ -0,0 +1,25 @@ +// Typical imports +import { unstable__StaticNotification as StaticNotification } from '@carbon/react'; +import { unstable__StaticNotification } from '@carbon/react'; + +// If they used a custom name +import { unstable__StaticNotification as SomeOtherName } from '@carbon/react'; + +// If they already renamed it Callout +import { unstable__StaticNotification as Callout } from '@carbon/react'; + +// Local renames like this are unlikely but technically possible +const LocallyRenamedStaticNotification = unstable__StaticNotification; + +// Component usages +// prettier-ignore +const App = () => { + return ( + <> + + + + + + ); +}; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout.output.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout.output.js new file mode 100644 index 000000000000..933d59d08359 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout.output.js @@ -0,0 +1,23 @@ +// Typical imports +import { unstable__Callout as Callout } from '@carbon/react'; +import { unstable__Callout } from '@carbon/react'; + +// If they used a custom name +import { unstable__Callout as SomeOtherName } from '@carbon/react'; + +// If they already renamed it Callout +import { unstable__Callout as Callout } from '@carbon/react'; + +// Local renames like this are unlikely but technically possible +const LocallyRenamedStaticNotification = unstable__Callout; + +// Component usages +// prettier-ignore +const App = () => { + return (<> + + + + + ); +}; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout2.input.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout2.input.js new file mode 100644 index 000000000000..d688d2116013 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout2.input.js @@ -0,0 +1,4 @@ +// Existing usages should not be transformed +import { unstable__Callout } from '@carbon/react'; +import { unstable__Callout as Callout } from '@carbon/react'; +import { unstable__Callout as SomeOtherOtherName } from '@carbon/react'; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout2.output.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout2.output.js new file mode 100644 index 000000000000..d688d2116013 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout2.output.js @@ -0,0 +1,4 @@ +// Existing usages should not be transformed +import { unstable__Callout } from '@carbon/react'; +import { unstable__Callout as Callout } from '@carbon/react'; +import { unstable__Callout as SomeOtherOtherName } from '@carbon/react'; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout3.input.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout3.input.js new file mode 100644 index 000000000000..0ecf8551ef73 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout3.input.js @@ -0,0 +1,5 @@ +// Do not transform potential naming collisions from local paths +import { unstable__Callout } from './my/local/project'; +import { unstable__Callout as Callout } from './my/local/project'; +import { unstable_StaticNotification } from './my/local/project'; +import { unstable_StaticNotification as StaticNotification } from './my/local/project'; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout3.output.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout3.output.js new file mode 100644 index 000000000000..0ecf8551ef73 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout3.output.js @@ -0,0 +1,5 @@ +// Do not transform potential naming collisions from local paths +import { unstable__Callout } from './my/local/project'; +import { unstable__Callout as Callout } from './my/local/project'; +import { unstable_StaticNotification } from './my/local/project'; +import { unstable_StaticNotification as StaticNotification } from './my/local/project'; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout4.input.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout4.input.js new file mode 100644 index 000000000000..1b10fca69362 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout4.input.js @@ -0,0 +1,3 @@ +// Do not transform potential naming collisions from local paths +import { Callout } from '../my/local/project'; +import { StaticNotification } from './my/local/project'; diff --git a/packages/upgrade/transforms/__testfixtures__/refactor-to-callout4.output.js b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout4.output.js new file mode 100644 index 000000000000..1b10fca69362 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/refactor-to-callout4.output.js @@ -0,0 +1,3 @@ +// Do not transform potential naming collisions from local paths +import { Callout } from '../my/local/project'; +import { StaticNotification } from './my/local/project'; diff --git a/packages/upgrade/transforms/__tests__/refactor-to-callout.js b/packages/upgrade/transforms/__tests__/refactor-to-callout.js new file mode 100644 index 000000000000..ccd4263ba9b7 --- /dev/null +++ b/packages/upgrade/transforms/__tests__/refactor-to-callout.js @@ -0,0 +1,15 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const { defineTest } = require('jscodeshift/dist/testUtils'); + +defineTest(__dirname, 'refactor-to-callout'); +defineTest(__dirname, 'refactor-to-callout', null, 'refactor-to-callout2'); +defineTest(__dirname, 'refactor-to-callout', null, 'refactor-to-callout3'); +defineTest(__dirname, 'refactor-to-callout', null, 'refactor-to-callout4'); diff --git a/packages/upgrade/transforms/refactor-to-callout.js b/packages/upgrade/transforms/refactor-to-callout.js new file mode 100644 index 000000000000..9ba8e18bb85b --- /dev/null +++ b/packages/upgrade/transforms/refactor-to-callout.js @@ -0,0 +1,157 @@ +/** + * Copyright IBM Corp. 2016, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const defaultOptions = { + quote: 'auto', + trailingComma: true, +}; + +function transform(fileInfo, api, options) { + const printOptions = options.printOptions || defaultOptions; + const j = api.jscodeshift; + const root = j(fileInfo.source); + + // Helper function to check if the import source is from '@carbon/react' or its subpaths + function isCarbonReactImport(sourceValue) { + return ( + sourceValue === '@carbon/react' || + sourceValue.startsWith('@carbon/react/es') || + sourceValue.startsWith('@carbon/react/lib') + ); + } + + // Collect names of identifiers imported from '@carbon/react' or its subpaths + const importedIdentifiers = new Map(); // Map of local name to transformed name + + // Transform import declarations + root.find(j.ImportDeclaration).forEach((path) => { + const sourceValue = path.node.source.value; + + // Only transform imports from '@carbon/react' and its subpaths + if (!isCarbonReactImport(sourceValue)) { + return; + } + + path.node.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + let importedName = specifier.imported.name; + let localName = specifier.local ? specifier.local.name : importedName; + let transformedImportedName = importedName; + let transformedLocalName = localName; + + // Transform imported names and local names as necessary + if (importedName === 'unstable__StaticNotification') { + transformedImportedName = 'unstable__Callout'; + specifier.imported.name = transformedImportedName; + + if (localName === 'StaticNotification') { + transformedLocalName = 'Callout'; + specifier.local.name = transformedLocalName; + } else if (localName === 'unstable__StaticNotification') { + transformedLocalName = 'unstable__Callout'; + specifier.local.name = transformedLocalName; + } + // If local name is something else (e.g., SomeOtherName), leave it unchanged + } else if (importedName === 'StaticNotification') { + transformedImportedName = 'Callout'; + specifier.imported.name = transformedImportedName; + + if (localName === 'StaticNotification') { + transformedLocalName = 'Callout'; + specifier.local.name = transformedLocalName; + } + // If local name is different, leave it unchanged + } else if (importedName === 'StaticNotificationProps') { + transformedImportedName = 'CalloutProps'; + specifier.imported.name = transformedImportedName; + + if (localName === 'StaticNotificationProps') { + transformedLocalName = 'CalloutProps'; + specifier.local.name = transformedLocalName; + } + // If local name is different, leave it unchanged + } + + // If imported name and local name are the same after transformation, remove the alias + if ( + specifier.local && + specifier.local.name === specifier.imported.name + ) { + delete specifier.local; + } + + // Update the mapping of imported identifiers + // Only add to the map if the local name or the transformed name is different + if (localName !== transformedLocalName) { + importedIdentifiers.set(localName, transformedLocalName); + } + } + }); + }); + + // Deduplicate imports + const importDeclarations = root.find(j.ImportDeclaration); + + importDeclarations.forEach((path) => { + const sourceValue = path.node.source.value; + + // Only deduplicate imports from '@carbon/react' and its subpaths + if (!isCarbonReactImport(sourceValue)) { + return; + } + + const specifiers = path.node.specifiers; + const uniqueSpecifiers = []; + const seen = new Set(); + + specifiers.forEach((specifier) => { + const importedName = specifier.imported.name; + const localName = specifier.local ? specifier.local.name : importedName; + const key = `${importedName}:${localName}`; + + if (!seen.has(key)) { + seen.add(key); + uniqueSpecifiers.push(specifier); + } + }); + + path.node.specifiers = uniqueSpecifiers; + }); + + // Remove empty import declarations + importDeclarations.forEach((path) => { + if (path.node.specifiers.length === 0) { + j(path).remove(); + } + }); + + // Update usages in the code + root.find(j.Identifier).forEach((path) => { + const name = path.node.name; + + // Skip if the identifier is part of an import specifier + if ( + path.parent.node.type === 'ImportSpecifier' || + path.parent.node.type === 'ImportDefaultSpecifier' || + path.parent.node.type === 'ImportNamespaceSpecifier' + ) { + return; + } + + // Only transform identifiers that match the imported identifiers + if (importedIdentifiers.has(name)) { + const transformedName = importedIdentifiers.get(name); + path.node.name = transformedName; + } + }); + + return root.toSource(printOptions); +} + +module.exports = transform;