From f7d82aec220c6acaa5c87974f49967e21297fd40 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Thu, 12 Dec 2024 16:26:24 -0500 Subject: [PATCH 1/3] feat(ResponseActions): Add click state Add click state to response actions block. The last currently selected item will remain selected. Clients can also pass in custom tooltips or aria labels based on the clicked state. --- .../MessageWithCustomResponseActions.tsx | 4 + .../chatbot/examples/Messages/Messages.md | 2 +- .../ResponseActionButton.test.tsx | 52 +++++++++ .../ResponseActions/ResponseActionButton.tsx | 72 +++++++----- .../src/ResponseActions/ResponseActions.scss | 18 +-- .../ResponseActions/ResponseActions.test.tsx | 108 +++++++++++++++++- .../src/ResponseActions/ResponseActions.tsx | 61 ++++++++-- 7 files changed, 269 insertions(+), 48 deletions(-) create mode 100644 packages/module/src/ResponseActions/ResponseActionButton.test.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx index 493860cd..d658915b 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx @@ -15,16 +15,20 @@ export const CustomActionExample: React.FunctionComponent = () => ( actions={{ regenerate: { ariaLabel: 'Regenerate', + clickedAriaLabel: 'Regenerated', // eslint-disable-next-line no-console onClick: () => console.log('Clicked regenerate'), tooltipContent: 'Regenerate', + clickedTooltipContent: 'Regenerated', icon: }, download: { ariaLabel: 'Download', + clickedAriaLabel: 'Downloaded', // eslint-disable-next-line no-console onClick: () => console.log('Clicked download'), tooltipContent: 'Download', + clickedTooltipContent: 'Downloaded', icon: }, info: { diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 4a97b6dc..0f28761d 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -79,7 +79,7 @@ You can add actions to a message, to allow users to interact with the message co ### Custom message actions -Beyond the standard message actions (positive, negative, copy, share, or listen), you can add custom actions to a bot message by passing an `actions` object to the `` component. This object can contain the following customizations: `ariaLabel`, `onClick`, `className`, `isDisabled`, `tooltipContent`, `tooltipProps`, and `icon`. +Beyond the standard message actions (positive, negative, copy, share, or listen), you can add custom actions to a bot message by passing an `actions` object to the `` component. This object can contain the following customizations: `ariaLabel`, `clickedAriaLabel`, `onClick`, `className`, `isDisabled`, `tooltipContent`, `tooltipContent`, `tooltipProps`, and `icon`. `clickedAriaLabel` and `clickedTooltipContent` are applied only when a button is clicked. If `clickedAriaLabel` or `clickedTooltipContent` are omitted, they will default to the `ariaLabel` or `tooltipContent` supplied. ```js file="./MessageWithCustomResponseActions.tsx" diff --git a/packages/module/src/ResponseActions/ResponseActionButton.test.tsx b/packages/module/src/ResponseActions/ResponseActionButton.test.tsx new file mode 100644 index 00000000..9cd3f8d2 --- /dev/null +++ b/packages/module/src/ResponseActions/ResponseActionButton.test.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import { DownloadIcon } from '@patternfly/react-icons'; +import ResponseActionButton from './ResponseActionButton'; + +describe('ResponseActionButton', () => { + it('renders aria-label correctly if not clicked', () => { + render(} ariaLabel="Download" clickedAriaLabel="Downloaded" />); + expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy(); + }); + it('renders aria-label correctly if clicked', () => { + render( + } ariaLabel="Download" clickedAriaLabel="Downloaded" isClicked /> + ); + expect(screen.getByRole('button', { name: 'Downloaded' })).toBeTruthy(); + }); + it('renders tooltip correctly if not clicked', async () => { + render( + } tooltipContent="Download" clickedTooltipContent="Downloaded" /> + ); + expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy(); + // clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked + await userEvent.click(screen.getByRole('button', { name: 'Download' })); + expect(screen.getByRole('tooltip', { name: 'Download' })).toBeTruthy(); + }); + it('renders tooltip correctly if clicked', async () => { + render( + } + tooltipContent="Download" + clickedTooltipContent="Downloaded" + isClicked + /> + ); + expect(screen.getByRole('button', { name: 'Downloaded' })).toBeTruthy(); + // clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked + await userEvent.click(screen.getByRole('button', { name: 'Downloaded' })); + expect(screen.getByRole('tooltip', { name: 'Downloaded' })).toBeTruthy(); + }); + it('if clicked variant for tooltip is not supplied, it uses the default', async () => { + render(} tooltipContent="Download" isClicked />); + // clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked + await userEvent.click(screen.getByRole('button', { name: 'Download' })); + expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy(); + }); + it('if clicked variant for aria label is not supplied, it uses the default', async () => { + render(} ariaLabel="Download" isClicked />); + expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy(); + }); +}); diff --git a/packages/module/src/ResponseActions/ResponseActionButton.tsx b/packages/module/src/ResponseActions/ResponseActionButton.tsx index 0d54a633..289ee4f6 100644 --- a/packages/module/src/ResponseActions/ResponseActionButton.tsx +++ b/packages/module/src/ResponseActions/ResponseActionButton.tsx @@ -4,6 +4,8 @@ import { Button, Icon, Tooltip, TooltipProps } from '@patternfly/react-core'; export interface ResponseActionButtonProps { /** Aria-label for the button. Defaults to the value of the tooltipContent if none provided */ ariaLabel?: string; + /** Aria-label for the button, shown when the button is clicked. Defaults to the value of ariaLabel or tooltipContent if not provided. */ + clickedAriaLabel?: string; /** Icon for the button */ icon: React.ReactNode; /** On-click handler for the button */ @@ -14,43 +16,59 @@ export interface ResponseActionButtonProps { isDisabled?: boolean; /** Content shown in the tooltip */ tooltipContent?: string; + /** Content shown in the tooltip when the button is clicked. Defaults to the value of tooltipContent if not provided. */ + clickedTooltipContent?: string; /** Props to control the PF Tooltip component */ tooltipProps?: TooltipProps; + /** Whether button is in clicked state */ + isClicked?: boolean; } export const ResponseActionButton: React.FunctionComponent = ({ ariaLabel, + clickedAriaLabel = ariaLabel, className, icon, isDisabled, onClick, tooltipContent, - tooltipProps -}) => ( - - - -); + clickedTooltipContent = tooltipContent, + tooltipProps, + isClicked = false +}) => { + const generateAriaLabel = () => { + if (ariaLabel) { + return isClicked ? clickedAriaLabel : ariaLabel; + } + return isClicked ? clickedTooltipContent : tooltipContent; + }; + + return ( + + + + ); +}; export default ResponseActionButton; diff --git a/packages/module/src/ResponseActions/ResponseActions.scss b/packages/module/src/ResponseActions/ResponseActions.scss index 5b305589..e40616a7 100644 --- a/packages/module/src/ResponseActions/ResponseActions.scss +++ b/packages/module/src/ResponseActions/ResponseActions.scss @@ -4,6 +4,7 @@ grid-template-columns: repeat(auto-fit, minmax(0, max-content)); .pf-v6-c-button { + --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle); border-radius: var(--pf-t--global--border--radius--pill); width: 2.3125rem; height: 2.3125rem; @@ -11,16 +12,17 @@ align-items: center; justify-content: center; - .pf-v6-c-button__icon { - color: var(--pf-t--global--icon--color--subtle); + &:hover { + --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle); } - - // Interactive states - &:hover, &:focus { - .pf-v6-c-button__icon { - color: var(--pf-t--global--icon--color--subtle); - } + --pf-v6-c-button--hover--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked); + --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular); } } } + +.pf-v6-c-button.pf-chatbot__button--response-action-clicked { + --pf-v6-c-button--m-plain--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked); + --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular); +} diff --git a/packages/module/src/ResponseActions/ResponseActions.test.tsx b/packages/module/src/ResponseActions/ResponseActions.test.tsx index 997bcd81..ac94af7a 100644 --- a/packages/module/src/ResponseActions/ResponseActions.test.tsx +++ b/packages/module/src/ResponseActions/ResponseActions.test.tsx @@ -4,27 +4,32 @@ import '@testing-library/jest-dom'; import ResponseActions from './ResponseActions'; import userEvent from '@testing-library/user-event'; import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons'; +import Message from '../Message'; const ALL_ACTIONS = [ - { type: 'positive', label: 'Good response' }, - { type: 'negative', label: 'Bad response' }, - { type: 'copy', label: 'Copy' }, - { type: 'share', label: 'Share' }, - { type: 'listen', label: 'Listen' } + { type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' }, + { type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' }, + { type: 'copy', label: 'Copy', clickedLabel: 'Copied' }, + { type: 'share', label: 'Share', clickedLabel: 'Shared' }, + { type: 'listen', label: 'Listen', clickedLabel: 'Listening' } ]; const CUSTOM_ACTIONS = [ { regenerate: { ariaLabel: 'Regenerate', + clickedAriaLabel: 'Regenerated', onClick: jest.fn(), tooltipContent: 'Regenerate', + clickedTooltipContent: 'Regenerated', icon: }, download: { ariaLabel: 'Download', + clickedAriaLabel: 'Downloaded', onClick: jest.fn(), tooltipContent: 'Download', + clickedTooltipContent: 'Downloaded', icon: }, info: { @@ -37,6 +42,81 @@ const CUSTOM_ACTIONS = [ ]; describe('ResponseActions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should handle click within group of buttons correctly', async () => { + render( + + ); + const goodBtn = screen.getByRole('button', { name: 'Good response' }); + const badBtn = screen.getByRole('button', { name: 'Bad response' }); + const copyBtn = screen.getByRole('button', { name: 'Copy' }); + const shareBtn = screen.getByRole('button', { name: 'Share' }); + const listenBtn = screen.getByRole('button', { name: 'Listen' }); + const buttons = [goodBtn, badBtn, copyBtn, shareBtn, listenBtn]; + buttons.forEach((button) => { + expect(button).toBeTruthy(); + }); + await userEvent.click(goodBtn); + expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + let unclickedButtons = buttons.filter((button) => button !== goodBtn); + unclickedButtons.forEach((button) => { + expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + }); + await userEvent.click(badBtn); + expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + unclickedButtons = buttons.filter((button) => button !== badBtn); + unclickedButtons.forEach((button) => { + expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + }); + }); + it('should handle click outside of group of buttons correctly', async () => { + // using message just so we have something outside the group that's rendered + render( + + ); + const goodBtn = screen.getByRole('button', { name: 'Good response' }); + const badBtn = screen.getByRole('button', { name: 'Bad response' }); + expect(goodBtn).toBeTruthy(); + expect(badBtn).toBeTruthy(); + + await userEvent.click(goodBtn); + expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + + await userEvent.click(badBtn); + expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass( + 'pf-chatbot__button--response-action-clicked' + ); + expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + await userEvent.click(screen.getByText('Example with all prebuilt actions')); + expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); + }); it('should render buttons correctly', () => { ALL_ACTIONS.forEach(({ type, label }) => { render(); @@ -53,6 +133,24 @@ describe('ResponseActions', () => { }); }); + it('should swap clicked and non-clicked aria labels on click', async () => { + ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => { + render(); + expect(screen.getByRole('button', { name: label })).toBeTruthy(); + await userEvent.click(screen.getByRole('button', { name: label })); + expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy(); + }); + }); + + it('should swap clicked and non-clicked tooltips on click', async () => { + ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => { + render(); + expect(screen.getByRole('button', { name: label })).toBeTruthy(); + await userEvent.click(screen.getByRole('button', { name: label })); + expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy(); + }); + }); + it('should be able to change aria labels', () => { const actions = [ { type: 'positive', ariaLabel: 'Thumbs up' }, diff --git a/packages/module/src/ResponseActions/ResponseActions.tsx b/packages/module/src/ResponseActions/ResponseActions.tsx index a766a8f9..b8462fe3 100644 --- a/packages/module/src/ResponseActions/ResponseActions.tsx +++ b/packages/module/src/ResponseActions/ResponseActions.tsx @@ -12,6 +12,8 @@ import { TooltipProps } from '@patternfly/react-core'; export interface ActionProps { /** Aria-label for the button */ ariaLabel?: string; + /** Aria-label for the button, shown when the button is clicked. */ + clickedAriaLabel?: string; /** On-click handler for the button */ onClick?: ((event: MouseEvent | React.MouseEvent | KeyboardEvent) => void) | undefined; /** Class name for the button */ @@ -20,6 +22,8 @@ export interface ActionProps { isDisabled?: boolean; /** Content shown in the tooltip */ tooltipContent?: string; + /** Content shown in the tooltip when the button is clicked. */ + clickedTooltipContent?: string; /** Props to control the PF Tooltip component */ tooltipProps?: TooltipProps; /** Icon for custom response action */ @@ -38,74 +42,117 @@ export interface ResponseActionProps { } export const ResponseActions: React.FunctionComponent = ({ actions }) => { + const [activeButton, setActiveButton] = React.useState(); const { positive, negative, copy, share, listen, ...additionalActions } = actions; + const responseActions = React.useRef(null); + + React.useEffect(() => { + const handleClickOutside = (e) => { + if (responseActions.current && !responseActions.current.contains(e.target)) { + setActiveButton(undefined); + } + }; + window.addEventListener('click', handleClickOutside); + + return () => { + window.removeEventListener('click', handleClickOutside); + }; + }, []); + + const handleClick = ( + e: MouseEvent | React.MouseEvent | KeyboardEvent, + id: string, + onClick?: (event: MouseEvent | React.MouseEvent | KeyboardEvent) => void + ) => { + setActiveButton(id); + onClick && onClick(e); + }; + return ( -
+
{positive && ( handleClick(e, 'positive', positive.onClick)} className={positive.className} isDisabled={positive.isDisabled} tooltipContent={positive.tooltipContent ?? 'Good response'} + clickedTooltipContent={positive.clickedTooltipContent ?? 'Response recorded'} tooltipProps={positive.tooltipProps} icon={} + isClicked={activeButton === 'positive'} > )} {negative && ( handleClick(e, 'negative', negative.onClick)} className={negative.className} isDisabled={negative.isDisabled} tooltipContent={negative.tooltipContent ?? 'Bad response'} + clickedTooltipContent={negative.clickedTooltipContent ?? 'Response recorded'} tooltipProps={negative.tooltipProps} icon={} + isClicked={activeButton === 'negative'} > )} {copy && ( handleClick(e, 'copy', copy.onClick)} className={copy.className} isDisabled={copy.isDisabled} tooltipContent={copy.tooltipContent ?? 'Copy'} + clickedTooltipContent={copy.clickedTooltipContent ?? 'Copied'} tooltipProps={copy.tooltipProps} icon={} + isClicked={activeButton === 'copy'} > )} {share && ( handleClick(e, 'share', share.onClick)} className={share.className} isDisabled={share.isDisabled} tooltipContent={share.tooltipContent ?? 'Share'} + clickedTooltipContent={share.clickedTooltipContent ?? 'Shared'} tooltipProps={share.tooltipProps} icon={} + isClicked={activeButton === 'share'} > )} {listen && ( handleClick(e, 'listen', listen.onClick)} className={listen.className} isDisabled={listen.isDisabled} tooltipContent={listen.tooltipContent ?? 'Listen'} + clickedTooltipContent={listen.clickedTooltipContent ?? 'Listening'} tooltipProps={listen.tooltipProps} icon={} + isClicked={activeButton === 'listen'} > )} {Object.keys(additionalActions).map((action) => ( handleClick(e, action, additionalActions[action]?.onClick)} className={additionalActions[action]?.className} isDisabled={additionalActions[action]?.isDisabled} tooltipContent={additionalActions[action]?.tooltipContent} tooltipProps={additionalActions[action]?.tooltipProps} + clickedTooltipContent={additionalActions[action]?.clickedTooltipContent} icon={additionalActions[action]?.icon} + isClicked={activeButton === action} /> ))}
From 64633dc8020992068fe15c682ba025bb53048bdc Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 13 Dec 2024 12:15:49 -0500 Subject: [PATCH 2/3] Update packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md Co-authored-by: Erin Donehoo <105813956+edonehoo@users.noreply.github.com> --- .../chatbot/examples/Messages/Messages.md | 15 +++++++++++++-- .../extensions/chatbot/examples/demos/Chatbot.md | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 0f28761d..7033369a 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -63,7 +63,7 @@ You can further customize the avatar by applying an additional class or passing ``` -### Messages actions +### Message actions You can add actions to a message, to allow users to interact with the message content. These actions can include: @@ -79,7 +79,18 @@ You can add actions to a message, to allow users to interact with the message co ### Custom message actions -Beyond the standard message actions (positive, negative, copy, share, or listen), you can add custom actions to a bot message by passing an `actions` object to the `` component. This object can contain the following customizations: `ariaLabel`, `clickedAriaLabel`, `onClick`, `className`, `isDisabled`, `tooltipContent`, `tooltipContent`, `tooltipProps`, and `icon`. `clickedAriaLabel` and `clickedTooltipContent` are applied only when a button is clicked. If `clickedAriaLabel` or `clickedTooltipContent` are omitted, they will default to the `ariaLabel` or `tooltipContent` supplied. +Beyond the standard message actions (good response, bad response, copy, share, or listen), you can add custom actions to a bot message by passing an `actions` object to the `` component. This object can contain the following customizations: + +- `ariaLabel` +- `onClick` +- `className` +- `isDisabled` +- `tooltipContent` +- `tooltipContent` +- `tooltipProps` +- `icon` + +You can apply a `clickedAriaLabel` and `clickedTooltipContent` once a button is clicked. If either of these props are omitted, their values will default to the `ariaLabel` or `tooltipContent` supplied. ```js file="./MessageWithCustomResponseActions.tsx" diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md index 4db1d10c..81a8ee63 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md @@ -66,7 +66,7 @@ This demo displays a basic ChatBot, which includes: 4. [`` and ``](/patternfly-ai/chatbot/ui#content-and-message-box) with: - A `` -- An initial [user ``](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages#messages-actions) +- An initial [user ``](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages#message-actions) - Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef` 5. A [``](/patternfly-ai/chatbot/ui#footer) with a [``](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `` that contains the abilities of: @@ -92,7 +92,7 @@ This demo displays an embedded ChatBot. Embedded ChatBots are meant to be placed 3. A [``](/patternfly-ai/chatbot/ui#header) with all built sub-components laid out, including a `` 4. [`` and ``](/patternfly-ai/chatbot/ui#content-and-message-box) with: - A `` - - An initial [user ``](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages/#messages-actions) + - An initial [user ``](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages/#message-actions) - Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef` 5. A [``](/patternfly-ai/chatbot/ui#footer) with a [``](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `` that contains the abilities of: - [Speech to text.](/patternfly-ai/chatbot/ui#message-bar-with-speech-recognition-and-file-attachment) From 862113af99d65e6b9a2fc7daf81090c7e336ba43 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 17 Dec 2024 16:31:08 -0500 Subject: [PATCH 3/3] Address feedback --- packages/module/src/ResponseActions/ResponseActionButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/module/src/ResponseActions/ResponseActionButton.tsx b/packages/module/src/ResponseActions/ResponseActionButton.tsx index 289ee4f6..8ef2e5ac 100644 --- a/packages/module/src/ResponseActions/ResponseActionButton.tsx +++ b/packages/module/src/ResponseActions/ResponseActionButton.tsx @@ -47,6 +47,7 @@ export const ResponseActionButton: React.FunctionComponent