Skip to content

Commit

Permalink
[DEVHUB-1898, UP-6913] Add Hotkey Indicator to Chat InputBar Component (
Browse files Browse the repository at this point in the history
#2476)

* Add hotkey indicator in the InputBar component in lg-chat

* Add test for InputBar hotkey indicator, run changeset script

* Fix styles

* Update variable naming, remove unused import

* Remove unused import

* Run prettier

* Hide hotkey indicator on disabled

* PR review

* Add default value for shouldRenderHotkeyIndicator prop

* Improve hotkey indicator visibility test

* Run autofix

* Fix hotkey indicator animation, focus input on hotkey press when shouldRenderHotkeyIndicator is true

* Remove debug values

* Be more explicit about ref check on hotkey press

* PR review

* Add bool cast to ref check

* Fix failing input focus on hotkey press test
  • Loading branch information
max-melo authored Sep 13, 2024
1 parent f76a5dc commit 79f0e76
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 12 deletions.
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

0 comments on commit 79f0e76

Please sign in to comment.