Skip to content

Commit ae49f3f

Browse files
committed
refactor(input-bar): reorg buttons and rename prop
1 parent e35e983 commit ae49f3f

File tree

12 files changed

+218
-152
lines changed

12 files changed

+218
-152
lines changed

.changeset/large-lands-worry.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@lg-chat/input-bar': minor
3+
---
4+
5+
[LG-5600](https://jira.mongodb.org/browse/LG-5600)
6+
Fix send button disabled logic: the send button now remains enabled during loading state (even with empty message body) to allow users to stop the request. The `disabled` and `disableSend` props still take precedence.
7+
8+
Add `onClickStopButton` prop to handle stop actions during loading state. When triggered, the previous message body is restored to the input field (similar to error state behavior).

chat/input-bar/README.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const Example = () => {
3939
// Simulate API call
4040
};
4141

42-
const handleClickStop = () => {
42+
const handleClickStopButton = () => {
4343
console.log('Stopping request');
4444
setState(State.Unset);
4545
// Cancel your API request here
@@ -49,7 +49,7 @@ const Example = () => {
4949
<LeafyGreenChatProvider variant={Variant.Compact}>
5050
<InputBar
5151
onMessageSend={handleMessageSend}
52-
onClickStop={handleClickStop}
52+
onClickStopButton={handleClickStopButton}
5353
state={state}
5454
errorMessage="Failed to send message. Please try again."
5555
/>
@@ -60,21 +60,21 @@ const Example = () => {
6060

6161
## Properties
6262

63-
| Prop | Type | Description | Default |
64-
| ----------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------- |
65-
| `badgeText` | `string` | Determines the text inside the rendered Badge | |
66-
| `darkMode` | `boolean` | Determines if the component will render in dark mode | `false` |
67-
| `disabled` | `boolean` | Determines whether the user can interact with the InputBar | `false` |
68-
| `disableSend` | `boolean` | When defined as `true`, disables the send action and button | |
69-
| `errorMessage` | `ReactNode` | Custom error message to display when `state='error'` | |
70-
| `onClickStop` | `() => void` | Callback fired when the stop button is clicked during a loading state. Restores the previous message body. | |
71-
| `onMessageSend` | `(messageBody: string, e?: FormEvent) => void` | Callback fired when the user sends a message. | |
72-
| `shouldRenderGradient` | `boolean` | Toggles the gradient animation around the input | `true` |
73-
| `shouldRenderHotkeyIndicator` | `boolean` | Toggles the hotkey indicator on the right side of the input | `false` |
74-
| `textareaProps` | `TextareaAutosizeProps` | Props passed to the TextareaAutosize component. | |
75-
| `textareaRef` | `RefObject<HTMLTextAreaElement>` | Ref object to access the textarea element directly | |
76-
| `state` | `'unset' \| 'error' \| 'loading'` | The current state of the InputBar. | |
77-
| `...` | `HTMLElementProps<'form'>` | Props spread on the root element | |
63+
| Prop | Type | Description | Default |
64+
| ----------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
65+
| `badgeText` | `string` | Determines the text inside the rendered Badge | |
66+
| `darkMode` | `boolean` | Determines if the component will render in dark mode | `false` |
67+
| `disabled` | `boolean` | Determines whether the user can interact with the InputBar | `false` |
68+
| `disableSend` | `boolean` | When defined as `true`, disables the send action and button | |
69+
| `errorMessage` | `ReactNode` | Custom error message to display when `state='error'` | |
70+
| `onClickStopButton` | `() => void` | Callback fired when the stop button is clicked during a loading state. Restores the previous message body. Only applies in compact variant. | |
71+
| `onMessageSend` | `(messageBody: string, e?: FormEvent) => void` | Callback fired when the user sends a message. | |
72+
| `shouldRenderGradient` | `boolean` | Toggles the gradient animation around the input | `true` |
73+
| `shouldRenderHotkeyIndicator` | `boolean` | Toggles the hotkey indicator on the right side of the input | `false` |
74+
| `textareaProps` | `TextareaAutosizeProps` | Props passed to the TextareaAutosize component. | |
75+
| `textareaRef` | `RefObject<HTMLTextAreaElement>` | Ref object to access the textarea element directly | |
76+
| `state` | `'unset' \| 'error' \| 'loading'` | The current state of the InputBar. | |
77+
| `...` | `HTMLElementProps<'form'>` | Props spread on the root element | |
7878

7979
## TextareaAutosizeProps
8080

chat/input-bar/src/InputBar/InputBar.spec.tsx

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -385,31 +385,39 @@ describe('packages/input-bar', () => {
385385
});
386386
});
387387

388-
describe('onClickStop', () => {
389-
test('enables send button during loading state even with empty message', () => {
388+
describe('onClickStopButton', () => {
389+
test('renders stop button during loading state', () => {
390390
renderInputBar({ state: State.Loading });
391391

392-
const sendButton = screen.getByRole('button', { name: 'Send message' });
393-
expect(sendButton).not.toHaveAttribute('aria-disabled', 'true');
392+
const stopButton = screen.getByRole('button', { name: 'Stop message' });
393+
expect(stopButton).toBeInTheDocument();
394+
expect(stopButton).not.toHaveAttribute('aria-disabled', 'true');
394395
});
395396

396-
test('calls onClickStop when button is clicked during loading state', () => {
397-
const onClickStop = jest.fn();
398-
renderInputBar({ state: State.Loading, onClickStop });
397+
test('does not render send button during loading state', () => {
398+
renderInputBar({ state: State.Loading });
399399

400-
const sendButton = screen.getByRole('button', { name: 'Send message' });
401-
userEvent.click(sendButton);
400+
const sendButton = screen.queryByRole('button', { name: 'Send message' });
401+
expect(sendButton).not.toBeInTheDocument();
402+
});
402403

403-
expect(onClickStop).toHaveBeenCalledTimes(1);
404+
test('calls onClickStopButton when button is clicked during loading state', () => {
405+
const onClickStopButton = jest.fn();
406+
renderInputBar({ state: State.Loading, onClickStopButton });
407+
408+
const stopButton = screen.getByRole('button', { name: 'Stop message' });
409+
userEvent.click(stopButton);
410+
411+
expect(onClickStopButton).toHaveBeenCalledTimes(1);
404412
});
405413

406414
test('restores previous message when stop is clicked in uncontrolled mode', () => {
407-
const onClickStop = jest.fn();
415+
const onClickStopButton = jest.fn();
408416
const onMessageSend = jest.fn();
409-
const { rerender } = renderInputBar({ onClickStop, onMessageSend });
417+
const { rerender } = renderInputBar({ onClickStopButton, onMessageSend });
410418

411419
const textarea = screen.getByRole('textbox');
412-
let sendButton = screen.getByRole('button', { name: 'Send message' });
420+
const sendButton = screen.getByRole('button', { name: 'Send message' });
413421

414422
userEvent.type(textarea, TEST_INPUT_TEXT);
415423
expect(textarea).toHaveValue(TEST_INPUT_TEXT);
@@ -422,68 +430,76 @@ describe('packages/input-bar', () => {
422430
<LeafyGreenChatProvider variant={Variant.Compact}>
423431
<InputBar
424432
state={State.Loading}
425-
onClickStop={onClickStop}
433+
onClickStopButton={onClickStopButton}
426434
onMessageSend={onMessageSend}
427435
/>
428436
</LeafyGreenChatProvider>,
429437
);
430438

431-
sendButton = screen.getByRole('button', { name: 'Send message' });
439+
const stopButton = screen.getByRole('button', { name: 'Stop message' });
432440

433-
userEvent.click(sendButton);
441+
userEvent.click(stopButton);
434442

435-
expect(onClickStop).toHaveBeenCalledTimes(1);
443+
expect(onClickStopButton).toHaveBeenCalledTimes(1);
436444
expect(textarea).toHaveValue(TEST_INPUT_TEXT);
437445
});
438446

439447
test('does not call onMessageSend when stopping during loading state', () => {
440448
const onMessageSend = jest.fn();
441-
const onClickStop = jest.fn();
449+
const onClickStopButton = jest.fn();
442450

443451
renderInputBar({
444452
state: State.Loading,
445453
onMessageSend,
446-
onClickStop,
454+
onClickStopButton,
447455
textareaProps: { value: TEST_INPUT_TEXT },
448456
});
449457

450-
const sendButton = screen.getByRole('button', { name: 'Send message' });
451-
userEvent.click(sendButton);
458+
const stopButton = screen.getByRole('button', { name: 'Stop message' });
459+
userEvent.click(stopButton);
452460

453-
expect(onClickStop).toHaveBeenCalledTimes(1);
461+
expect(onClickStopButton).toHaveBeenCalledTimes(1);
454462
expect(onMessageSend).not.toHaveBeenCalled();
455463
});
456464

457465
test('disabled prop takes precedence over loading state', () => {
458-
const onClickStop = jest.fn();
459-
renderInputBar({ state: State.Loading, disabled: true, onClickStop });
466+
const onClickStopButton = jest.fn();
467+
renderInputBar({
468+
state: State.Loading,
469+
disabled: true,
470+
onClickStopButton,
471+
});
460472

461-
const sendButton = screen.getByRole('button', { name: 'Send message' });
462-
expect(sendButton).toHaveAttribute('aria-disabled', 'true');
473+
const stopButton = screen.getByRole('button', { name: 'Stop message' });
474+
expect(stopButton).toHaveAttribute('aria-disabled', 'true');
463475
});
464476

465477
test('disableSend prop takes precedence over loading state', () => {
466-
const onClickStop = jest.fn();
467-
renderInputBar({ state: State.Loading, disableSend: true, onClickStop });
478+
const onClickStopButton = jest.fn();
479+
renderInputBar({
480+
state: State.Loading,
481+
disableSend: true,
482+
onClickStopButton,
483+
});
468484

469-
const sendButton = screen.getByRole('button', { name: 'Send message' });
470-
expect(sendButton).toHaveAttribute('aria-disabled', 'true');
485+
const stopButton = screen.getByRole('button', { name: 'Stop message' });
486+
expect(stopButton).toHaveAttribute('aria-disabled', 'true');
471487
});
472488

473-
test('does not call onClickStop if not in loading state', () => {
474-
const onClickStop = jest.fn();
489+
test('does not call onClickStopButton if not in loading state', () => {
490+
const onClickStopButton = jest.fn();
475491
const onMessageSend = jest.fn();
476492

477493
renderInputBar({
478-
onClickStop,
494+
onClickStopButton,
479495
onMessageSend,
480496
textareaProps: { value: TEST_INPUT_TEXT },
481497
});
482498

483499
const sendButton = screen.getByRole('button', { name: 'Send message' });
484500
userEvent.click(sendButton);
485501

486-
expect(onClickStop).not.toHaveBeenCalled();
502+
expect(onClickStopButton).not.toHaveBeenCalled();
487503
expect(onMessageSend).toHaveBeenCalledTimes(1);
488504
});
489505
});

chat/input-bar/src/InputBar/InputBar.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { breakpoints } from '@leafygreen-ui/tokens';
4848
import { DisclaimerText } from '../DisclaimerText';
4949
import { InputBarFeedback } from '../InputBarFeedback';
5050
import { InputBarSendButton } from '../InputBarSendButton';
51+
import { InputBarStopButton } from '../InputBarStopButton';
5152
import { State } from '../shared.types';
5253
import { setReactTextAreaValue } from '../utils/setReactTextAreaValue';
5354

@@ -76,7 +77,7 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
7677
dropdownFooterSlot,
7778
dropdownProps,
7879
errorMessage,
79-
onClickStop,
80+
onClickStopButton,
8081
onMessageSend,
8182
onSubmit,
8283
shouldRenderGradient: shouldRenderGradientProp = true,
@@ -145,7 +146,8 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
145146

146147
const isLoading = state === State.Loading;
147148
const isSendButtonDisabled =
148-
disabled || disableSend || (!isLoading && messageBody?.trim() === '');
149+
disabled || disableSend || messageBody?.trim() === '';
150+
const isStopButtonDisabled = disabled || !!disableSend;
149151
const shouldRenderGradient =
150152
!isCompact && shouldRenderGradientProp && isFocused && !disabled;
151153
const showHotkeyIndicator =
@@ -368,12 +370,6 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
368370
return;
369371
}
370372

371-
if (isLoading && onClickStop) {
372-
onClickStop();
373-
restorePreviousMessage();
374-
return;
375-
}
376-
377373
if (onMessageSend && messageBody) {
378374
onMessageSend(messageBody, e);
379375
if (!isControlled) {
@@ -385,6 +381,13 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
385381
onSubmit?.(e);
386382
};
387383

384+
const handleStop = () => {
385+
if (onClickStopButton) {
386+
onClickStopButton();
387+
}
388+
restorePreviousMessage();
389+
};
390+
388391
const handleFocus: FocusEventHandler<HTMLTextAreaElement> = _ => {
389392
setIsFocused(true);
390393
openMenu();
@@ -505,12 +508,18 @@ export const InputBar = forwardRef<HTMLFormElement, InputBarProps>(
505508
/
506509
</div>
507510
)}
508-
<InputBarSendButton
509-
disabled={isSendButtonDisabled}
510-
isCompact={isCompact}
511-
shouldRenderButtonText={shouldRenderButtonText}
512-
state={state}
513-
/>
511+
{isLoading && isCompact ? (
512+
<InputBarStopButton
513+
disabled={isStopButtonDisabled}
514+
onClick={handleStop}
515+
/>
516+
) : (
517+
<InputBarSendButton
518+
disabled={isSendButtonDisabled}
519+
isCompact={isCompact}
520+
shouldRenderButtonText={shouldRenderButtonText}
521+
/>
522+
)}
514523
</div>
515524
</div>
516525
</div>

chat/input-bar/src/InputBar/InputBar.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export type InputBarProps = React.ComponentPropsWithoutRef<'form'> &
4949
* Callback fired when the stop button is clicked during a loading state.
5050
* When triggered, the message input will be restored to the previous message body.
5151
*/
52-
onClickStop?: () => void;
52+
onClickStopButton?: () => void;
5353

5454
/**
5555
* Callback fired when the user sends a message.
Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
1-
import { css, cx } from '@leafygreen-ui/emotion';
21
import { Theme } from '@leafygreen-ui/lib';
3-
import { palette } from '@leafygreen-ui/palette';
42
import { color, InteractionState, Variant } from '@leafygreen-ui/tokens';
53

6-
/**
7-
* Off-palette value specific to primary button instances
8-
* @todo Consolidate usage of #00593F
9-
* @see https://jira.mongodb.org/browse/LG-5388
10-
*
11-
* @remarks This is a temporary duplicate to avoid importing the
12-
* `PRIMARY_BUTTON_INTERACTIVE_GREEN` constant from
13-
* `@leafygreen-ui/button/constants`. Consumers are blocked from upgrading
14-
* to the latest version of button package due to type issues from
15-
* `@leafygreen-ui/[email protected]` https://jira.mongodb.org/browse/LG-5462
16-
*/
17-
const PRIMARY_BUTTON_INTERACTIVE_GREEN = '#00593F';
18-
194
export const getIconFill = ({
205
disabled,
216
theme,
@@ -29,61 +14,3 @@ export const getIconFill = ({
2914

3015
return color[theme].icon[Variant.Primary][InteractionState.Default];
3116
};
32-
33-
const getBaseIconButtonStyles = ({ theme }: { theme: Theme }) => {
34-
const darkMode = theme === Theme.Dark;
35-
return css`
36-
background-color: ${palette.green.dark2};
37-
border: 1px solid ${palette.green[darkMode ? 'base' : 'dark2']};
38-
color: ${palette.white};
39-
40-
&:active,
41-
&:hover {
42-
background-color: ${PRIMARY_BUTTON_INTERACTIVE_GREEN};
43-
color: ${palette.white};
44-
box-shadow: 0 0 0 3px ${palette.green[darkMode ? 'dark3' : 'light2']};
45-
}
46-
47-
&:focus-visible {
48-
background-color: ${PRIMARY_BUTTON_INTERACTIVE_GREEN};
49-
color: ${palette.white};
50-
}
51-
`;
52-
};
53-
54-
const getDisabledIconButtonStyles = (theme: Theme) => css`
55-
background-color: ${color[theme].background[Variant.Disabled][
56-
InteractionState.Default
57-
]};
58-
color: ${color[theme].icon[Variant.Disabled][InteractionState.Default]};
59-
border-color: ${color[theme].border[Variant.Disabled][
60-
InteractionState.Default
61-
]};
62-
63-
&:active,
64-
&:hover {
65-
background-color: ${color[theme].background[Variant.Disabled][
66-
InteractionState.Default
67-
]};
68-
color: ${color[theme].icon[Variant.Disabled][InteractionState.Default]};
69-
box-shadow: none;
70-
}
71-
72-
&:focus-visible {
73-
background-color: ${color[theme].background[Variant.Disabled][
74-
InteractionState.Default
75-
]};
76-
color: ${color[theme].icon[Variant.Disabled][InteractionState.Default]};
77-
}
78-
`;
79-
80-
export const getIconButtonStyles = ({
81-
disabled,
82-
theme,
83-
}: {
84-
disabled: boolean;
85-
theme: Theme;
86-
}) =>
87-
cx(getBaseIconButtonStyles({ theme }), {
88-
[getDisabledIconButtonStyles(theme)]: disabled,
89-
});

0 commit comments

Comments
 (0)