Skip to content

Commit

Permalink
Merge pull request #349 from rebeccaalpert/terms
Browse files Browse the repository at this point in the history
feat(TermsOfUse): Add Terms of Use modal
  • Loading branch information
nicolethoen authored Dec 17, 2024
2 parents 8020752 + ecf4438 commit 3b45d76
Show file tree
Hide file tree
Showing 9 changed files with 637 additions and 0 deletions.
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)}
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
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);
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);
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

0 comments on commit 3b45d76

Please sign in to comment.