Skip to content

Commit 2032136

Browse files
feat: Allow custom primary actions in prompt input (#3655)
1 parent 29f54b7 commit 2032136

File tree

10 files changed

+172
-54
lines changed

10 files changed

+172
-54
lines changed

pages/prompt-input/permutations.page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import React from 'react';
44

5+
import { Button } from '~components';
56
import PromptInput, { PromptInputProps } from '~components/prompt-input';
67

78
import img from '../icon/custom-icon.png';
@@ -73,6 +74,16 @@ const permutations = createPermutations<PromptInputProps>([
7374
disableSecondaryActionsPaddings: [false, true],
7475
disableSecondaryContentPaddings: [false, true],
7576
},
77+
{
78+
value: ['Short value for custom primary actions'],
79+
actionButtonIconName: [undefined, 'send'],
80+
customPrimaryAction: [
81+
undefined,
82+
<Button variant="icon" iconName="add-plus" ariaLabel="Custom action" key="button" />,
83+
],
84+
secondaryActions: [undefined, 'secondary actions'],
85+
disableSecondaryActionsPaddings: [false, true],
86+
},
7687
]);
7788

7889
export default function PromptInputPermutations() {

pages/prompt-input/simple.page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type DemoContext = React.Context<
3131
hasText: boolean;
3232
hasSecondaryContent: boolean;
3333
hasSecondaryActions: boolean;
34+
hasPrimaryActions: boolean;
3435
hasInfiniteMaxRows: boolean;
3536
}>
3637
>;
@@ -52,6 +53,7 @@ export default function PromptInputPage() {
5253
hasText,
5354
hasSecondaryActions,
5455
hasSecondaryContent,
56+
hasPrimaryActions,
5557
hasInfiniteMaxRows,
5658
} = urlParams;
5759

@@ -141,6 +143,16 @@ export default function PromptInputPage() {
141143
>
142144
Secondary actions
143145
</Checkbox>
146+
<Checkbox
147+
checked={hasPrimaryActions}
148+
onChange={() =>
149+
setUrlParams({
150+
hasPrimaryActions: !hasPrimaryActions,
151+
})
152+
}
153+
>
154+
Custom primary actions
155+
</Checkbox>
144156
<Checkbox
145157
checked={hasInfiniteMaxRows}
146158
onChange={() =>
@@ -191,6 +203,29 @@ export default function PromptInputPage() {
191203
warning={hasWarning}
192204
ref={ref}
193205
disableSecondaryActionsPaddings={true}
206+
customPrimaryAction={
207+
hasPrimaryActions ? (
208+
<ButtonGroup
209+
variant="icon"
210+
items={[
211+
{
212+
type: 'icon-button',
213+
id: 'record',
214+
text: 'Record',
215+
iconName: 'microphone',
216+
disabled: isDisabled || isReadOnly,
217+
},
218+
{
219+
type: 'icon-button',
220+
id: 'submit',
221+
text: 'Submit',
222+
iconName: 'send',
223+
disabled: isDisabled || isReadOnly,
224+
},
225+
]}
226+
/>
227+
) : undefined
228+
}
194229
secondaryActions={
195230
hasSecondaryActions ? (
196231
<Box padding={{ left: 'xxs', top: 'xs' }}>

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16763,6 +16763,16 @@ In most cases, they aren't needed, as the \`svg\` element inherits styles from t
1676316763
"isDefault": false,
1676416764
"name": "actionButtonIconSvg",
1676516765
},
16766+
{
16767+
"description": "Use this to replace the primary action.
16768+
If this is provided then any other \`actionButton*\` properties will be ignored.
16769+
Note that you should still provide an \`onAction\` function in order to handle keyboard submission.",
16770+
"isDefault": false,
16771+
"name": "customPrimaryAction",
16772+
"systemTags": [
16773+
"core",
16774+
],
16775+
},
1676616776
{
1676716777
"description": "Use this slot to add secondary actions to the prompt input.",
1676816778
"isDefault": false,
@@ -74673,6 +74683,7 @@ Note that programmatic events ignore disabled attribute and will trigger listene
7467374683
},
7467474684
},
7467574685
{
74686+
"description": "Finds the action button. Note that, despite its typings, this may return null.",
7467674687
"name": "findActionButton",
7467774688
"parameters": [],
7467874689
"returnType": {
@@ -74832,6 +74843,14 @@ Component's wrapper class",
7483274843
"type": "union",
7483374844
},
7483474845
},
74846+
{
74847+
"name": "findCustomPrimaryAction",
74848+
"parameters": [],
74849+
"returnType": {
74850+
"name": "ElementWrapper | null",
74851+
"type": "union",
74852+
},
74853+
},
7483574854
{
7483674855
"name": "findNativeTextarea",
7483774856
"parameters": [],
@@ -74847,6 +74866,7 @@ Component's wrapper class",
7484774866
},
7484874867
},
7484974868
{
74869+
"description": "Finds the secondary actions slot. Note that, despite its typings, this may return null.",
7485074870
"name": "findSecondaryActions",
7485174871
"parameters": [],
7485274872
"returnType": {
@@ -74864,14 +74884,8 @@ Component's wrapper class",
7486474884
"name": "findSecondaryContent",
7486574885
"parameters": [],
7486674886
"returnType": {
74867-
"name": "ElementWrapper",
74868-
"type": "reference",
74869-
"typeArguments": [
74870-
{
74871-
"name": "HTMLDivElement",
74872-
"type": "reference",
74873-
},
74874-
],
74887+
"name": "ElementWrapper | null",
74888+
"type": "union",
7487574889
},
7487674890
},
7487774891
{
@@ -133883,6 +133897,7 @@ If not specified, the method returns the result text that is currently displayed
133883133897
},
133884133898
},
133885133899
{
133900+
"description": "Finds the action button. Note that, despite its typings, this may return null.",
133886133901
"name": "findActionButton",
133887133902
"parameters": [],
133888133903
"returnType": {
@@ -134045,6 +134060,15 @@ Component's wrapper class",
134045134060
"type": "typeParameter",
134046134061
},
134047134062
},
134063+
{
134064+
"name": "findCustomPrimaryAction",
134065+
"parameters": [],
134066+
"returnType": {
134067+
"name": "ElementWrapper",
134068+
"type": "reference",
134069+
"typeArguments": [],
134070+
},
134071+
},
134048134072
{
134049134073
"name": "findNativeTextarea",
134050134074
"parameters": [],
@@ -134055,6 +134079,7 @@ Component's wrapper class",
134055134079
},
134056134080
},
134057134081
{
134082+
"description": "Finds the secondary actions slot. Note that, despite its typings, this may return null.",
134058134083
"name": "findSecondaryActions",
134059134084
"parameters": [],
134060134085
"returnType": {

src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ exports[`test-utils selectors 1`] = `
478478
],
479479
"prompt-input": [
480480
"awsui_action-button_nr3gs",
481+
"awsui_primary-action_nr3gs",
481482
"awsui_root_nr3gs",
482483
"awsui_secondary-actions_nr3gs",
483484
"awsui_secondary-content_nr3gs",

src/prompt-input/__tests__/prompt-input.test.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe('action button', () => {
136136
expect(wrapper.findActionButton().getElement()).toBeInTheDocument();
137137
});
138138

139-
test('should render action button inside secondary actions container when secondary actions are present', () => {
139+
test('should not find primary button within secondaryActions', () => {
140140
const { wrapper } = renderPromptInput({
141141
value: '',
142142
minRows: 4,
@@ -145,9 +145,9 @@ describe('action button', () => {
145145
});
146146

147147
const secondaryActionsContainer = wrapper.findSecondaryActions()!.getElement();
148-
const actionButton = within(secondaryActionsContainer).getByRole('button');
148+
const actionButton = within(secondaryActionsContainer).queryByRole('button');
149149

150-
expect(actionButton).toBeInTheDocument();
150+
expect(actionButton).toBeFalsy();
151151
});
152152

153153
test('disabled when in disabled state', () => {
@@ -171,6 +171,30 @@ describe('action button', () => {
171171
});
172172
});
173173

174+
describe('custom primary action', () => {
175+
test('customPrimaryAction can be provided', () => {
176+
const { wrapper } = renderPromptInput({
177+
value: '',
178+
actionButtonIconName: 'send',
179+
customPrimaryAction: (
180+
<>
181+
<button>One</button>
182+
<button>Two</button>
183+
</>
184+
),
185+
});
186+
expect(wrapper.findCustomPrimaryAction()!.getElement().querySelectorAll('button').length).toBe(2);
187+
});
188+
test('default primary action is removed if custom primaryAction provided', () => {
189+
const { wrapper } = renderPromptInput({
190+
value: '',
191+
actionButtonIconName: 'send',
192+
customPrimaryAction: 'custom content',
193+
});
194+
expect(wrapper.findActionButton()).toBeFalsy();
195+
});
196+
});
197+
174198
describe('prompt input in form', () => {
175199
function renderPromptInputInForm(props: PromptInputProps = { value: '', actionButtonIconName: 'send' }) {
176200
const submitSpy = jest.fn();

src/prompt-input/interfaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ export interface PromptInputProps
8585
*/
8686
maxRows?: number;
8787

88+
/**
89+
* Use this to replace the primary action.
90+
* If this is provided then any other `actionButton*` properties will be ignored.
91+
* Note that you should still provide an `onAction` function in order to handle keyboard submission.
92+
*
93+
* @awsuiSystem core
94+
*/
95+
customPrimaryAction?: React.ReactNode;
96+
8897
/**
8998
* Use this slot to add secondary actions to the prompt input.
9099
*/

src/prompt-input/internal.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const InternalPromptInput = React.forwardRef(
5050
placeholder,
5151
readOnly,
5252
spellcheck,
53+
customPrimaryAction,
5354
secondaryActions,
5455
secondaryContent,
5556
disableSecondaryActionsPaddings,
@@ -105,7 +106,7 @@ const InternalPromptInput = React.forwardRef(
105106
adjustTextareaHeight();
106107
};
107108

108-
const hasActionButton = actionButtonIconName || actionButtonIconSvg || actionButtonIconUrl;
109+
const hasActionButton = actionButtonIconName || actionButtonIconSvg || actionButtonIconUrl || customPrimaryAction;
109110

110111
const adjustTextareaHeight = useCallback(() => {
111112
if (textareaRef.current) {
@@ -173,19 +174,21 @@ const InternalPromptInput = React.forwardRef(
173174
}
174175

175176
const action = (
176-
<div className={styles.button}>
177-
<InternalButton
178-
className={clsx(styles['action-button'], testutilStyles['action-button'])}
179-
ariaLabel={actionButtonAriaLabel}
180-
disabled={disabled || readOnly || disableActionButton}
181-
__focusable={readOnly}
182-
iconName={actionButtonIconName}
183-
iconUrl={actionButtonIconUrl}
184-
iconSvg={actionButtonIconSvg}
185-
iconAlt={actionButtonIconAlt}
186-
onClick={() => fireNonCancelableEvent(onAction, { value })}
187-
variant="icon"
188-
/>
177+
<div className={clsx(styles['primary-action'], testutilStyles['primary-action'])}>
178+
{customPrimaryAction ?? (
179+
<InternalButton
180+
className={clsx(styles['action-button'], testutilStyles['action-button'])}
181+
ariaLabel={actionButtonAriaLabel}
182+
disabled={disabled || readOnly || disableActionButton}
183+
__focusable={readOnly}
184+
iconName={actionButtonIconName}
185+
iconUrl={actionButtonIconUrl}
186+
iconSvg={actionButtonIconSvg}
187+
iconAlt={actionButtonIconAlt}
188+
onClick={() => fireNonCancelableEvent(onAction, { value })}
189+
variant="icon"
190+
/>
191+
)}
189192
</div>
190193
);
191194

@@ -219,13 +222,20 @@ const InternalPromptInput = React.forwardRef(
219222
</div>
220223
{secondaryActions && (
221224
<div
222-
className={clsx(styles['secondary-actions'], testutilStyles['secondary-actions'], {
223-
[styles['with-paddings']]: !disableSecondaryActionsPaddings,
225+
className={clsx(styles['action-stripe'], {
224226
[styles.invalid]: invalid,
225227
[styles.warning]: warning,
226228
})}
227229
>
228-
{secondaryActions}
230+
<div
231+
className={clsx(styles['secondary-actions'], testutilStyles['secondary-actions'], {
232+
[styles['with-paddings']]: !disableSecondaryActionsPaddings,
233+
[styles.invalid]: invalid,
234+
[styles.warning]: warning,
235+
})}
236+
>
237+
{secondaryActions}
238+
</div>
229239
<div className={styles.buffer} onClick={() => textareaRef.current?.focus()} />
230240
{hasActionButton && action}
231241
</div>

0 commit comments

Comments
 (0)