Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: feature flags: Add feature flag config for Octuple #875

Merged
merged 5 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading