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

[DEVHUB-1898, UP-6913] Add Hotkey Indicator to Chat InputBar Component #2476

Merged
merged 18 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stupid-toes-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lg-chat/input-bar': minor
---

Add `shouldRenderHotkeyIndicator` prop to InputBar component which determines if hotkey indicator is rendered
21 changes: 11 additions & 10 deletions chat/input-bar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,17 @@ return <InputBar badgeText="Beta" />;

## Properties

| Prop | Type | Description | Default |
| ---------------------- | --------------------------------------------- | -------------------------------------------------------------- | ------- |
| `badgeText` | `string` | If provided, renders a badge with text next to the wizard icon | |
| `darkMode` | `boolean` | Determines if the component will render in dark mode | `false` |
| `onMessageSend` | `(messageBody: string, e: FormEvent) => void` | Callback fired when message is sent | |
| `shouldRenderGradient` | `boolean` | Determines if component renders with gradient box-shadow | `true` |
| `textAreaProps` | `TextareaAutosizeProps` | `TextareaAutosize` props spread on textarea component | |
| `disabled` | `boolean` | Determines whether the user can interact with the InputBar | |
| `disableSend` | `boolean` | When defined as `true`, disables the send action and button | |
| `...` | `HTMLElementProps<'form'>` | Props spread on the root element | |
| Prop | Type | Description | Default |
| ----------------------------- | --------------------------------------------- | -------------------------------------------------------------- | ------- |
| `badgeText` | `string` | If provided, renders a badge with text next to the wizard icon | |
| `darkMode` | `boolean` | Determines if the component will render in dark mode | `false` |
| `onMessageSend` | `(messageBody: string, e: FormEvent) => void` | Callback fired when message is sent | |
| `shouldRenderGradient` | `boolean` | Determines if component renders with gradient box-shadow | `true` |
| `shouldRenderHotkeyIndicator` | `boolean` | Determines if component renders with hotkey indicator | `false` |
| `textAreaProps` | `TextareaAutosizeProps` | `TextareaAutosize` props spread on textarea component | |
| `disabled` | `boolean` | Determines whether the user can interact with the InputBar | |
| `disableSend` | `boolean` | When defined as `true`, disables the send action and button | |
| `...` | `HTMLElementProps<'form'>` | Props spread on the root element | |

## TextareaAutosizeProps

Expand Down
5 changes: 5 additions & 0 deletions chat/input-bar/src/InputBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ WithBadge.args = {
badgeText: 'Beta',
};

export const WithHotkeyIndicator = Template.bind({});
WithHotkeyIndicator.args = {
shouldRenderHotkeyIndicator: true,
};

export const WithDropdown = Template.bind({});
WithDropdown.args = {
children: (
Expand Down
42 changes: 41 additions & 1 deletion chat/input-bar/src/InputBar/InputBar.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { transitionDuration } from '@leafygreen-ui/tokens';

import { InputBar } from '.';

const testText = 'test';
Expand Down Expand Up @@ -90,4 +92,42 @@ describe('packages/input-bar', () => {

expect(screen.getByText('beta')).toBeInTheDocument();
});

test('Hotkey Indicator is rendered when the prop is set', () => {
render(<InputBar shouldRenderHotkeyIndicator />);

expect(screen.getByTestId('lg-chat-hotkey-indicator')).toBeInTheDocument();
});

test('Hotkey Indicator is hidden when InputBar is focused and visible when unfocused', async () => {
render(<InputBar shouldRenderHotkeyIndicator />);
const textarea = screen.getByRole('textbox');

act(() => {
textarea.focus();
});
// Wait for CSS animation
await new Promise(resolve =>
setTimeout(resolve, transitionDuration.default),
);

expect(screen.getByTestId('lg-chat-hotkey-indicator')).not.toBeVisible();

act(() => {
textarea.blur();
});
// Wait for CSS animation
await new Promise(resolve =>
setTimeout(resolve, transitionDuration.default),
);
expect(screen.getByTestId('lg-chat-hotkey-indicator')).toBeVisible();
});

test('When the hotkey indicator is enabled, pressing the hotkey focuses the input', async () => {
render(<InputBar shouldRenderHotkeyIndicator />);

userEvent.keyboard('/');
const textarea = screen.getByRole('textbox');
expect(textarea).toHaveFocus();
});
});
59 changes: 58 additions & 1 deletion chat/input-bar/src/InputBar/InputBar.styles.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { css } from '@leafygreen-ui/emotion';
import { css, keyframes } from '@leafygreen-ui/emotion';
import { Theme } from '@leafygreen-ui/lib';
import { palette } from '@leafygreen-ui/palette';
import {
BaseFontSize,
borderRadius,
focusRing,
fontFamilies,
fontWeights,
Expand Down Expand Up @@ -221,6 +222,62 @@ export const sendButtonDisabledStyles = css`
}
`;

export const baseHotkeyIndicatorStyles = css`
padding: ${spacing[100]}px ${spacing[400]}px;
border-radius: ${borderRadius[400]}px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: default;
user-select: none;
`;

export const themedHotkeyIndicatorStyles = {
[Theme.Dark]: css`
background-color: ${palette.gray.dark4};
border: 1px solid ${palette.gray.dark2};
color: ${palette.gray.light2};
`,
[Theme.Light]: css`
background-color: ${palette.gray.light2};
border: 1px solid ${palette.gray.light2};
color: ${palette.green.dark2};
`,
};

const appearAnimation = keyframes`
from {
opacity: 0;
display: none;
}
to {
opacity: 1;
display: flex;
}
`;

export const hotkeyIndicatorUnfocusedStyles = css`
opacity: 1;
animation: ${appearAnimation} ${transitionDuration.default}ms forwards;
`;

const vanishAnimation = keyframes`
from {
display: flex;
opacity: 1;
}
to {
display: none;
opacity: 0;
}
`;

export const hotkeyIndicatorFocusedStyles = css`
opacity: 0;
animation: ${vanishAnimation} ${transitionDuration.default}ms forwards;
`;

export const getIconFill = (theme: Theme, disabled?: boolean) => {
if (theme === Theme.Dark) {
if (disabled) {
Expand Down
36 changes: 36 additions & 0 deletions chat/input-bar/src/InputBar/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
useAutoScroll,
useBackdropClick,
useDynamicRefs,
useEventListener,
useForwardedRef,
} from '@leafygreen-ui/hooks';
import LeafyGreenProvider, {
Expand All @@ -39,6 +40,7 @@ import { breakpoints } from '@leafygreen-ui/tokens';
import { setReactTextAreaValue } from '../utils/setReactTextAreaValue';

import {
baseHotkeyIndicatorStyles,
baseStyles,
contentWrapperFocusStyles,
contentWrapperStyles,
Expand All @@ -48,11 +50,14 @@ import {
focusStyles,
getIconFill,
gradientAnimationStyles,
hotkeyIndicatorFocusedStyles,
hotkeyIndicatorUnfocusedStyles,
inputStyles,
inputThemeStyles,
leftContentStyles,
rightContentStyles,
sendButtonDisabledStyles,
themedHotkeyIndicatorStyles,
} from './InputBar.styles';
import { ReturnIcon } from './ReturnIcon';
import { SparkleIcon } from './SparkleIcon';
Expand All @@ -65,6 +70,7 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
textareaProps,
onMessageSend,
onSubmit,
shouldRenderHotkeyIndicator = false,
shouldRenderGradient: shouldRenderGradientProp = true,
badgeText,
darkMode: darkModeProp,
Expand Down Expand Up @@ -336,6 +342,20 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
isOpen && withTypeAhead,
);

useEventListener(
'keydown',
(e: KeyboardEvent) => {
if (!e.repeat && e.key === '/' && textareaRef.current) {
e.preventDefault();
e.stopPropagation();
textareaRef.current.focus();
}
},
{
enabled: shouldRenderHotkeyIndicator && !isFocused,
},
);

return (
<LeafyGreenProvider darkMode={darkMode}>
<form
Expand Down Expand Up @@ -366,6 +386,7 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
{badgeText && <Badge variant="blue">{badgeText}</Badge>}
</div>
<TextareaAutosize
aria-keyshortcuts="/"
placeholder={'Type your message here'}
value={messageBody}
disabled={disabled}
Expand All @@ -382,6 +403,21 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
ref={textareaRef}
/>
<div className={rightContentStyles}>
{shouldRenderHotkeyIndicator && !disabled && (
<div
data-testid="lg-chat-hotkey-indicator"
className={cx(
baseHotkeyIndicatorStyles,
themedHotkeyIndicatorStyles[theme],
{
[hotkeyIndicatorFocusedStyles]: isFocused,
[hotkeyIndicatorUnfocusedStyles]: !isFocused,
},
)}
>
/
</div>
)}
<Button
size="small"
rightGlyph={
Expand Down
5 changes: 5 additions & 0 deletions chat/input-bar/src/InputBar/InputBar.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export type InputBarProps = HTMLElementProps<'form'> &
messageBody: string,
e?: FormEvent<HTMLFormElement>,
) => void;
/**
* Toggles the hotkey indicator on the right side of the input
* @default false
*/
shouldRenderHotkeyIndicator?: boolean;
/**
* Toggles the gradient animation around the input
* @default true
Expand Down
Loading