Skip to content

Commit

Permalink
feat: feature flags: Add feature flag config for Octuple (#875)
Browse files Browse the repository at this point in the history
* feat: feature flags: Add feature flag config for Octuple

Add option to lazy load panels by default

* rename property

* add jsdoc for the flag
  • Loading branch information
dcoblentz-eightfold authored Aug 22, 2024
1 parent e652582 commit 9fc6b07
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 27 deletions.
19 changes: 19 additions & 0 deletions src/components/ConfigProvider/ConfigProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() });

Expand Down Expand Up @@ -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 }) => (
<ConfigProvider featureFlags={{ panelLazyLoadContent: true }}>
{children}
</ConfigProvider>
),
});
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();
});
});
39 changes: 24 additions & 15 deletions src/components/ConfigProvider/ConfigProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
'use client';

import React, {
createContext,
FC,
useContext,
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<Partial<IConfigContext>> = createContext<
Partial<IConfigContext>
Expand Down Expand Up @@ -54,6 +56,7 @@ const ConfigProvider: FC<ConfigProviderProps> = ({
locale,
shape,
size,
featureFlags,
themeOptions: defaultThemeOptions,
}) => {
const [fontOptions, setFontOptions] =
Expand Down Expand Up @@ -160,6 +163,12 @@ const ConfigProvider: FC<ConfigProviderProps> = ({
);
}

childNode = (
<FeatureFlagContextProvider featureFlags={featureFlags}>
{childNode}
</FeatureFlagContextProvider>
);

return (
<LocaleReceiver>
{(_, __) => (
Expand Down
20 changes: 13 additions & 7 deletions src/components/ConfigProvider/ConfigProvider.types.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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' }
Expand Down
54 changes: 54 additions & 0 deletions src/components/ConfigProvider/FeatureFlagProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<FeatureFlags>(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<FeatureFlagContextProps> = ({
children,
featureFlags,
}) => {
const ancestorFeatureFlags = useContext(FeatureFlagContext);

const currentContext = useMemo(
() => ({
...defaultFeatureFlagValues,
...ancestorFeatureFlags,
...featureFlags,
}),
[featureFlags, ancestorFeatureFlags]
);

return (
<FeatureFlagContext.Provider value={currentContext}>
{children}
</FeatureFlagContext.Provider>
);
};

/**
* 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;
37 changes: 33 additions & 4 deletions src/components/Panel/Panel.stories.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -117,9 +120,34 @@ export default {
options: ['top', 'right', 'bottom', 'left'],
control: { type: 'radio' },
},
panelLazyLoadContent: {
control: { type: 'boolean' },
defaultValue: false,
},
},
decorators: [
(Story, context) => (
<ConfigProvider
featureFlags={{
panelLazyLoadContent: context.args.panelLazyLoadContent,
}}
>
<Story />
</ConfigProvider>
),
],
} as ComponentMeta<typeof Panel>;

const FeatureFlag_Story: ComponentStory<typeof FeatureFlagContextProvider> = (
args
) => {
return (
<ConfigProvider featureFlags={args.featureFlags}>
{args.children}
</ConfigProvider>
);
};

const Panel_Story: ComponentStory<typeof Panel> = (args) => {
const [visible, setVisible] = useState<boolean>(false);
return (
Expand Down Expand Up @@ -405,6 +433,7 @@ const panelArgs: Object = {
footerClassNames: 'my-panel-footer-class',
autoFocus: true,
focusTrap: true,
featureFlags: undefined,
};

Small.args = {
Expand Down
50 changes: 50 additions & 0 deletions src/components/Panel/Panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() });

Expand Down Expand Up @@ -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(
<FeatureFlagContextProvider
featureFlags={{ panelLazyLoadContent: false }}
>
<Panel>
<div>Content is not always rendered</div>
</Panel>
</FeatureFlagContextProvider>
);
expect(queryByText('Content is not always rendered')).toBeTruthy();
});

test('Should not render content when panelLazyLoadContent is true and panel is hidden', () => {
const { queryByText } = render(
<FeatureFlagContextProvider featureFlags={{ panelLazyLoadContent: true }}>
<Panel visible={false}>
<div>Content is not always rendered</div>
</Panel>
</FeatureFlagContextProvider>
);
expect(queryByText('Content is not always rendered')).toBeNull();
});

test('Should render content when panelLazyLoadContent is true and panel is visible', () => {
const { queryByText } = render(
<FeatureFlagContextProvider featureFlags={{ panelLazyLoadContent: true }}>
<Panel visible>
<div>Content is not always rendered</div>
</Panel>
</FeatureFlagContextProvider>
);
expect(queryByText('Content is not always rendered')).toBeTruthy();
});

test('Should respect render content always when panelLazyLoadContent is false', () => {
const { queryByText } = render(
<FeatureFlagContextProvider
featureFlags={{ panelLazyLoadContent: false }}
>
<Panel renderContentAlways>
<div>Content is not always rendered</div>
</Panel>
</FeatureFlagContextProvider>
);
expect(queryByText('Content is not always rendered')).toBeTruthy();
});
});
6 changes: 5 additions & 1 deletion src/components/Panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -120,6 +121,9 @@ export const Panel = React.forwardRef<PanelRef, PanelProps>(
mergedLocale = panelLocale || props.locale;
}

const { panelLazyLoadContent } = useFeatureFlags();
const renderContent = panelLazyLoadContent ? visible : renderContentAlways;

const [closeButtonAriaLabelText, setCloseButtonAriaLabelText] =
useState<string>(defaultCloseButtonAriaLabelText);

Expand Down Expand Up @@ -360,7 +364,7 @@ export const Panel = React.forwardRef<PanelRef, PanelProps>(
onClick={stopPropagation}
style={getPanelStyle()}
>
{renderContentAlways && (
{renderContent && (
<>
{getHeader()}
{getBody()}
Expand Down
1 change: 1 addition & 0 deletions src/components/Panel/Panel.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export interface PanelProps extends Omit<OcBaseProps<HTMLElement>, 'title'> {
push?: boolean;
/**
* Whether to render Panel content when Panel `visible` is `false`.
* @deprecated Use the feature flag panelLazyLoadContent instead.
* @default true
*/
renderContentAlways?: boolean;
Expand Down

0 comments on commit 9fc6b07

Please sign in to comment.