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(SkipToContent): Add SkipToContent to demos #296

Merged
merged 11 commits into from
Nov 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ChatbotContent from '@patternfly/virtual-assistant/dist/dynamic/ChatbotCo
import ChatbotWelcomePrompt from '@patternfly/virtual-assistant/dist/dynamic/ChatbotWelcomePrompt';
import MessageBox from '@patternfly/virtual-assistant/dist/dynamic/MessageBox';
import Message from '@patternfly/virtual-assistant/dist/dynamic/Message';
import ChatbotToggle from '@patternfly/virtual-assistant/dist/dynamic/ChatbotToggle';

### Container

Expand Down Expand Up @@ -71,3 +72,14 @@ To provide users with a more specific direction, you can also include optional w
```js file="./ChatbotWelcomePrompt.tsx"

```

### Skip to content

To provide page context, we recommend using a "skip to chatbot" button. This allows you to skip past other content on the page, directly to the chatbot content, using a [PatternFly skip to content component](/components/skip-to-content). To display this button, you must tab into the main window.
<br />
<br />
When using default or docked modes, we recommend putting focus on the toggle if the chatbot is closed, and the chatbot when it is open. For fullscreen and embedded, we recommend putting the focus on the first focusable item in the chatbot, such as a menu toggle. This can be seen in our more fully-featured demos for the [default, embedded, and fullscreen chatbot](patternfly-ai/chatbot/chatbot-container/react-demos/basic-chatbot) and the [embedded chatbot](/patternfly-ai/chatbot/chatbot-container/react-demos/embedded-chatbot).

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

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

import { SkipToContent } from '@patternfly/react-core';
import ChatbotToggle from '@patternfly/virtual-assistant/dist/dynamic/ChatbotToggle';
import Chatbot, { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot';

export const ChatbotDemo: React.FunctionComponent = () => {
const [chatbotVisible, setChatbotVisible] = React.useState<boolean>(true);
const toggleRef = React.useRef<HTMLButtonElement>(null);
const chatbotRef = React.useRef<HTMLDivElement>(null);
const displayMode = ChatbotDisplayMode.default;

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

return (
<>
<SkipToContent onClick={handleSkipToContent} href="#">
Skip to chatbot
</SkipToContent>
<ChatbotToggle
toolTipLabel="Chatbot"
isChatbotVisible={chatbotVisible}
onToggleChatbot={() => setChatbotVisible(!chatbotVisible)}
id="chatbot-toggle"
ref={toggleRef}
/>
<Chatbot isVisible={chatbotVisible} displayMode={displayMode} ref={chatbotRef}>
&nbsp;
</Chatbot>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ This demo displays a basic chatbot, which includes:

6. A [`<ChatbotConversationHistoryNav>`](/patternfly-ai/chatbot/chatbot-conversation-history) toggled open and closed by the `<ChatbotHeaderMenu`> in the `<ChatbotHeader>`.

7. A "skip to chatbot" button that allows you to skip to the chatbot content via the [PatternFly skip to content component](/components/skip-to-content). To display this button you must tab into the main window.

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

```
Expand All @@ -85,7 +87,7 @@ This demo displays a basic chatbot, which includes:

This demo displays an embedded chatbot. Embedded chatbots are meant to be placed within a page in your product. This demo includes:

1. A [PatternFly page](/components/page) with a sidebar and masthead
1. A [PatternFly page](/components/page) with a sidebar, "skip to chatbot" button, and masthead. To display the "skip to chatbot" button you must tab into the main window.
2. A [`<Chatbot>`](/patternfly-ai/chatbot/chatbot-container) container.
3. A [`<ChatbotHeader>`](/patternfly-ai/chatbot/chatbot-header) with all built sub-components laid out, including a `<ChatbotHeaderTitle>`
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/chatbot-container#content-and-message-box) with:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup } from '@patternfly/react-core';
import { Bullseye, Brand, DropdownList, DropdownItem, DropdownGroup, SkipToContent } from '@patternfly/react-core';

import ChatbotToggle from '@patternfly/virtual-assistant/dist/dynamic/ChatbotToggle';
import Chatbot, { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot';
Expand Down Expand Up @@ -175,6 +175,9 @@ export const ChatbotDemo: React.FunctionComponent = () => {
);
const [announcement, setAnnouncement] = React.useState<string>();
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);
const toggleRef = React.useRef<HTMLButtonElement>(null);
const chatbotRef = React.useRef<HTMLDivElement>(null);
const historyRef = React.useRef<HTMLButtonElement>(null);

// Autu-scrolls to the latest message
React.useEffect(() => {
Expand Down Expand Up @@ -299,14 +302,46 @@ export const ChatbotDemo: React.FunctionComponent = () => {
</>
);

const handleSkipToContent = (e) => {
e.preventDefault();
/* eslint-disable indent */
switch (displayMode) {
case ChatbotDisplayMode.default:
if (!chatbotVisible && toggleRef.current) {
toggleRef.current.focus();
}
if (chatbotVisible && chatbotRef.current) {
chatbotRef.current.focus();
}
break;

case ChatbotDisplayMode.docked:
if (chatbotRef.current) {
chatbotRef.current.focus();
}
break;
default:
if (historyRef.current) {
historyRef.current.focus();
}
break;
}
/* eslint-enable indent */
};

return (
<>
<SkipToContent onClick={handleSkipToContent} href="#">
Skip to chatbot
edonehoo marked this conversation as resolved.
Show resolved Hide resolved
</SkipToContent>
<ChatbotToggle
toolTipLabel="Chatbot"
isChatbotVisible={chatbotVisible}
onToggleChatbot={() => setChatbotVisible(!chatbotVisible)}
id="chatbot-toggle"
ref={toggleRef}
/>
<Chatbot isVisible={chatbotVisible} displayMode={displayMode}>
<Chatbot isVisible={chatbotVisible} displayMode={displayMode} ref={chatbotRef}>
<ChatbotConversationHistoryNav
displayMode={displayMode}
onDrawerToggle={() => {
Expand Down Expand Up @@ -337,7 +372,11 @@ export const ChatbotDemo: React.FunctionComponent = () => {
<>
<ChatbotHeader>
<ChatbotHeaderMain>
<ChatbotHeaderMenu aria-expanded={isDrawerOpen} onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)} />
<ChatbotHeaderMenu
ref={historyRef}
aria-expanded={isDrawerOpen}
onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)}
/>
<ChatbotHeaderTitle
displayMode={displayMode}
showOnFullScreen={horizontalLogo}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
PageSidebarBody,
PageSidebar,
MastheadToggle,
PageToggleButton
PageToggleButton,
SkipToContent
} from '@patternfly/react-core';

import Chatbot, { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot';
Expand Down Expand Up @@ -181,6 +182,8 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const [announcement, setAnnouncement] = React.useState<string>();
const scrollToBottomRef = React.useRef<HTMLDivElement>(null);
const historyRef = React.useRef<HTMLButtonElement>(null);

const displayMode = ChatbotDisplayMode.embedded;
// Autu-scrolls to the latest message
React.useEffect(() => {
Expand Down Expand Up @@ -320,8 +323,22 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
</PageSidebar>
);

const skipToChatbot = (event: React.MouseEvent) => {
event.preventDefault();
if (historyRef.current) {
historyRef.current.focus();
}
};

const skipToContent = (
/* You can also add a SkipToContent for your main content here */
<SkipToContent href="#" onClick={skipToChatbot}>
Skip to chatbot
</SkipToContent>
);

return (
<Page masthead={masthead} sidebar={sidebar} isContentFilled>
<Page skipToContent={skipToContent} masthead={masthead} sidebar={sidebar} isContentFilled>
<Chatbot displayMode={displayMode}>
<ChatbotConversationHistoryNav
displayMode={displayMode}
Expand Down Expand Up @@ -353,7 +370,11 @@ export const EmbeddedChatbotDemo: React.FunctionComponent = () => {
<>
<ChatbotHeader>
<ChatbotHeaderMain>
<ChatbotHeaderMenu aria-expanded={isDrawerOpen} onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)} />
<ChatbotHeaderMenu
ref={historyRef}
aria-expanded={isDrawerOpen}
onMenuToggle={() => setIsDrawerOpen(!isDrawerOpen)}
/>
<ChatbotHeaderTitle>{horizontalLogo}</ChatbotHeaderTitle>
</ChatbotHeaderMain>
<ChatbotHeaderActions>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 24 additions & 1 deletion packages/module/src/Chatbot/Chatbot.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
border-radius: var(--pf-t--global--border--radius--medium);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
font-size: var(--pf-t--chatbot--font-size);
overflow: hidden;
z-index: var(--pf-t--global--z-index--md);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
Expand Down Expand Up @@ -64,3 +63,27 @@ html.pf-chatbot-allow--docked {
border-radius: 0;
box-shadow: none;
}

.pf-chatbot-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
border-radius: var(--pf-t--global--border--radius--medium);
overflow: hidden;

// Hide chatbot
&--hidden {
pointer-events: none;
}
}

.pf-chatbot-container--embedded {
min-height: 100%;
}

.pf-chatbot-container--docked,
.pf-chatbot-container--embedded,
.pf-chatbot-container--fullscreen {
border-radius: unset;
}
29 changes: 26 additions & 3 deletions packages/module/src/Chatbot/Chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export interface ChatbotProps {
isVisible?: boolean;
/** Custom classname for the Chatbot component */
className?: string;
/** Ref applied to chatbot */
innerRef?: React.Ref<HTMLDivElement>;
/** Custom aria label applied to focusable container */
ariaLabel?: string;
}

export enum ChatbotDisplayMode {
Expand All @@ -22,11 +26,14 @@ export enum ChatbotDisplayMode {
fullscreen = 'fullscreen'
}

export const Chatbot: React.FunctionComponent<ChatbotProps> = ({
const ChatbotBase: React.FunctionComponent<ChatbotProps> = ({
children,
displayMode = ChatbotDisplayMode.default,
isVisible = true,
className
className,
innerRef,
ariaLabel,
...props
}: ChatbotProps) => {
// Configure docked mode
React.useEffect(() => {
Expand All @@ -49,10 +56,26 @@ export const Chatbot: React.FunctionComponent<ChatbotProps> = ({
variants={motionChatbot}
initial="hidden"
animate={isVisible ? 'visible' : 'hidden'}
{...props}
>
{isVisible ? children : undefined}
{/* Ref is intended for use with skip to chatbot links, etc. */}
{/* Motion.div does not accept refs */}
{isVisible ? (
<section
aria-label={ariaLabel ?? 'Chatbot'}
className={`pf-chatbot-container pf-chatbot-container--${displayMode} ${!isVisible ? 'pf-chatbot-container--hidden' : ''}`}
tabIndex={-1}
ref={innerRef}
>
{children}
</section>
) : undefined}
</motion.div>
);
};

const Chatbot = React.forwardRef((props: ChatbotProps, ref: React.Ref<HTMLDivElement>) => (
<ChatbotBase innerRef={ref} {...props} />
));

export default Chatbot;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.pf-chatbot__history {
.pf-chatbot__drawer-backdrop {
position: absolute;
border-radius: var(--pf-t--global--border--radius--medium);
}
// Drawer input
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -153,3 +154,13 @@
}
}
}

.pf-chatbot--docked,
.pf-chatbot--embedded,
.pf-chatbot--fullscreen {
.pf-chatbot__history {
.pf-chatbot__drawer-backdrop {
border-radius: unset;
}
}
}
1 change: 1 addition & 0 deletions packages/module/src/ChatbotFooter/ChatbotFooter.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
display: flex;
flex-direction: column;
row-gap: var(--pf-chatbot__footer--RowGap);
position: relative; // this is so focus ring on parent chatbot doesn't include footer
}
.pf-chatbot__footer-container {
padding: 0 var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg);
Expand Down
2 changes: 1 addition & 1 deletion packages/module/src/ChatbotHeader/ChatbotHeader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
display: grid;
grid-template-columns: 1fr auto;
gap: var(--pf-t--global--spacer--sm);
position: relative;
position: relative; // this is so focus ring on parent chatbot doesn't include header
background-color: var(--pf-t--chatbot--background);
justify-content: space-between;
padding: var(--pf-t--global--spacer--lg);
Expand Down
14 changes: 11 additions & 3 deletions packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ export interface ChatbotHeaderMenuProps {
tooltipProps?: TooltipProps;
/** Aria label for menu */
menuAriaLabel?: string;
/** Ref applied to menu */
innerRef?: React.Ref<HTMLButtonElement>;
}

export const ChatbotHeaderMenu: React.FunctionComponent<ChatbotHeaderMenuProps> = ({
const ChatbotHeaderMenuBase: React.FunctionComponent<ChatbotHeaderMenuProps> = ({
className,
onMenuToggle,
tooltipProps,
menuAriaLabel = 'Toggle menu'
menuAriaLabel = 'Toggle menu',
innerRef
}: ChatbotHeaderMenuProps) => (
<div className={`pf-chatbot__menu ${className}`}>
<Tooltip content="Menu" position="bottom" {...tooltipProps}>
Expand All @@ -27,6 +30,7 @@ export const ChatbotHeaderMenu: React.FunctionComponent<ChatbotHeaderMenuProps>
variant="plain"
onClick={onMenuToggle}
aria-label={menuAriaLabel}
ref={innerRef}
icon={
<Icon size="xl" isInline>
<BarsIcon />
Expand All @@ -37,4 +41,8 @@ export const ChatbotHeaderMenu: React.FunctionComponent<ChatbotHeaderMenuProps>
</div>
);

export default ChatbotHeaderMenu;
export const ChatbotHeaderMenu = React.forwardRef(
(props: ChatbotHeaderMenuProps, ref: React.Ref<HTMLButtonElement>) => (
<ChatbotHeaderMenuBase innerRef={ref} {...props} />
)
);
Loading
Loading