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(sidepanel): implement decorator prop #6511

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,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 @@ -197,14 +198,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 @@ -312,7 +315,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 @@ -367,7 +371,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,8 @@ $action-set-block-class: #{c4p-settings.$pkg-prefix}--action-set;
}

.#{$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 {
/* stylelint-disable-next-line carbon/theme-token-use */
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 Down Expand Up @@ -675,7 +690,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
Loading