Skip to content

Commit

Permalink
Merge pull request #360 from rebeccaalpert/settings
Browse files Browse the repository at this point in the history
feat(Settings): Add settings layout
  • Loading branch information
nicolethoen authored Dec 17, 2024
2 parents bfa650d + 074b5e6 commit 55452b5
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import React from 'react';

import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings';
import {
Button,
Divider,
Dropdown,
DropdownGroup,
DropdownItem,
DropdownList,
MenuToggle,
MenuToggleElement,
Switch,
Title
} from '@patternfly/react-core';
import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
import ChatbotHeader, {
ChatbotHeaderActions,
ChatbotHeaderCloseButton,
ChatbotHeaderMain,
ChatbotHeaderOptionsDropdown,
ChatbotHeaderTitle
} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
import { CogIcon, ExpandIcon, OpenDrawerRightIcon, OutlinedWindowRestoreIcon } from '@patternfly/react-icons';

export const SettingsDemo: React.FunctionComponent = () => {
const [isChecked, setIsChecked] = React.useState<boolean>(true);
const [isThemeOpen, setIsThemeOpen] = React.useState(false);
const [isLanguageOpen, setIsLanguageOpen] = React.useState(false);
const [isVoiceOpen, setIsVoiceOpen] = React.useState(false);
const [displayMode, setDisplayMode] = React.useState(ChatbotDisplayMode.default);
const [areSettingsOpen, setAreSettingsOpen] = React.useState(true);
const chatbotVisible = true;

const onFocus = (id: string) => {
const element = document.getElementById(id);
(element as HTMLElement).focus();
};

const onThemeToggleClick = () => {
setIsThemeOpen(!isThemeOpen);
};

const onThemeSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined
) => {
// eslint-disable-next-line no-console
console.log('selected', value);
onFocus('theme');
setIsThemeOpen(false);
};

const onLanguageToggleClick = () => {
setIsLanguageOpen(!isLanguageOpen);
};

const onLanguageSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined
) => {
// eslint-disable-next-line no-console
console.log('selected', value);
onFocus('language');
setIsLanguageOpen(false);
};

const onVoiceToggleClick = () => {
onFocus('voice');
setIsVoiceOpen(!isVoiceOpen);
};

const onVoiceSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined
) => {
// eslint-disable-next-line no-console
console.log('selected', value);
setIsVoiceOpen(false);
};

const handleChange = (_event: React.FormEvent<HTMLInputElement>, checked: boolean) => {
setIsChecked(checked);
};

const themeDropdown = (
<Dropdown
isOpen={isThemeOpen}
onSelect={onThemeSelect}
onOpenChange={(isOpen: boolean) => setIsThemeOpen(isOpen)}
shouldFocusToggleOnSelect
shouldFocusFirstItemOnOpen
shouldPreventScrollOnItemFocus
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
<MenuToggle id="theme" ref={toggleRef} onClick={onThemeToggleClick} isExpanded={isThemeOpen}>
System
</MenuToggle>
)}
ouiaId="ThemeDropdown"
>
<DropdownList>
<DropdownItem value="System" key="system">
System
</DropdownItem>
</DropdownList>
</Dropdown>
);

const languageDropdown = (
<Dropdown
isOpen={isLanguageOpen}
onSelect={onLanguageSelect}
onOpenChange={(isOpen: boolean) => setIsLanguageOpen(isOpen)}
shouldFocusToggleOnSelect
shouldFocusFirstItemOnOpen
shouldPreventScrollOnItemFocus
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
<MenuToggle id="language" ref={toggleRef} onClick={onLanguageToggleClick} isExpanded={isLanguageOpen}>
Auto-detect
</MenuToggle>
)}
ouiaId="LanguageDropdown"
>
<DropdownList>
<DropdownItem value="Auto-detect" key="auto-detect">
Auto-detect
</DropdownItem>
</DropdownList>
</Dropdown>
);
const voiceDropdown = (
<Dropdown
isOpen={isVoiceOpen}
onSelect={onVoiceSelect}
onOpenChange={(isOpen: boolean) => setIsVoiceOpen(isOpen)}
shouldFocusToggleOnSelect
shouldFocusFirstItemOnOpen
shouldPreventScrollOnItemFocus
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
<MenuToggle id="voice" ref={toggleRef} onClick={onVoiceToggleClick} isExpanded={isVoiceOpen}>
Bot
</MenuToggle>
)}
ouiaId="VoiceDropdown"
>
<DropdownList>
<DropdownItem value="Bot" key="bot">
Bot
</DropdownItem>
</DropdownList>
</Dropdown>
);
const children = [
{ id: 'theme', label: 'Theme', field: themeDropdown },
{ id: 'language', label: 'Language', field: languageDropdown },
{ id: 'voice', label: 'Voice', field: voiceDropdown },
{
id: 'analytics',
label: 'Share analytics',
field: (
<Switch
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
id="analytics"
aria-label="Togglable option for whether to share analytics"
isChecked={isChecked}
onChange={handleChange}
/>
)
},
{
id: 'archived-chat',
label: 'Archive chat',
field: (
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
<Button id="archived-chat" variant="secondary">
Manage
</Button>
)
},
{
id: 'archive-all',
label: 'Archive all chat',
field: (
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
<Button id="archive-all" variant="secondary">
Archive all
</Button>
)
},
{
id: 'delete-all',
label: 'Delete all chats',
field: (
// We want to add the id property here as well so the label is coupled
// with the button on screen readers.
<Button id="delete-all" variant="danger">
Delete all
</Button>
)
}
];

const onSelectDropdownItem = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
value: string | number | undefined
) => {
if (value === 'Settings') {
setAreSettingsOpen(true);
} else {
setDisplayMode(value as ChatbotDisplayMode);
}
};

const regularChatbot = (
<ChatbotHeader>
<ChatbotHeaderActions>
<ChatbotHeaderOptionsDropdown onSelect={onSelectDropdownItem}>
<DropdownGroup label="Display mode">
<DropdownList>
<DropdownItem
value={ChatbotDisplayMode.default}
key="switchDisplayOverlay"
icon={<OutlinedWindowRestoreIcon aria-hidden />}
isSelected={displayMode === ChatbotDisplayMode.default}
>
<span>Overlay</span>
</DropdownItem>
<DropdownItem
value={ChatbotDisplayMode.docked}
key="switchDisplayDock"
icon={<OpenDrawerRightIcon aria-hidden />}
isSelected={displayMode === ChatbotDisplayMode.docked}
>
<span>Dock to window</span>
</DropdownItem>
<DropdownItem
value={ChatbotDisplayMode.fullscreen}
key="switchDisplayFullscreen"
icon={<ExpandIcon aria-hidden />}
isSelected={displayMode === ChatbotDisplayMode.fullscreen}
>
<span>Fullscreen</span>
</DropdownItem>
</DropdownList>
</DropdownGroup>
<Divider />
<DropdownList>
<DropdownItem value="Settings" key="switchSettings" icon={<CogIcon aria-hidden />}>
<span>Settings</span>
</DropdownItem>
</DropdownList>
</ChatbotHeaderOptionsDropdown>
</ChatbotHeaderActions>
</ChatbotHeader>
);

return (
<>
<Chatbot isVisible={chatbotVisible} displayMode={displayMode}>
{areSettingsOpen ? (
<>
<ChatbotHeader>
<ChatbotHeaderMain>
<ChatbotHeaderTitle>
<Title headingLevel="h1" size="2xl">
Settings
</Title>
</ChatbotHeaderTitle>
</ChatbotHeaderMain>
<ChatbotHeaderCloseButton onClick={() => setAreSettingsOpen(false)} />
</ChatbotHeader>
<SettingsForm fields={children} />
</>
) : (
<>{regularChatbot}</>
)}
</Chatbot>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import ChatbotAlert from '@patternfly/chatbot/dist/dynamic/ChatbotAlert';
import TermsOfUse from '@patternfly/chatbot/dist/dynamic/TermsOfUse';
import {
ChatbotHeader,
ChatbotHeaderCloseButton,
ChatbotHeaderMain,
ChatbotHeaderMenu,
ChatbotHeaderActions,
Expand All @@ -66,6 +67,7 @@ import { ChatbotFooter, ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic
import { MessageBar } from '@patternfly/chatbot/dist/dynamic/MessageBar';
import SourceDetailsMenuItem from '@patternfly/chatbot/dist/dynamic/SourceDetailsMenuItem';
import { ChatbotModal } from '@patternfly/chatbot/dist/dynamic/ChatbotModal';
import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings';
import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, UploadIcon } from '@patternfly/react-icons';
import { useDropzone } from 'react-dropzone';

Expand All @@ -75,11 +77,13 @@ import { DropdownItem, DropdownList, Checkbox } from '@patternfly/react-core';
import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
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';
import { CloseIcon } from '@patternfly/react-icons';

## Structure

Expand Down Expand Up @@ -377,6 +381,16 @@ This example also includes an example of how to use [skip to content](/patternfl

```

### Settings

To contain user preference controls and other ChatBot setting options, you can create a separate settings page that can accept any number of buttons, dropdown menus, toggles, labels, and so on. This settings page will render all components appropriately within all 4 display modes.

In this demo, you can toggle the settings page by clicking the "Settings" button in the display mode menu.

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

```

## Modals

### Modal
Expand Down
51 changes: 51 additions & 0 deletions packages/module/src/ChatbotHeader/ChatbotHeaderCloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';

import { Button, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
import { CloseIcon } from '@patternfly/react-icons';

export interface ChatbotHeaderCloseButtonProps {
/** Callback function for when button is clicked */
onClick: () => void;
/** Custom classname for the header component */
className?: string;
/** Props spread to the PF Tooltip component wrapping the display mode dropdown */
tooltipProps?: TooltipProps;
/** Aria label for menu */
menuAriaLabel?: string;
/** Ref applied to menu */
innerRef?: React.Ref<HTMLButtonElement>;
/** Content used in tooltip */
tooltipContent?: string;
}

const ChatbotHeaderCloseButtonBase: React.FunctionComponent<ChatbotHeaderCloseButtonProps> = ({
className,
onClick,
tooltipProps,
menuAriaLabel = 'Close',
innerRef,
tooltipContent = 'Close'
}: ChatbotHeaderCloseButtonProps) => (
<div className={`pf-chatbot__menu ${className}`}>
<Tooltip content={tooltipContent} position="bottom" {...tooltipProps}>
<Button
className="pf-chatbot__button--toggle-menu"
variant="plain"
onClick={onClick}
aria-label={menuAriaLabel}
ref={innerRef}
icon={
<Icon size="xl" isInline>
<CloseIcon />
</Icon>
}
/>
</Tooltip>
</div>
);

export const ChatbotHeaderCloseButton = React.forwardRef(
(props: ChatbotHeaderCloseButtonProps, ref: React.Ref<HTMLButtonElement>) => (
<ChatbotHeaderCloseButtonBase innerRef={ref} {...props} />
)
);
Loading

0 comments on commit 55452b5

Please sign in to comment.