Skip to content

Commit

Permalink
feat(sidepanel): implement decorator prop (#6511)
Browse files Browse the repository at this point in the history
* refactor(useFocus): refactor repeated useEffect code

* feat(sidepanel): implement decorator prop

* test(sidepanel): revert class name

* test(sidepanel): test coverage for decorator

* test(sidepanel): test coverage for decorator

* test(sidepanel): test coverage for decorator

* feat(sidepanel): decorator props changes

* fix(SidePanel): include decorator in remaining stories

---------

Co-authored-by: Alexander Melo <[email protected]>
  • Loading branch information
makafsal and AlexanderMelox authored Dec 10, 2024
1 parent 096a53f commit 336a5b0
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set;
border-right: 1px solid $border-subtle-02;
}
&.#{$block-class}.#{$block-class}--has-slug,
&.#{$block-class}.#{$block-class}--has-ai-label {
&.#{$block-class}.#{$block-class}--has-ai-label,
&.#{$block-class}.#{$block-class}--has-decorator {
border-color: transparent;
box-shadow: inset 0 -80px 70px -65px $ai-inner-shadow,
0 4px 10px 2px $ai-drop-shadow;
Expand Down Expand Up @@ -199,14 +200,16 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set;
&.#{$block-class}:has(.#{$block-class}__action-toolbar),
&.#{$block-class}--has-action-toolbar,
&.#{$block-class}--has-slug,
&.#{$block-class}--has-ai-label {
&.#{$block-class}--has-ai-label,
&.#{$block-class}--has-decorator {
--#{$block-class}--title-padding-right: #{$spacing-10};
}

&.#{$block-class}:has(.#{$block-class}__action-toolbar),
&.#{$block-class}--has-action-toolbar {
&.#{$block-class}--has-slug,
&.#{$block-class}--has-ai-label {
&.#{$block-class}--has-ai-label,
&.#{$block-class}--has-decorator {
--#{$block-class}--title-padding-right: #{$spacing-11};
}
}
Expand Down Expand Up @@ -313,7 +316,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set;
}

&.#{$block-class}--has-slug .#{$block-class}--scrolls,
&.#{$block-class}--has-ai-label .#{$block-class}--scrolls {
&.#{$block-class}--has-ai-label .#{$block-class}--scrolls,
&.#{$block-class}--has-decorator .#{$block-class}--scrolls {
@include utilities.ai-popover-gradient('default', 0, 'layer');

box-shadow: inset 0 -80px 70px -65px $ai-inner-shadow,
Expand Down Expand Up @@ -368,7 +372,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set;
}

.#{$block-class}__slug-and-close,
.#{$block-class}__ai-label-and-close {
.#{$block-class}__ai-label-and-close,
.#{$block-class}__decorator-and-close {
position: absolute;
z-index: 10; /* must be higher than title container border bottom */
top: 0;
Expand Down Expand Up @@ -470,7 +475,9 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set;
inset: 0;
}

/* stylelint-disable-next-line carbon/theme-token-use */
.#{$block-class}--has-slug + .#{$block-class}__overlay,
.#{$block-class}--has-ai-label + .#{$block-class}__overlay {
.#{$block-class}--has-ai-label + .#{$block-class}__overlay,
.#{$block-class}--has-decorator + .#{$block-class}__overlay {
background-color: $ai-overlay;
}
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,19 @@ export default {
'Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level.',
options: [0, 1],
},
decorator: {
control: {
type: 'select',
labels: {
0: 'No AI Label',
1: 'with AI Label',
},
default: 0,
},
description:
'Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level.',
options: [0, 1],
},
},
decorators: [sidePanelDecorator(renderUIShellHeader, prefix)],
};
Expand All @@ -456,6 +469,7 @@ const SlideOverTemplate = ({
actions,
aiLabel,
slug,
decorator,
...args
}) => {
const [open, setOpen] = useState(false);
Expand All @@ -479,6 +493,7 @@ const SlideOverTemplate = ({
ref={testRef}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
{!minimalContent && <ChildrenContent />}
Expand All @@ -492,6 +507,7 @@ const FirstElementDisabledTemplate = ({
actions,
aiLabel,
slug,
decorator,
...args
}) => {
const [open, setOpen] = useState(false);
Expand All @@ -515,6 +531,7 @@ const FirstElementDisabledTemplate = ({
ref={testRef}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
{!minimalContent && (
Expand Down Expand Up @@ -553,7 +570,7 @@ const FirstElementDisabledTemplate = ({
};

// eslint-disable-next-line react/prop-types
const StepTemplate = ({ actions, aiLabel, slug, ...args }) => {
const StepTemplate = ({ actions, aiLabel, slug, decorator, ...args }) => {
const [open, setOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const buttonRef = useRef(undefined);
Expand All @@ -576,6 +593,7 @@ const StepTemplate = ({ actions, aiLabel, slug, ...args }) => {
actions={actionSets[actions]}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
<ChildrenContentWithSteps
Expand All @@ -588,7 +606,7 @@ const StepTemplate = ({ actions, aiLabel, slug, ...args }) => {
};

// eslint-disable-next-line react/prop-types
const SlideInTemplate = ({ actions, aiLabel, slug, ...args }) => {
const SlideInTemplate = ({ actions, aiLabel, slug, decorator, ...args }) => {
const [open, setOpen] = useState(false);
const buttonRef = useRef(undefined);

Expand All @@ -612,6 +630,7 @@ const SlideInTemplate = ({ actions, aiLabel, slug, ...args }) => {
actions={actionSets[actions]}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
<ChildrenContent />
Expand Down Expand Up @@ -675,7 +694,7 @@ export const WithAILabel = SlideOverTemplate.bind({});
WithAILabel.args = {
includeOverlay: true,
actions: 0,
aiLabel: 1,
decorator: 1,
...defaultStoryProps,
};

Expand Down
58 changes: 38 additions & 20 deletions packages/ibm-products/src/components/SidePanel/SidePanel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ const selectorPageContentValue = '#side-panel-test-page-content';
const onRequestCloseFn = jest.fn();
const onUnmountFn = jest.fn();

const sampleAILabel = (
<AILabel className="aiLabel-container" size="xs" align="left-start">
<AILabelContent>
<div>
<p className="secondary">AI Explained</p>
<h1>84%</h1>
<p className="secondary bold">Confidence score</p>
<p className="secondary">
This is not really Lorem Ipsum but the spell checker did not like the
previous text with it&apos;s non-words which is why this unwieldy
sentence, should one choose to call it that, here.
</p>
<hr />
<p className="secondary">Model type</p>
<p className="bold">Foundation model</p>
</div>
</AILabelContent>
</AILabel>
);

const renderSidePanel = ({ ...rest } = {}, children = <p>test</p>) =>
render(
<SidePanel
Expand Down Expand Up @@ -363,35 +383,33 @@ describe('SidePanel', () => {
);
expect(navigationAction).toBeTruthy();
});
it('should not have AI Label when it is not passed', () => {

it('should have AI Label when it is passed through slug', () => {
const { container } = renderSidePanel({
slug: sampleAILabel,
});
expect(container.querySelector('.aiLabel-container')).toBeTruthy();
});

it('should not have a ai label container when a it is not passed', () => {
const { container } = renderSidePanel();
expect(container.querySelector('.aiLabel-container')).toBe(null);
});

it('should have AI Label when it is passed', () => {
const sampleAILabel = (
<AILabel className="aiLabel-container" size="xs" align="left-start">
<AILabelContent>
<div>
<p className="secondary">AI Explained</p>
<h1>84%</h1>
<p className="secondary bold">Confidence score</p>
<p className="secondary">
This is not really Lorem Ipsum but the spell checker did not like
the previous text with it&apos;s non-words which is why this
unwieldy sentence, should one choose to call it that, here.
</p>
<hr />
<p className="secondary">Model type</p>
<p className="bold">Foundation model</p>
</div>
</AILabelContent>
</AILabel>
);
const { container } = renderSidePanel({
aiLabel: sampleAILabel,
});
expect(container.querySelector('.aiLabel-container')).toBeTruthy();
});

it('should have AI Label when it is passed to decorator', () => {
const { container } = renderSidePanel({
decorator: sampleAILabel,
});
expect(container.querySelector('.aiLabel-container')).toBeTruthy();
});

it('should throw console warning if labelText passed without Title', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
Expand Down
52 changes: 39 additions & 13 deletions packages/ibm-products/src/components/SidePanel/SidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,22 @@ type SidePanelBaseProps = {
slideIn?: boolean;

/**
* @deprecated please use the `aiLabel` prop
* @deprecated please use the `decorator` instead
* **Experimental:** Provide a `Slug` component to be rendered inside the `SidePanel` component
*/
slug?: ReactNode;

/**
* @deprecated please use the `decorator` instead
* Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level.
*/
aiLabel?: ReactNode;

/**
* Provide a `decorator` component to be rendered inside the `SidePanel` component
*/
decorator?: ReactNode;

/**
* Sets the subtitle text
*/
Expand Down Expand Up @@ -247,6 +253,7 @@ export let SidePanel = React.forwardRef(
closeIconDescription = defaults.closeIconDescription,
condensedActions,
currentStep = defaults.currentStep,
decorator,
id = blockClass,
includeOverlay,
labelText,
Expand Down Expand Up @@ -670,7 +677,9 @@ export let SidePanel = React.forwardRef(
[`${blockClass}--right-placement`]: placement === 'right',
[`${blockClass}--left-placement`]: placement === 'left',
[`${blockClass}--slide-in`]: slideIn,
[`${blockClass}--has-ai-label`]: !!aiLabel || !!slug,
[`${blockClass}--has-decorator`]: decorator,
[`${blockClass}--has-slug`]: slug,
[`${blockClass}--has-ai-label`]: aiLabel,
[`${blockClass}--condensed-actions`]: condensedActions,
[`${blockClass}--has-overlay`]: includeOverlay,
},
Expand Down Expand Up @@ -704,29 +713,39 @@ export let SidePanel = React.forwardRef(
);

const renderHeader = () => {
const aiLabelCloseSize =
const closeSize =
actions && actions.length && /l/.test(size) ? 'md' : 'sm';
let normalizedAILabel;
let normalizedDecorator;
/**
* slug is deprecated
* can remove this condition in future release
*/
if (slug && slug['type']?.displayName === 'Slug') {
normalizedAILabel = React.cloneElement(
if (slug && slug['type']?.displayName === 'AILabel') {
normalizedDecorator = React.cloneElement(
slug as React.ReactElement<any>,
{
// slug size is sm unless actions and size > md
size: aiLabelCloseSize,
size: closeSize,
}
);
}

if (aiLabel && aiLabel['type']?.displayName === 'AILabel') {
normalizedAILabel = React.cloneElement(
normalizedDecorator = React.cloneElement(
aiLabel as React.ReactElement<any>,
{
// aiLabel size is sm unless actions and size > md
size: aiLabelCloseSize,
size: closeSize,
}
);
}

if (decorator?.['type']?.displayName === 'AILabel') {
normalizedDecorator = React.cloneElement(
decorator as React.ReactElement<any>,
{
// decorator size is sm unless actions and size > md
size: closeSize,
}
);
}
Expand All @@ -745,7 +764,7 @@ export let SidePanel = React.forwardRef(
{currentStep > 0 && (
<Button
kind="ghost"
size={aiLabelCloseSize}
size={closeSize}
disabled={false}
renderIcon={(props) => <ArrowLeft size={20} {...props} />}
iconDescription={navigationBackIconDescription}
Expand All @@ -761,9 +780,9 @@ export let SidePanel = React.forwardRef(
)}
{/* title */}
{title && title.length && renderTitle()}
{/* aiLabel and close */}
<div className={`${blockClass}__ai-label-and-close`}>
{normalizedAILabel}
{/* decorator and close */}
<div className={`${blockClass}__decorator-and-close`}>
{normalizedDecorator}
<IconButton
className={`${blockClass}__close-button`}
label={closeIconDescription}
Expand Down Expand Up @@ -939,6 +958,13 @@ const deprecatedProps = {
* **Experimental:** Provide a `Slug` component to be rendered inside the `SidePanel` component
*/
slug: PropTypes.node,

/**
* **deprecated**
* Please use the `decorator` instead
* Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level.
*/
aiLabel: PropTypes.node,
};

SidePanel.propTypes = {
Expand Down

0 comments on commit 336a5b0

Please sign in to comment.