Skip to content

Commit

Permalink
feat(Settings): Add settings layout
Browse files Browse the repository at this point in the history
  • Loading branch information
rebeccaalpert committed Dec 6, 2024
1 parent 1a669e1 commit bfe5c87
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import React from 'react';

import Settings from '@patternfly/chatbot/dist/dynamic/Settings';
import {
Button,
Dropdown,
DropdownItem,
DropdownList,
FormGroup,
MenuToggle,
MenuToggleElement,
Radio,
Switch
} from '@patternfly/react-core';
import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';

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 [isModalOpen, setIsModalOpen] = React.useState(true);

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

const onEscapePress = (event: KeyboardEvent) => {
const target = event.target as Element;
if (target?.id === 'voice') {
setIsVoiceOpen(!isVoiceOpen);
onFocus('voice');
return;
}
if (target?.id === 'language') {
setIsLanguageOpen(!isLanguageOpen);
onFocus('language');
return;
}
if (target?.id === 'theme') {
setIsThemeOpen(!isThemeOpen);
onFocus('theme');
return;
}
setIsModalOpen(!isModalOpen);
};

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>
)
}
];

return (
<>
<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>
<Button onClick={() => setIsModalOpen(!isModalOpen)}>Launch modal</Button>
</div>
<Settings
onEscapePress={onEscapePress}
displayMode={displayMode}
isModalOpen={isModalOpen}
handleModalToggle={() => setIsModalOpen(!isModalOpen)}
fields={children}
></Settings>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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 Settings from '@patternfly/chatbot/dist/dynamic/Settings';
import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, UploadIcon } from '@patternfly/react-icons';
import { useDropzone } from 'react-dropzone';

Expand Down Expand Up @@ -342,3 +343,9 @@ Based on the [PatternFly modal](/components/modal), this modal adapts to the Cha
```js file="./ChatbotModal.tsx" isFullscreen

```

### Settings

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

```
25 changes: 25 additions & 0 deletions packages/module/src/Settings/Settings.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.pf-chatbot__settings-form {
display: flex;
flex-direction: column;
}

.pf-chatbot__settings-form-row {
font-size: var(--pf-t--global--font--size--body--lg);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--pf-t--global--border--color--default);
padding: var(--pf-t--global--spacer--lg);
font-weight: 500;
}

.pf-chatbot__settings-form-row:last-of-type {
border-bottom: 0px;
}

.pf-chatbot__settings--title {
font-family: var(--pf-t--global--font--family--heading);
font-size: var(--pf-t--global--font--size--xl);
font-weight: 500;
text-align: center;
}
60 changes: 60 additions & 0 deletions packages/module/src/Settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import ChatbotModal from '../ChatbotModal';
import { ModalBody, ModalHeader, ModalProps } from '@patternfly/react-core';
import { ChatbotDisplayMode } from '../Chatbot';

export interface SettingsProps extends Omit<ModalProps, 'children'> {
/** Class applied to modal */
className?: string;
/** Function that handles modal toggle */
handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
/** Whether modal is open */
isModalOpen: boolean;
/** Title of modal */
title?: string;
/** Display mode for the Chatbot parent; this influences the styles applied */
displayMode?: ChatbotDisplayMode;
/** Array of fields to display in the settings layout */
fields?: { id: string; label: string; field: React.ReactElement }[];
}

export const Settings: React.FunctionComponent<SettingsProps> = ({
handleModalToggle,
isModalOpen,
title = 'Settings',
displayMode = ChatbotDisplayMode.default,
className,
fields = [],
...props
}) => (
<ChatbotModal
onClose={handleModalToggle}
isOpen={isModalOpen}
ouiaId="Settings"
aria-labelledby="settings-title"
aria-describedby="settings-modal"
className={`pf-chatbot__settings ${className ? className : ''}`}
displayMode={displayMode}
{...props}
>
<ModalHeader>
<div className="pf-chatbot__settings--header">
<h1 className="pf-chatbot__settings--title">{title}</h1>
</div>
</ModalHeader>
<ModalBody>
<form className="pf-chatbot__settings-form">
{fields.map((field) => (
<div className="pf-chatbot__settings-form-row" key={field.label}>
<label className="pf-chatbot__settings-label" htmlFor={field.id}>
{field.label}
</label>
{field.field}
</div>
))}
</form>
</ModalBody>
</ChatbotModal>
);

export default Settings;
3 changes: 3 additions & 0 deletions packages/module/src/Settings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './Settings';

export * from './Settings';
3 changes: 3 additions & 0 deletions packages/module/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export * from './PreviewAttachment';
export { default as ResponseActions } from './ResponseActions';
export * from './ResponseActions';

export { default as Settings } from './Settings';
export * from './Settings';

export { default as SourceDetailsMenuItem } from './SourceDetailsMenuItem';
export * from './SourceDetailsMenuItem';

Expand Down
Loading

0 comments on commit bfe5c87

Please sign in to comment.