diff --git a/src/components/ConfigProvider/ConfigProvider.test.tsx b/src/components/ConfigProvider/ConfigProvider.test.tsx index 2e7e0b08f..4dc28144b 100644 --- a/src/components/ConfigProvider/ConfigProvider.test.tsx +++ b/src/components/ConfigProvider/ConfigProvider.test.tsx @@ -13,6 +13,7 @@ import esES from '../Locale/es_ES'; import iconSet from '../Icon/selection.json'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +import { useFeatureFlags } from './FeatureFlagProvider'; Enzyme.configure({ adapter: new Adapter() }); @@ -272,4 +273,22 @@ describe('ConfigProvider', () => { }); expect(result.current.form).toEqual(undefined); }); + + test('Provides feature flag values if provided as a prop', () => { + const { result } = renderHook(() => useFeatureFlags(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + expect(result.current.panelLazyLoadContent).toBeTruthy(); + }); + + test('Provides default feature flag values if not provided as a prop', () => { + const { result } = renderHook(() => useFeatureFlags(), { + wrapper: ConfigProvider, + }); + expect(result.current.panelLazyLoadContent).toBeFalsy(); + }); }); diff --git a/src/components/ConfigProvider/ConfigProvider.tsx b/src/components/ConfigProvider/ConfigProvider.tsx index 790b7feb0..8b7aa3e68 100644 --- a/src/components/ConfigProvider/ConfigProvider.tsx +++ b/src/components/ConfigProvider/ConfigProvider.tsx @@ -1,5 +1,3 @@ -'use client'; - import React, { createContext, FC, @@ -7,26 +5,30 @@ import React, { useEffect, useState, } from 'react'; -import LocaleReceiver from '../LocaleProvider/LocaleReceiver'; + +import { useFocusVisibleClassName } from '../../hooks/useFocusVisibleClassName'; +import { canUseDocElement } from '../../shared/utilities'; +import { OcFormProvider } from '../Form/Internal'; +import { ValidateMessages } from '../Form/Internal/OcForm.types'; +import defaultLocale from '../Locale/Default'; import LocaleProvider from '../LocaleProvider'; -import { registerFont, registerTheme } from './Theming/styleGenerator'; +import LocaleReceiver from '../LocaleProvider/LocaleReceiver'; import { ConfigProviderProps, IConfigContext } from './ConfigProvider.types'; +import { DisabledContextProvider } from './DisabledContext'; +import { FeatureFlagContextProvider } from './FeatureFlagProvider'; +import { GradientContextProvider } from './GradientContext'; +import { ParentComponentsContextProvider } from './ParentComponentsContext'; +import { ShapeContextProvider } from './ShapeContext'; +import { SizeContextProvider } from './SizeContext'; import { + FontOptions, IRegisterFont, IRegisterTheme, - FontOptions, ThemeOptions, } from './Theming'; -import { useFocusVisibleClassName } from '../../hooks/useFocusVisibleClassName'; -import { ParentComponentsContextProvider } from './ParentComponentsContext'; -import { DisabledContextProvider } from './DisabledContext'; -import { GradientContextProvider } from './GradientContext'; -import { ShapeContextProvider } from './ShapeContext'; -import { SizeContextProvider } from './SizeContext'; -import { ValidateMessages } from '../Form/Internal/OcForm.types'; -import { OcFormProvider } from '../Form/Internal'; -import defaultLocale from '../Locale/Default'; -import { canUseDocElement } from '../../shared/utilities'; +import { registerFont, registerTheme } from './Theming/styleGenerator'; + +('use client'); const ConfigContext: React.Context> = createContext< Partial @@ -54,6 +56,7 @@ const ConfigProvider: FC = ({ locale, shape, size, + featureFlags, themeOptions: defaultThemeOptions, }) => { const [fontOptions, setFontOptions] = @@ -160,6 +163,12 @@ const ConfigProvider: FC = ({ ); } + childNode = ( + + {childNode} + + ); + return ( {(_, __) => ( diff --git a/src/components/ConfigProvider/ConfigProvider.types.ts b/src/components/ConfigProvider/ConfigProvider.types.ts index d609bc245..53463cede 100644 --- a/src/components/ConfigProvider/ConfigProvider.types.ts +++ b/src/components/ConfigProvider/ConfigProvider.types.ts @@ -1,17 +1,19 @@ import React from 'react'; + +import { RequiredMark } from '../Form/Form.types'; +import { ValidateMessages } from '../Form/Internal/OcForm.types'; +import { FocusVisibleOptions } from './A11y'; +import { FeatureFlags } from './FeatureFlagProvider'; +import { Shape } from './ShapeContext'; +import { Size } from './SizeContext'; import { + FontOptions, IRegisterFont, IRegisterTheme, - FontOptions, ThemeOptions, } from './Theming'; -import type { Locale } from '../LocaleProvider'; -import { FocusVisibleOptions } from './A11y'; -import { Shape } from './ShapeContext'; -import { Size } from './SizeContext'; -import { ValidateMessages } from '../Form/Internal/OcForm.types'; -import { RequiredMark } from '../Form/Form.types'; +import type { Locale } from '../LocaleProvider'; export type DirectionType = 'ltr' | 'rtl' | undefined; export interface ConfigContextProps { @@ -76,6 +78,10 @@ export interface ConfigProviderProps { * Used by the disabled context provider to disable components. */ disabled?: boolean; + /** + * Options for Octuple features that can be conditionally enabled. + */ + featureFlags?: FeatureFlags; /** * Options for font * @default { fontFamily: 'Source Sans Pro', fontSize: '16px', fontStack: '-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif' } diff --git a/src/components/ConfigProvider/FeatureFlagProvider.tsx b/src/components/ConfigProvider/FeatureFlagProvider.tsx new file mode 100644 index 000000000..b47535054 --- /dev/null +++ b/src/components/ConfigProvider/FeatureFlagProvider.tsx @@ -0,0 +1,54 @@ +'use client'; + +import React, { createContext, FC, useContext, useMemo } from 'react'; + +export interface FeatureFlags { + panelLazyLoadContent?: boolean; +} + +export interface FeatureFlagContextProps { + featureFlags?: FeatureFlags; + children?: React.ReactNode; +} + +const FeatureFlagContext = createContext(undefined); + +const defaultFeatureFlagValues: FeatureFlags = { + /** + * This feature flag configures panels to only render their content if they are currently visible. This feature deprecates the + * `renderContentAlways` option and completely overrides its value when it is enabled. + * it overrides the behvavior of `renderContentAlways` + */ + panelLazyLoadContent: false, +}; + +export const FeatureFlagContextProvider: FC = ({ + children, + featureFlags, +}) => { + const ancestorFeatureFlags = useContext(FeatureFlagContext); + + const currentContext = useMemo( + () => ({ + ...defaultFeatureFlagValues, + ...ancestorFeatureFlags, + ...featureFlags, + }), + [featureFlags, ancestorFeatureFlags] + ); + + return ( + + {children} + + ); +}; + +/** + * Hook to retreive a set of currently configured features flags + * @returns The currently set feature flags. + */ +export const useFeatureFlags = () => + useContext(FeatureFlagContext) || { ...defaultFeatureFlagValues }; + +export default FeatureFlagContext; diff --git a/src/components/Panel/Panel.stories.tsx b/src/components/Panel/Panel.stories.tsx index 83fd41cd4..4d0fa7d30 100644 --- a/src/components/Panel/Panel.stories.tsx +++ b/src/components/Panel/Panel.stories.tsx @@ -1,10 +1,13 @@ -import React, { useState } from 'react'; import { Stories } from '@storybook/addon-docs'; -import { ComponentStory, ComponentMeta } from '@storybook/react'; -import { Panel, PanelSize } from './'; -import { PanelHeader } from './PanelHeader'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import React, { useState } from 'react'; + import { Button, ButtonVariant } from '../Button'; +import { ConfigProvider } from '../ConfigProvider'; +import { FeatureFlagContextProvider } from '../ConfigProvider/FeatureFlagProvider'; import { IconName } from '../Icon'; +import { Panel, PanelSize } from './'; +import { PanelHeader } from './PanelHeader'; export default { title: 'Panel', @@ -117,9 +120,34 @@ export default { options: ['top', 'right', 'bottom', 'left'], control: { type: 'radio' }, }, + panelLazyLoadContent: { + control: { type: 'boolean' }, + defaultValue: false, + }, }, + decorators: [ + (Story, context) => ( + + + + ), + ], } as ComponentMeta; +const FeatureFlag_Story: ComponentStory = ( + args +) => { + return ( + + {args.children} + + ); +}; + const Panel_Story: ComponentStory = (args) => { const [visible, setVisible] = useState(false); return ( @@ -405,6 +433,7 @@ const panelArgs: Object = { footerClassNames: 'my-panel-footer-class', autoFocus: true, focusTrap: true, + featureFlags: undefined, }; Small.args = { diff --git a/src/components/Panel/Panel.test.tsx b/src/components/Panel/Panel.test.tsx index 3adb3aaef..bb129b4de 100644 --- a/src/components/Panel/Panel.test.tsx +++ b/src/components/Panel/Panel.test.tsx @@ -6,6 +6,8 @@ import { Panel } from './'; import { Button, ButtonVariant } from '../Button'; import { IconName } from '../Icon'; import { render } from '@testing-library/react'; +import { ConfigProvider } from '../ConfigProvider'; +import { FeatureFlagContextProvider } from '../ConfigProvider/FeatureFlagProvider'; Enzyme.configure({ adapter: new Adapter() }); @@ -163,4 +165,52 @@ describe('Panel', () => { ); expect(queryByText('Content is not always rendered')).toBeNull(); }); + + test('Should not lazy load content when panelLazyLoadContent is false', () => { + const { queryByText } = render( + + + Content is not always rendered + + + ); + expect(queryByText('Content is not always rendered')).toBeTruthy(); + }); + + test('Should not render content when panelLazyLoadContent is true and panel is hidden', () => { + const { queryByText } = render( + + + Content is not always rendered + + + ); + expect(queryByText('Content is not always rendered')).toBeNull(); + }); + + test('Should render content when panelLazyLoadContent is true and panel is visible', () => { + const { queryByText } = render( + + + Content is not always rendered + + + ); + expect(queryByText('Content is not always rendered')).toBeTruthy(); + }); + + test('Should respect render content always when panelLazyLoadContent is false', () => { + const { queryByText } = render( + + + Content is not always rendered + + + ); + expect(queryByText('Content is not always rendered')).toBeTruthy(); + }); }); diff --git a/src/components/Panel/Panel.tsx b/src/components/Panel/Panel.tsx index 2c5e8b71e..aab44ee56 100644 --- a/src/components/Panel/Panel.tsx +++ b/src/components/Panel/Panel.tsx @@ -17,6 +17,7 @@ import { Button, ButtonShape, ButtonVariant } from '../Button'; import { IconName } from '../Icon'; import { Portal } from '../Portal'; import { FocusTrap } from '../../shared/FocusTrap'; +import { useFeatureFlags } from '../ConfigProvider/FeatureFlagProvider'; import { NoFormStyle } from '../Form/Context'; import { useCanvasDirection } from '../../hooks/useCanvasDirection'; import { useScrollLock } from '../../hooks/useScrollLock'; @@ -120,6 +121,9 @@ export const Panel = React.forwardRef( mergedLocale = panelLocale || props.locale; } + const { panelLazyLoadContent } = useFeatureFlags(); + const renderContent = panelLazyLoadContent ? visible : renderContentAlways; + const [closeButtonAriaLabelText, setCloseButtonAriaLabelText] = useState(defaultCloseButtonAriaLabelText); @@ -360,7 +364,7 @@ export const Panel = React.forwardRef( onClick={stopPropagation} style={getPanelStyle()} > - {renderContentAlways && ( + {renderContent && ( <> {getHeader()} {getBody()} diff --git a/src/components/Panel/Panel.types.ts b/src/components/Panel/Panel.types.ts index bb1ac35a5..85f23d9d7 100644 --- a/src/components/Panel/Panel.types.ts +++ b/src/components/Panel/Panel.types.ts @@ -201,6 +201,7 @@ export interface PanelProps extends Omit, 'title'> { push?: boolean; /** * Whether to render Panel content when Panel `visible` is `false`. + * @deprecated Use the feature flag panelLazyLoadContent instead. * @default true */ renderContentAlways?: boolean;