Skip to content

Commit 554104c

Browse files
feat(SettingsForm): Add compact variant (#521)
Co-authored-by: Erin Donehoo <105813956+edonehoo@users.noreply.github.com>
1 parent 9d08d6a commit 554104c

13 files changed

Lines changed: 377 additions & 11 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import React from 'react';
2+
3+
import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings';
4+
import {
5+
Button,
6+
Divider,
7+
Dropdown,
8+
DropdownGroup,
9+
DropdownItem,
10+
DropdownList,
11+
MenuToggle,
12+
MenuToggleElement,
13+
Switch,
14+
Title
15+
} from '@patternfly/react-core';
16+
import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
17+
import ChatbotHeader, {
18+
ChatbotHeaderActions,
19+
ChatbotHeaderCloseButton,
20+
ChatbotHeaderMain,
21+
ChatbotHeaderOptionsDropdown,
22+
ChatbotHeaderTitle
23+
} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader';
24+
import { CogIcon, ExpandIcon, OpenDrawerRightIcon, OutlinedWindowRestoreIcon } from '@patternfly/react-icons';
25+
26+
export const CompactSettingsDemo: React.FunctionComponent = () => {
27+
const [isChecked, setIsChecked] = React.useState<boolean>(true);
28+
const [isThemeOpen, setIsThemeOpen] = React.useState(false);
29+
const [isLanguageOpen, setIsLanguageOpen] = React.useState(false);
30+
const [isVoiceOpen, setIsVoiceOpen] = React.useState(false);
31+
const [displayMode, setDisplayMode] = React.useState(ChatbotDisplayMode.default);
32+
const [areSettingsOpen, setAreSettingsOpen] = React.useState(true);
33+
const chatbotVisible = true;
34+
35+
const onFocus = (id: string) => {
36+
const element = document.getElementById(id);
37+
(element as HTMLElement).focus();
38+
};
39+
40+
const onThemeToggleClick = () => {
41+
setIsThemeOpen(!isThemeOpen);
42+
};
43+
44+
const onThemeSelect = (
45+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
46+
value: string | number | undefined
47+
) => {
48+
// eslint-disable-next-line no-console
49+
console.log('selected', value);
50+
onFocus('theme');
51+
setIsThemeOpen(false);
52+
};
53+
54+
const onLanguageToggleClick = () => {
55+
setIsLanguageOpen(!isLanguageOpen);
56+
};
57+
58+
const onLanguageSelect = (
59+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
60+
value: string | number | undefined
61+
) => {
62+
// eslint-disable-next-line no-console
63+
console.log('selected', value);
64+
onFocus('language');
65+
setIsLanguageOpen(false);
66+
};
67+
68+
const onVoiceToggleClick = () => {
69+
onFocus('voice');
70+
setIsVoiceOpen(!isVoiceOpen);
71+
};
72+
73+
const onVoiceSelect = (
74+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
75+
value: string | number | undefined
76+
) => {
77+
// eslint-disable-next-line no-console
78+
console.log('selected', value);
79+
setIsVoiceOpen(false);
80+
};
81+
82+
const handleChange = (_event: React.FormEvent<HTMLInputElement>, checked: boolean) => {
83+
setIsChecked(checked);
84+
};
85+
86+
const themeDropdown = (
87+
<Dropdown
88+
isOpen={isThemeOpen}
89+
onSelect={onThemeSelect}
90+
onOpenChange={(isOpen: boolean) => setIsThemeOpen(isOpen)}
91+
shouldFocusToggleOnSelect
92+
shouldFocusFirstItemOnOpen
93+
shouldPreventScrollOnItemFocus
94+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
95+
// We want to add the id property here as well so the label is coupled
96+
// with t he button on screen readers.
97+
<MenuToggle size="sm" id="theme" ref={toggleRef} onClick={onThemeToggleClick} isExpanded={isThemeOpen}>
98+
System
99+
</MenuToggle>
100+
)}
101+
ouiaId="ThemeDropdown"
102+
>
103+
<DropdownList>
104+
<DropdownItem value="System" key="system">
105+
System
106+
</DropdownItem>
107+
</DropdownList>
108+
</Dropdown>
109+
);
110+
111+
const languageDropdown = (
112+
<Dropdown
113+
isOpen={isLanguageOpen}
114+
onSelect={onLanguageSelect}
115+
onOpenChange={(isOpen: boolean) => setIsLanguageOpen(isOpen)}
116+
shouldFocusToggleOnSelect
117+
shouldFocusFirstItemOnOpen
118+
shouldPreventScrollOnItemFocus
119+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
120+
// We want to add the id property here as well so the label is coupled
121+
// with the button on screen readers.
122+
<MenuToggle size="sm" id="language" ref={toggleRef} onClick={onLanguageToggleClick} isExpanded={isLanguageOpen}>
123+
Auto-detect
124+
</MenuToggle>
125+
)}
126+
ouiaId="LanguageDropdown"
127+
>
128+
<DropdownList>
129+
<DropdownItem value="Auto-detect" key="auto-detect">
130+
Auto-detect
131+
</DropdownItem>
132+
</DropdownList>
133+
</Dropdown>
134+
);
135+
const voiceDropdown = (
136+
<Dropdown
137+
isOpen={isVoiceOpen}
138+
onSelect={onVoiceSelect}
139+
onOpenChange={(isOpen: boolean) => setIsVoiceOpen(isOpen)}
140+
shouldFocusToggleOnSelect
141+
shouldFocusFirstItemOnOpen
142+
shouldPreventScrollOnItemFocus
143+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
144+
// We want to add the id property here as well so the label is coupled
145+
// with the button on screen readers.
146+
<MenuToggle size="sm" id="voice" ref={toggleRef} onClick={onVoiceToggleClick} isExpanded={isVoiceOpen}>
147+
Bot
148+
</MenuToggle>
149+
)}
150+
ouiaId="VoiceDropdown"
151+
>
152+
<DropdownList>
153+
<DropdownItem value="Bot" key="bot">
154+
Bot
155+
</DropdownItem>
156+
</DropdownList>
157+
</Dropdown>
158+
);
159+
const children = [
160+
{ id: 'theme', label: 'Theme', field: themeDropdown },
161+
{ id: 'language', label: 'Language', field: languageDropdown },
162+
{ id: 'voice', label: 'Voice', field: voiceDropdown },
163+
{
164+
id: 'analytics',
165+
label: 'Share analytics',
166+
field: (
167+
<Switch
168+
// We want to add the id property here as well so the label is coupled
169+
// with the button on screen readers.
170+
id="analytics"
171+
aria-label="Togglable option for whether to share analytics"
172+
isChecked={isChecked}
173+
onChange={handleChange}
174+
/>
175+
)
176+
},
177+
{
178+
id: 'archived-chat',
179+
label: 'Archive chat',
180+
field: (
181+
// We want to add the id property here as well so the label is coupled
182+
// with the button on screen readers.
183+
<Button size="sm" id="archived-chat" variant="secondary">
184+
Manage
185+
</Button>
186+
)
187+
},
188+
{
189+
id: 'archive-all',
190+
label: 'Archive all chat',
191+
field: (
192+
// We want to add the id property here as well so the label is coupled
193+
// with the button on screen readers.
194+
<Button size="sm" id="archive-all" variant="secondary">
195+
Archive all
196+
</Button>
197+
)
198+
},
199+
{
200+
id: 'delete-all',
201+
label: 'Delete all chats',
202+
field: (
203+
// We want to add the id property here as well so the label is coupled
204+
// with the button on screen readers.
205+
<Button size="sm" id="delete-all" variant="danger">
206+
Delete all
207+
</Button>
208+
)
209+
}
210+
];
211+
212+
const onSelectDropdownItem = (
213+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
214+
value: string | number | undefined
215+
) => {
216+
if (value === 'Settings') {
217+
setAreSettingsOpen(true);
218+
} else {
219+
setDisplayMode(value as ChatbotDisplayMode);
220+
}
221+
};
222+
223+
const regularChatbot = (
224+
<ChatbotHeader>
225+
<ChatbotHeaderActions>
226+
<ChatbotHeaderOptionsDropdown isCompact onSelect={onSelectDropdownItem}>
227+
<DropdownGroup label="Display mode">
228+
<DropdownList>
229+
<DropdownItem
230+
value={ChatbotDisplayMode.default}
231+
key="switchDisplayOverlay"
232+
icon={<OutlinedWindowRestoreIcon aria-hidden />}
233+
isSelected={displayMode === ChatbotDisplayMode.default}
234+
>
235+
<span>Overlay</span>
236+
</DropdownItem>
237+
<DropdownItem
238+
value={ChatbotDisplayMode.docked}
239+
key="switchDisplayDock"
240+
icon={<OpenDrawerRightIcon aria-hidden />}
241+
isSelected={displayMode === ChatbotDisplayMode.docked}
242+
>
243+
<span>Dock to window</span>
244+
</DropdownItem>
245+
<DropdownItem
246+
value={ChatbotDisplayMode.fullscreen}
247+
key="switchDisplayFullscreen"
248+
icon={<ExpandIcon aria-hidden />}
249+
isSelected={displayMode === ChatbotDisplayMode.fullscreen}
250+
>
251+
<span>Fullscreen</span>
252+
</DropdownItem>
253+
</DropdownList>
254+
</DropdownGroup>
255+
<Divider />
256+
<DropdownList>
257+
<DropdownItem value="Settings" key="switchSettings" icon={<CogIcon aria-hidden />}>
258+
<span>Settings</span>
259+
</DropdownItem>
260+
</DropdownList>
261+
</ChatbotHeaderOptionsDropdown>
262+
</ChatbotHeaderActions>
263+
</ChatbotHeader>
264+
);
265+
266+
return (
267+
<>
268+
<Chatbot isCompact isVisible={chatbotVisible} displayMode={displayMode}>
269+
{areSettingsOpen ? (
270+
<>
271+
<ChatbotHeader className="pf-m-compact">
272+
<ChatbotHeaderMain>
273+
<ChatbotHeaderTitle>
274+
<Title headingLevel="h1" size="2xl">
275+
Settings
276+
</Title>
277+
</ChatbotHeaderTitle>
278+
</ChatbotHeaderMain>
279+
<ChatbotHeaderCloseButton isCompact onClick={() => setAreSettingsOpen(false)} />
280+
</ChatbotHeader>
281+
<SettingsForm isCompact fields={children} />
282+
</>
283+
) : (
284+
<>{regularChatbot}</>
285+
)}
286+
</Chatbot>
287+
</>
288+
);
289+
};

packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,13 @@ In this demo, you can toggle the settings page by clicking the "Settings" button
406406

407407
```
408408

409+
### Compact settings
410+
411+
To make the settings menu compact, with less spacing between the menu contents, pass `isCompact` to the `<SettingsForm>`.
412+
```js file="./CompactSettings.tsx" isFullscreen
413+
414+
```
415+
409416
## Modals
410417

411418
### Modal
17.7 KB
Loading

packages/module/src/Chatbot/Chatbot.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,10 @@
125125
border-radius: var(--pf-t--global--border--radius--sharp);
126126
}
127127
}
128+
129+
// ============================================================================
130+
// Information density styles
131+
// ============================================================================
132+
.pf-chatbot.pf-m-compact {
133+
font-size: var(--pf-t--global--font--size--sm);
134+
}

packages/module/src/Chatbot/Chatbot.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface ChatbotProps {
1717
innerRef?: React.Ref<HTMLDivElement>;
1818
/** Custom aria label applied to focusable container */
1919
ariaLabel?: string;
20+
/** Density of information within the ChatBot */
21+
isCompact?: boolean;
2022
}
2123

2224
export enum ChatbotDisplayMode {
@@ -34,6 +36,7 @@ const ChatbotBase: React.FunctionComponent<ChatbotProps> = ({
3436
className,
3537
innerRef,
3638
ariaLabel,
39+
isCompact,
3740
...props
3841
}: ChatbotProps) => {
3942
// Configure animations
@@ -44,7 +47,7 @@ const ChatbotBase: React.FunctionComponent<ChatbotProps> = ({
4447

4548
return (
4649
<motion.div
47-
className={`pf-chatbot pf-chatbot--${displayMode} ${!isVisible ? 'pf-chatbot--hidden' : ''} ${className ?? ''}`}
50+
className={`pf-chatbot pf-chatbot--${displayMode} ${!isVisible ? 'pf-chatbot--hidden' : ''} ${isCompact ? 'pf-m-compact' : ''} ${className ?? ''}`}
4851
variants={motionChatbot}
4952
initial="hidden"
5053
animate={isVisible ? 'visible' : 'hidden'}

packages/module/src/ChatbotHeader/ChatbotHeader.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,11 @@
137137
:where(.pf-v6-theme-dark) .show-dark .pf-m-picture {
138138
display: inline-flex;
139139
}
140+
141+
// ============================================================================
142+
// Information density styles
143+
// ============================================================================
144+
.pf-chatbot__button--toggle-menu.pf-m-compact {
145+
width: 2rem;
146+
height: 2rem;
147+
}

packages/module/src/ChatbotHeader/ChatbotHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const ChatbotHeader: React.FunctionComponent<ChatbotHeaderProps> = ({
1414
children
1515
}: ChatbotHeaderProps) => (
1616
<div className="pf-chatbot__header-container">
17-
<div className={`pf-chatbot__header ${className ?? ''}`}>{children}</div>
17+
<div className={`pf-chatbot__header${className ? ` ${className}` : ''}`}>{children}</div>
1818
<Divider className="pf-chatbot__header__divider" />
1919
</div>
2020
);

packages/module/src/ChatbotHeader/ChatbotHeaderCloseButton.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { fireEvent, render, screen } from '@testing-library/react';
33
import { ChatbotHeaderCloseButton } from './ChatbotHeaderCloseButton';
4+
import '@testing-library/jest-dom';
45

56
describe('ChatbotHeaderCloseButton', () => {
67
it('should render ChatbotHeaderCloseButton', () => {
@@ -17,4 +18,9 @@ describe('ChatbotHeaderCloseButton', () => {
1718
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
1819
expect(onClick).toHaveBeenCalled();
1920
});
21+
22+
it('should render button with isCompact', () => {
23+
render(<ChatbotHeaderCloseButton data-testid="close-button" onClick={jest.fn()} isCompact />);
24+
expect(screen.getByTestId('close-button')).toHaveClass('pf-m-compact');
25+
});
2026
});

0 commit comments

Comments
 (0)