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(TermsOfUse): Add Terms of Use modal #349

Merged
merged 8 commits into from
Dec 17, 2024
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import { Button, Checkbox, FormGroup, Radio, SkipToContent } from '@patternfly/react-core';
import TermsOfUse from '@patternfly/chatbot/dist/dynamic/TermsOfUse';
import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';

export const TermsOfUseExample: React.FunctionComponent = () => {
const [isModalOpen, setIsModalOpen] = React.useState(true);
const [displayMode, setDisplayMode] = React.useState(ChatbotDisplayMode.default);
const [hasImage, setHasImage] = React.useState(true);
const chatbotRef = React.useRef<HTMLDivElement>(null);
const termsRef = React.useRef<HTMLDivElement>(null);

const handleSkipToContent = (e) => {
e.preventDefault();
if (!isModalOpen && chatbotRef.current) {
chatbotRef.current.focus();
}
if (isModalOpen && termsRef.current) {
termsRef.current.focus();
}
};

const handleModalToggle = (_event: React.MouseEvent | MouseEvent | KeyboardEvent) => {
setIsModalOpen(!isModalOpen);
};

const onPrimaryAction = () => {
// eslint-disable-next-line no-console
console.log('Clicked primary action');
};

const onSecondaryAction = () => {
// eslint-disable-next-line no-console
console.log('Clicked secondary action');
};

const introduction = (
<>
<p>
Welcome to PatternFly! These terms and conditions outline the rules and regulations for the use of PatternFly's
website, located at <a href="https://patternfly.org">www.patternfly.org.</a>
</p>
<p>
By accessing this website, you are agreeing with our terms and conditions. If you do not agree to all of these
terms and conditions, do not continue to use PatternFly.
</p>
</>
);

const terminology = (
<>
<p>
The following terminology applies to these Terms and Conditions, Privacy Statement, Disclaimer Notice, and all
Agreements:
</p>
<ul>
<li>
"Client", "You", and "Your" refer to you, the person using this website who is compliant with the Company's
terms and conditions.
</li>
<li>
"The Company", "Ourselves", "We", "Our", and "Us", refer to our Company. "Party", "Parties", or "Us", refers
to both the Client and ourselves.
</li>
</ul>
</>
);

const body = (
<>
<h2>Introduction</h2>
{introduction}
<h2>Terminology</h2>
{terminology}
</>
);

return (
<>
<SkipToContent style={{ zIndex: '999' }} onClick={handleSkipToContent} href="#">
Skip to chatbot
</SkipToContent>
<div
style={{
position: 'fixed',
padding: 'var(--pf-t--global--spacer--lg)',
zIndex: '601',
boxShadow: 'var(--pf-t--global--box-shadow--lg)'
}}
>
<FormGroup role="radiogroup" isInline fieldId="basic-form-radio-group" label="Display mode">
<Radio
isChecked={displayMode === ChatbotDisplayMode.default}
onChange={() => setDisplayMode(ChatbotDisplayMode.default)}
name="basic-inline-radio"
label="Default"
id="default"
/>
<Radio
isChecked={displayMode === ChatbotDisplayMode.docked}
onChange={() => setDisplayMode(ChatbotDisplayMode.docked)}
name="basic-inline-radio"
label="Docked"
id="docked"
/>
<Radio
isChecked={displayMode === ChatbotDisplayMode.fullscreen}
onChange={() => setDisplayMode(ChatbotDisplayMode.fullscreen)}
rebeccaalpert marked this conversation as resolved.
Show resolved Hide resolved
name="basic-inline-radio"
label="Fullscreen"
id="fullscreen"
/>
<Radio
isChecked={displayMode === ChatbotDisplayMode.embedded}
onChange={() => setDisplayMode(ChatbotDisplayMode.embedded)}
name="basic-inline-radio"
label="Embedded"
id="embedded"
/>
</FormGroup>
<Checkbox
isChecked={hasImage}
aria-label="Toggle whether terms and conditions has a header image"
id="toggle-header-image"
name="toggle-header-image"
label="Has image in header"
onChange={(_event, checked) => setHasImage(checked)}
></Checkbox>
<Button onClick={handleModalToggle}>Launch modal</Button>
</div>
<Chatbot ref={chatbotRef} displayMode={displayMode} isVisible></Chatbot>
<TermsOfUse
ref={termsRef}
displayMode={displayMode}
isModalOpen={isModalOpen}
handleModalToggle={handleModalToggle}
onPrimaryAction={onPrimaryAction}
onSecondaryAction={onSecondaryAction}
image={hasImage ? termsAndConditionsHeader : undefined}
altText={hasImage ? 'Open book' : undefined}
>
{body}
</TermsOfUse>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import FileDetailsLabel from '@patternfly/chatbot/dist/dynamic/FileDetailsLabel'
import FileDropZone from '@patternfly/chatbot/dist/dynamic/FileDropZone';
import { PreviewAttachment } from '@patternfly/chatbot/dist/dynamic/PreviewAttachment';
import ChatbotAlert from '@patternfly/chatbot/dist/dynamic/ChatbotAlert';
import TermsOfUse from '@patternfly/chatbot/dist/dynamic/TermsOfUse';
import {
ChatbotHeader,
ChatbotHeaderMain,
Expand All @@ -78,6 +79,7 @@ import PFHorizontalLogoColor from './PF-HorizontalLogo-Color.svg';
import PFHorizontalLogoReverse from './PF-HorizontalLogo-Reverse.svg';
import userAvatar from '../Messages/user_avatar.svg';
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';

## Structure

Expand Down Expand Up @@ -365,6 +367,18 @@ If you're showing a conversation that is already active, you can set the `active

```

### Terms of use

Based on the [PatternFly modal](/components/modal), this modal adapts to the ChatBot display mode and is meant to display terms and conditions for using a ChatBot in your project. The image in the header can be toggled on or off depending on whether the `image` and `altText` props are provided.

This example also includes an example of how to use [skip to content](/patternfly-ai/chatbot/ui#skip-to-content). When the terms of use modal is open, focus is placed on the terms of use container. When it is closed, focus is placed on the ChatBot. In a real example with a functioning ChatBot toggle, you would also want to place focus on the toggle when appropriate.

```js file="./TermsOfUse.tsx" isFullscreen

```

## Modals

### Modal

Based on the [PatternFly modal](/components/modal), this modal adapts to the ChatBot display mode and accepts components typically used in a modal. It is primarily used and tested in the context of the attachment modals, but you can customize this modal to adapt it to other use cases as needed. The modal will overlay the ChatBot in default and docked modes, and will behave more like a traditional PatternFly modal in fullscreen and embedded modes.
Expand Down
66 changes: 66 additions & 0 deletions packages/module/src/TermsOfUse/TermsOfUse.scss
rebeccaalpert marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.pf-chatbot__terms-of-use-modal {
.pf-v6-c-content {
font-size: var(--pf-t--global--font--size--body--lg);

h2 {
font-size: var(--pf-t--global--icon--size--font--heading--h2);
rebeccaalpert marked this conversation as resolved.
Show resolved Hide resolved
font-family: var(--pf-t--global--font--family--heading);
margin-bottom: var(--pf-t--global--spacer--md);
margin-top: var(--pf-t--global--spacer--md);
font-weight: var(--pf-t--global--font--weight--heading--default);
}
h2:first-of-type {
margin-top: 0;
}
}

.pf-chatbot__terms-of-use--header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--pf-t--global--spacer--xl);
margin-block-start: var(--pf-t--global--spacer--xl);
}

.pf-chatbot__terms-of-use--title {
font-size: var(--pf-t--global--font--size--heading--h1);
rebeccaalpert marked this conversation as resolved.
Show resolved Hide resolved
font-family: var(--pf-t--global--font--family--heading);
font-weight: var(--pf-t--global--font--weight--heading--bold);
}

.pf-chatbot__terms-of-use--footer {
margin-block-start: var(--pf-t--global--spacer--md);
}

.pf-chatbot__terms-of-use--section {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}

// for handling zoom conditions; zoom to 125% or higher to see this
@media screen and (max-height: 620px) {
.pf-v6-c-modal-box__body {
--pf-v6-c-modal-box__body--MinHeight: auto;
overflow: visible;
}
}
}

.pf-chatbot__chatbot-modal.pf-chatbot__chatbot-modal--fullscreen.pf-chatbot__terms-of-use-modal.pf-chatbot__terms-of-use-modal--fullscreen,
.pf-chatbot__chatbot-modal.pf-chatbot__chatbot-modal--embedded.pf-chatbot__terms-of-use-modal.pf-chatbot__terms-of-use-modal--embedded {
// override parent modal style
height: inherit;

.pf-v6-c-content {
h2 {
font-size: var(--pf-t--global--icon--size--font--heading--h1);
}
}

.pf-chatbot__terms-of-use--title {
font-size: var(--pf-t--global--font--size--heading--2xl);
}
}
138 changes: 138 additions & 0 deletions packages/module/src/TermsOfUse/TermsOfUse.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import TermsOfUse from './TermsOfUse';
import { Content } from '@patternfly/react-core';

const handleModalToggle = jest.fn();
const onPrimaryAction = jest.fn();
const onSecondaryAction = jest.fn();

const body = (
<Content>
<h1>Heading 1</h1>
<p>Legal text</p>
</Content>
);
describe('TermsOfUse', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should render modal correctly', () => {
render(
<TermsOfUse
isModalOpen
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
ouiaId="Terms"
>
{body}
</TermsOfUse>
);
expect(screen.getByRole('heading', { name: /Terms of use/i })).toBeTruthy();
expect(screen.getByRole('button', { name: /Accept/i })).toBeTruthy();
expect(screen.getByRole('button', { name: /Decline/i })).toBeTruthy();
expect(screen.getByRole('heading', { name: /Heading 1/i })).toBeTruthy();
expect(screen.getByText(/Legal text/i)).toBeTruthy();
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal');
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal--default');
});
it('should handle image and altText props', () => {
render(
<TermsOfUse
isModalOpen
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
image="./image.png"
altText="Test image"
>
{body}
</TermsOfUse>
);
expect(screen.getByRole('img')).toBeTruthy();
expect(screen.getByRole('img')).toHaveAttribute('alt', 'Test image');
});
it('should handle className prop', () => {
render(
<TermsOfUse
isModalOpen
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
className="test"
>
{body}
</TermsOfUse>
);
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal');
expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__terms-of-use-modal--default');
expect(screen.getByRole('dialog')).toHaveClass('test');
});
it('should handle title prop', () => {
render(
<TermsOfUse
isModalOpen
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
title="Updated title"
>
{body}
</TermsOfUse>
);
expect(screen.getByRole('heading', { name: /Updated title/i })).toBeTruthy();
expect(screen.queryByRole('heading', { name: /Terms of use/i })).toBeFalsy();
});
it('should handle primary button prop', () => {
render(
<TermsOfUse
isModalOpen
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
primaryActionBtn="First"
>
{body}
</TermsOfUse>
);
expect(screen.getByRole('button', { name: /First/i })).toBeTruthy();
expect(screen.queryByRole('button', { name: /Accept/i })).toBeFalsy();
});
it('should handle secondary button prop', () => {
render(
<TermsOfUse
isModalOpen
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
secondaryActionBtn="Second"
>
{body}
</TermsOfUse>
);
expect(screen.getByRole('button', { name: /Second/i })).toBeTruthy();
expect(screen.queryByRole('button', { name: /Deny/i })).toBeFalsy();
});
it('should handle primary button click', async () => {
render(
<TermsOfUse
isModalOpen
onPrimaryAction={onPrimaryAction}
onSecondaryAction={onSecondaryAction}
handleModalToggle={handleModalToggle}
>
{body}
</TermsOfUse>
);
await userEvent.click(screen.getByRole('button', { name: /Accept/i }));
expect(onPrimaryAction).toHaveBeenCalledTimes(1);
expect(handleModalToggle).toHaveBeenCalledTimes(1);
});
it('should handle secondary button click', async () => {
render(
<TermsOfUse isModalOpen onSecondaryAction={onSecondaryAction} handleModalToggle={handleModalToggle}>
{body}
</TermsOfUse>
);
await userEvent.click(screen.getByRole('button', { name: /Decline/i }));
expect(onSecondaryAction).toHaveBeenCalledTimes(1);
expect(handleModalToggle).not.toHaveBeenCalled();
});
});
Loading
Loading