diff --git a/packages/feature-flags/feature-flags.yml b/packages/feature-flags/feature-flags.yml index 203a7057b9d1..209458fc544b 100644 --- a/packages/feature-flags/feature-flags.yml +++ b/packages/feature-flags/feature-flags.yml @@ -9,11 +9,6 @@ feature-flags: - name: enable-css-custom-properties description: Describe what the flag does enabled: false - - name: enable-use-controlled-state-with-value - description: > - Enable components to be created in either a controlled or uncontrolled - mode - enabled: false - name: enable-css-grid description: > Enable CSS Grid Layout in the Grid and Column React components diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 927fae777c62..9b982d35f52b 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -9801,14 +9801,22 @@ Map { "children": Object { "type": "node", }, - "flags": Object { - "args": Array [ - Object { - "type": "bool", - }, - ], - "type": "objectOf", + "enableExperimentalFocusWrapWithoutSentinels": Object { + "type": "bool", + }, + "enableTreeviewControllable": Object { + "type": "bool", + }, + "enableV12Overflowmenu": Object { + "type": "bool", + }, + "enableV12TileDefaultIcons": Object { + "type": "bool", + }, + "enableV12TileRadioIcons": Object { + "type": "bool", }, + "flags": [Function], }, }, "unstable_Layout" => Object { diff --git a/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js b/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js index 0b423bc56490..798ac1de8431 100644 --- a/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js +++ b/packages/react/src/components/FeatureFlags/__tests__/FeatureFlags-test.js @@ -32,8 +32,8 @@ describe('FeatureFlags', () => { expect(checkFlags).toHaveBeenLastCalledWith(true); expect(checkFlag).toHaveBeenLastCalledWith(true); }); - - it('should provide access to the feature flags for a scope', () => { + it('should provide access to the feature flags for a scope through deprecated flags prop', () => { + consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const checkFlags = jest.fn(); const checkFlag = jest.fn(); @@ -69,6 +69,49 @@ describe('FeatureFlags', () => { a: true, b: false, }); + consoleSpy.mockRestore(); + }); + + it('should provide access to the feature flags for a scope', () => { + const checkFlags = jest.fn(); + const checkFlag = jest.fn(); + + function TestComponent() { + const featureFlags = useFeatureFlags(); + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + const enableTreeviewControllable = useFeatureFlag( + 'enable-treeview-controllable' + ); + + checkFlags({ + enableV12Overflowmenu: featureFlags.enabled('enable-v12-overflowmenu'), + enableTreeviewControllable: featureFlags.enabled( + 'enable-treeview-controllable' + ), + }); + + checkFlag({ + enableV12Overflowmenu, + enableTreeviewControllable, + }); + + return null; + } + + render( + + + + ); + + expect(checkFlags).toHaveBeenLastCalledWith({ + enableV12Overflowmenu: true, + enableTreeviewControllable: false, + }); + expect(checkFlag).toHaveBeenLastCalledWith({ + enableV12Overflowmenu: true, + enableTreeviewControllable: false, + }); }); it('should re-render when flags change', () => { @@ -77,95 +120,101 @@ describe('FeatureFlags', () => { function TestComponent() { const featureFlags = useFeatureFlags(); - const a = useFeatureFlag('a'); - const b = useFeatureFlag('b'); + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + const enableTreeviewControllable = useFeatureFlag( + 'enable-treeview-controllable' + ); checkFlags({ - a: featureFlags.enabled('a'), - b: featureFlags.enabled('b'), + enableV12Overflowmenu: featureFlags.enabled('enable-v12-overflowmenu'), + enableTreeviewControllable: featureFlags.enabled( + 'enable-treeview-controllable' + ), }); checkFlag({ - a, - b, + enableV12Overflowmenu, + enableTreeviewControllable, }); return null; } const { rerender } = render( - + ); expect(checkFlags).toHaveBeenLastCalledWith({ - a: true, - b: false, + enableV12Overflowmenu: true, + enableTreeviewControllable: false, }); expect(checkFlag).toHaveBeenLastCalledWith({ - a: true, - b: false, + enableV12Overflowmenu: true, + enableTreeviewControllable: false, }); rerender( - + ); expect(checkFlags).toHaveBeenLastCalledWith({ - a: false, - b: true, + enableV12Overflowmenu: false, + enableTreeviewControllable: true, }); expect(checkFlag).toHaveBeenLastCalledWith({ - a: false, - b: true, + enableV12Overflowmenu: false, + enableTreeviewControllable: true, }); }); it('should merge scopes and overwrite duplicate keys', () => { - GlobalFeatureFlags.add('global', true); - const checkFlag = jest.fn(); function TestComponent() { - const global = useFeatureFlag('global'); - const local = useFeatureFlag('local'); + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + const enableTreeviewControllable = useFeatureFlag( + 'enable-treeview-controllable' + ); - checkFlag({ global, local }); + checkFlag({ enableV12Overflowmenu, enableTreeviewControllable }); return null; } render( - + ); expect(checkFlag).toHaveBeenLastCalledWith({ - global: true, - local: true, + enableV12Overflowmenu: false, + enableTreeviewControllable: true, }); render( - - + + ); expect(checkFlag).toHaveBeenLastCalledWith({ - global: false, - local: true, + enableV12Overflowmenu: true, + enableTreeviewControllable: false, }); render( - - - + + + @@ -173,8 +222,90 @@ describe('FeatureFlags', () => { ); expect(checkFlag).toHaveBeenLastCalledWith({ - global: false, - local: false, + enableV12Overflowmenu: false, + enableTreeviewControllable: false, + }); + }); + it('should handle boolean props and flags object with no overlapping keys', () => { + const checkFlags = jest.fn(); + const checkFlag = jest.fn(); + + function TestComponent() { + const featureFlags = useFeatureFlags(); + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + const enableExperimentalFocusWrapWithoutSentinels = useFeatureFlag( + 'enable-experimental-focus-wrap-without-sentinels' + ); + + checkFlags({ + enableV12Overflowmenu: featureFlags.enabled('enable-v12-overflowmenu'), + enableExperimentalFocusWrapWithoutSentinels: featureFlags.enabled( + 'enable-experimental-focus-wrap-without-sentinels' + ), + }); + + checkFlag({ + enableV12Overflowmenu, + enableExperimentalFocusWrapWithoutSentinels, + }); + + return null; + } + + render( + + + + ); + + expect(checkFlags).toHaveBeenLastCalledWith({ + enableV12Overflowmenu: false, + enableExperimentalFocusWrapWithoutSentinels: true, + }); + expect(checkFlag).toHaveBeenLastCalledWith({ + enableV12Overflowmenu: false, + enableExperimentalFocusWrapWithoutSentinels: true, + }); + }); + it('should handle boolean props correctly when no flags object is provided', () => { + const checkFlags = jest.fn(); + const checkFlag = jest.fn(); + + function TestComponent() { + const featureFlags = useFeatureFlags(); + const enableV12Overflowmenu = useFeatureFlag('enable-v12-overflowmenu'); + const enableTreeviewControllable = useFeatureFlag( + 'enable-treeview-controllable' + ); + + checkFlags({ + enableV12Overflowmenu: featureFlags.enabled('enable-v12-overflowmenu'), + enableTreeviewControllable: featureFlags.enabled( + 'enable-treeview-controllable' + ), + }); + + checkFlag({ + enableV12Overflowmenu, + enableTreeviewControllable, + }); + + return null; + } + + render( + + + + ); + + expect(checkFlags).toHaveBeenLastCalledWith({ + enableV12Overflowmenu: true, + enableTreeviewControllable: false, + }); + expect(checkFlag).toHaveBeenLastCalledWith({ + enableV12Overflowmenu: true, + enableTreeviewControllable: false, }); }); }); diff --git a/packages/react/src/components/FeatureFlags/index.js b/packages/react/src/components/FeatureFlags/index.js index f8b00c764b4a..4d1a270b3f2b 100644 --- a/packages/react/src/components/FeatureFlags/index.js +++ b/packages/react/src/components/FeatureFlags/index.js @@ -17,7 +17,7 @@ import React, { useRef, useState, } from 'react'; - +import deprecate from '../../prop-types/deprecate'; /** * Our FeatureFlagContext is used alongside the FeatureFlags component to enable * or disable feature flags in a given React tree @@ -29,17 +29,35 @@ const FeatureFlagContext = createContext(GlobalFeatureFlags); * along with the current `FeatureFlagContext` to provide consumers to check if * a feature flag is enabled or disabled in a given React tree */ -function FeatureFlags({ children, flags = {} }) { +function FeatureFlags({ + children, + flags = {}, + enableV12TileDefaultIcons = false, + enableV12TileRadioIcons = false, + enableV12Overflowmenu = false, + enableTreeviewControllable = false, + enableExperimentalFocusWrapWithoutSentinels = false, +}) { const parentScope = useContext(FeatureFlagContext); const [prevParentScope, setPrevParentScope] = useState(parentScope); + + const combinedFlags = { + 'enable-v12-tile-default-icons': enableV12TileDefaultIcons, + 'enable-v12-tile-radio-icons': enableV12TileRadioIcons, + 'enable-v12-overflowmenu': enableV12Overflowmenu, + 'enable-treeview-controllable': enableTreeviewControllable, + 'enable-experimental-focus-wrap-without-sentinels': + enableExperimentalFocusWrapWithoutSentinels, + ...flags, + }; const [scope, updateScope] = useState(() => { - const scope = createScope(flags); + const scope = createScope(combinedFlags); scope.mergeWithScope(parentScope); return scope; }); if (parentScope !== prevParentScope) { - const scope = createScope(flags); + const scope = createScope(combinedFlags); scope.mergeWithScope(parentScope); updateScope(scope); setPrevParentScope(parentScope); @@ -48,7 +66,7 @@ function FeatureFlags({ children, flags = {} }) { // We use a custom hook to detect if any of the keys or their values change // for flags that are passed in. If they have changed, then we re-create the // FeatureFlagScope using the new flags - useChangedValue(flags, isEqual, (changedFlags) => { + useChangedValue(combinedFlags, isEqual, (changedFlags) => { const scope = createScope(changedFlags); scope.mergeWithScope(parentScope); updateScope(scope); @@ -65,9 +83,19 @@ FeatureFlags.propTypes = { children: PropTypes.node, /** - * Provide the feature flags to enabled or disabled in the current React tree + * Provide the feature flags to enabled or disabled in the current Rea,ct tree */ - flags: PropTypes.objectOf(PropTypes.bool), + flags: deprecate( + PropTypes.objectOf(PropTypes.bool), + 'The `flags` prop for `FeatureFlag` has ' + + 'been deprecated. Please run the `featureflag-deprecate-flags-prop` codemod to migrate to individual boolean props.' + + `npx @carbon/upgrade migrate featureflag-deprecate-flags-prop --write` + ), + enableV12TileDefaultIcons: PropTypes.bool, + enableV12TileRadioIcons: PropTypes.bool, + enableV12Overflowmenu: PropTypes.bool, + enableTreeviewControllable: PropTypes.bool, + enableExperimentalFocusWrapWithoutSentinels: PropTypes.bool, }; /** @@ -113,7 +141,8 @@ function useChangedValue(value, compare, callback) { */ function useFeatureFlag(flag) { const scope = useContext(FeatureFlagContext); - return scope.enabled(flag); + //updated to return false for undefined flags + return scope.enabled(flag) ?? false; } /** diff --git a/packages/react/src/components/FeatureFlags/overview.mdx b/packages/react/src/components/FeatureFlags/overview.mdx index 2cebe7d4d1b0..410b0f632d7e 100644 --- a/packages/react/src/components/FeatureFlags/overview.mdx +++ b/packages/react/src/components/FeatureFlags/overview.mdx @@ -114,3 +114,33 @@ Feature flags can also be enabled via the provided `enable()` mixin @include feature-flags.enable('enable-experimental-tile-contrast'); ``` + +## FeatureFlags Prop Update + +The `FeatureFlags` component has been updated to improve compatibility. The `flags` object prop is now deprecated and is replaced with individual boolean props for each feature flag. + +The `flags` prop will be removed in a future release. Instead, use individual boolean props for each feature flag. +A `featureflag-deprecate-flags-prop` codemod has been provided to help deprecate the `flags` object prop and switch to individual boolean props. + + +```bash +npx @carbon/upgrade migrate featureflag-deprecate-flags-prop --write +``` + +```jsx +//Before migration + + + + + + +//After migration + + + + +``` \ No newline at end of file diff --git a/packages/react/src/components/OverflowMenuV2/index.js b/packages/react/src/components/OverflowMenuV2/index.js index e4d93b18b686..4c70b357926b 100644 --- a/packages/react/src/components/OverflowMenuV2/index.js +++ b/packages/react/src/components/OverflowMenuV2/index.js @@ -25,10 +25,7 @@ function OverflowMenuV2(props) { } return ( - + ); diff --git a/packages/react/src/components/TileGroup/__tests__/TileGroup-test.js b/packages/react/src/components/TileGroup/__tests__/TileGroup-test.js index 17e63fb4b2a4..2d028cd941d5 100644 --- a/packages/react/src/components/TileGroup/__tests__/TileGroup-test.js +++ b/packages/react/src/components/TileGroup/__tests__/TileGroup-test.js @@ -198,10 +198,7 @@ describe('TileGroup', () => { //Feature flag : enable-v12-tile-radio-icons it('should keep radio unselected if no `defaultSelected` is provided', () => { render( - + Option 1 diff --git a/packages/styles/scss/_feature-flags.scss b/packages/styles/scss/_feature-flags.scss index 20029d6d6989..69125e4cb100 100644 --- a/packages/styles/scss/_feature-flags.scss +++ b/packages/styles/scss/_feature-flags.scss @@ -8,7 +8,6 @@ @forward '@carbon/feature-flags' with ( $feature-flags: ( 'enable-css-custom-properties': true, - 'enable-use-controlled-state-with-value': true, 'enable-css-grid': true, 'enable-v11-release': true, 'enable-experimental-tile-contrast': false, diff --git a/packages/upgrade/src/upgrades.js b/packages/upgrade/src/upgrades.js index 486937bb7a06..18a2699573d3 100644 --- a/packages/upgrade/src/upgrades.js +++ b/packages/upgrade/src/upgrades.js @@ -291,6 +291,46 @@ export const upgrades = [ }); }, }, + { + name: 'featureflag-deprecate-flags-prop', + description: ` + Updates the component usage: + 1. Deprecates the 'flags' object prop + 2. Replaces it with individual boolean props for each feature flag + 3. Removes usage of no longer needed flags (e.g., 'enable-v11-release') + + Example transformation: + Before: + After: + `, + + migrate: async (options) => { + const transform = path.join( + TRANSFORM_DIR, + 'featureflag-deprecate-flags-prop.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__/featureflag-deprecate-flags-prop.input.js b/packages/upgrade/transforms/__testfixtures__/featureflag-deprecate-flags-prop.input.js new file mode 100644 index 000000000000..0de313b749e7 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/featureflag-deprecate-flags-prop.input.js @@ -0,0 +1,134 @@ +import { FeatureFlags } from '../FeatureFlags'; +import { + RadioTile, + TileGroup, + TreeView, + VStack, + TreeNode, + OverflowMenu, + MenuItem, +} from '@carbon/react'; +import { Document, Folder } from '@carbon/icons-react'; +export const EnableV12TileDefaultIconsFlag = () => { + return ( + + + + Option 1 + + + Option 2 + + + + ); +}; +export const EnableExperimentalFocusWrapWithoutSentinels = () => { + return ( + // prettier-ignore + + + + ); +}; +export const EnableTreeviewControllable = (args) => { + return ( +
+ + { + console.log('test'); + }} + selected={[]} + onSelect={() => { + console.log('test'); + }}> + {renderTree(nodes)} + + +
+ ); +}; +export const EnableV12Overflowmenu = () => { + return ( + + + + + + + + + + + ); +}; +export const EnableV12TileRadioIcons = () => { + return ( + + + + Option 1 + + + Option 2 + + + + ); +}; +export const TestRegularJsx = () => { + return
; +}; +export const CombinedFlags = () => { + return ( + // prettier-ignore + + + + Option 1 + + + Option 2 + + + + ); +}; +export const OldFlags = () => { + return ( + + + + Option 1 + + + Option 2 + + + + ); +}; diff --git a/packages/upgrade/transforms/__testfixtures__/featureflag-deprecate-flags-prop.output.js b/packages/upgrade/transforms/__testfixtures__/featureflag-deprecate-flags-prop.output.js new file mode 100644 index 000000000000..4fff2eb0a5f5 --- /dev/null +++ b/packages/upgrade/transforms/__testfixtures__/featureflag-deprecate-flags-prop.output.js @@ -0,0 +1,124 @@ +import { FeatureFlags } from '../FeatureFlags'; +import { + RadioTile, + TileGroup, + TreeView, + VStack, + TreeNode, + OverflowMenu, + MenuItem, +} from '@carbon/react'; +import { Document, Folder } from '@carbon/icons-react'; +export const EnableV12TileDefaultIconsFlag = () => { + return ( + ( + + + Option 1 + + + Option 2 + + + ) + ); +}; +export const EnableExperimentalFocusWrapWithoutSentinels = () => { + return ( + // prettier-ignore + ( + + ) + ); +}; +export const EnableTreeviewControllable = (args) => { + return ( + (
+ + { + console.log('test'); + }} + selected={[]} + onSelect={() => { + console.log('test'); + }}> + {renderTree(nodes)} + + +
) + ); +}; +export const EnableV12Overflowmenu = () => { + return ( + ( + + + + + + + + + ) + ); +}; +export const EnableV12TileRadioIcons = () => { + return ( + ( + + + Option 1 + + + Option 2 + + + ) + ); +}; +export const TestRegularJsx = () => { + return
; +}; +export const CombinedFlags = () => { + return ( + // prettier-ignore + ( + + + Option 1 + + + Option 2 + + + ) + ); +}; +export const OldFlags = () => { + return ( + ( + + + Option 1 + + + Option 2 + + + ) + ); +}; diff --git a/packages/upgrade/transforms/__tests__/featureflag-deprecate-flags-prop-test.js b/packages/upgrade/transforms/__tests__/featureflag-deprecate-flags-prop-test.js new file mode 100644 index 000000000000..be9af914cd88 --- /dev/null +++ b/packages/upgrade/transforms/__tests__/featureflag-deprecate-flags-prop-test.js @@ -0,0 +1,12 @@ +/** + * 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, 'featureflag-deprecate-flags-prop'); diff --git a/packages/upgrade/transforms/featureflag-deprecate-flags-prop.js b/packages/upgrade/transforms/featureflag-deprecate-flags-prop.js new file mode 100644 index 000000000000..98bd87f9a24c --- /dev/null +++ b/packages/upgrade/transforms/featureflag-deprecate-flags-prop.js @@ -0,0 +1,80 @@ +/** + * Copyright IBM Corp. 2024 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * Migrate the `flags` object prop to individual boolean props + * + * Transforms: + * + * + * + * Into: + * + * + */ + +'use strict'; + +const defaultOptions = { + quote: 'single', + trailingComma: true, +}; + +//This list can be updated as needed, if any flags are made true by default +const flagsToRemove = [ + 'enable-v11-release', + 'enable-css-custom-properties', + 'enable-css-grid', +]; + +function transform(fileInfo, api, options) { + const { jscodeshift: j } = api; + const root = j(fileInfo.source); + const printOptions = options.printOptions || defaultOptions; + if ( + !root.find(j.JSXOpeningElement, { name: { name: 'FeatureFlags' } }).size() + ) { + return null; // if no FeatureFlags found, don't modify & return the file + } + root + .find(j.JSXOpeningElement, { name: { name: 'FeatureFlags' } }) + .forEach((path) => { + const flagsAttribute = path.node.attributes.find( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'flags' + ); + + if (flagsAttribute?.value?.expression?.type === 'ObjectExpression') { + const properties = flagsAttribute.value.expression.properties; + + // Filter out flags to remove + const filteredProperties = properties.filter((prop) => { + const keyName = + prop.key.type === 'Identifier' ? prop.key.name : prop.key.value; + return !flagsToRemove.includes(keyName); + }); + + // Convert remaining flags to boolean props + const newAttributes = filteredProperties + .filter((flag) => flag.value.value === true) + .map((flag) => { + const flagName = + flag.key.type === 'Identifier' ? flag.key.name : flag.key.value; + const propName = flagName.replace(/-(\w)/g, (_, c) => + c.toUpperCase() + ); + return j.jsxAttribute(j.jsxIdentifier(propName)); + }); + + path.node.attributes = [ + ...path.node.attributes.filter((attr) => attr.name.name !== 'flags'), + ...newAttributes, + ]; + } + }); + + return root.toSource(printOptions); +} + +module.exports = transform;