Skip to content

Commit

Permalink
feat: Prompt input secondary actions and content slots (#2720)
Browse files Browse the repository at this point in the history
Co-authored-by: Katie George <[email protected]>
  • Loading branch information
katiegeorge and Katie George authored Sep 26, 2024
1 parent 50863f6 commit 680ece3
Show file tree
Hide file tree
Showing 13 changed files with 484 additions and 87 deletions.
33 changes: 24 additions & 9 deletions pages/prompt-input/permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const permutations = createPermutations<PromptInputProps>([
actionButtonIconName: [undefined, 'send'],
value: [
'',
'Short value',
'Long value, enough to extend beyond the input width. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
'Short value 1',
'Long value 1, enough to extend beyond the input width. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
],
},
{
Expand All @@ -30,13 +30,13 @@ const permutations = createPermutations<PromptInputProps>([
{
disabled: [false, true],
actionButtonIconName: [undefined, 'send'],
value: ['', 'Short value'],
value: ['', 'Short value 2'],
},
{
value: [
'',
'Short value',
'Long value, enough to extend beyond the input width. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
'Short value 3',
'Long value 3, enough to extend beyond the input width. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
],
actionButtonIconSvg: [
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" focusable="false" key="0">
Expand All @@ -52,12 +52,27 @@ const permutations = createPermutations<PromptInputProps>([
{
value: [
'',
'Short value',
'Long value, enough to extend beyond the input width. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
'Short value 4',
'Long value 4, enough to extend beyond the input width. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
],
actionButtonIconUrl: [img],
actionButtonIconAlt: ['Letter A'],
},
{
value: ['Short value 5'],
actionButtonIconName: [undefined, 'send'],
secondaryActions: [undefined, 'secondary actions 1'],
secondaryContent: [undefined, 'secondary content 1'],
invalid: [false, true],
},
{
value: ['Short value 6'],
actionButtonIconName: ['send'],
secondaryActions: ['secondary actions 2'],
secondaryContent: ['secondary content 2'],
disableSecondaryActionsPaddings: [false, true],
disableSecondaryContentPaddings: [false, true],
},
]);

export default function PromptInputPermutations() {
Expand All @@ -67,9 +82,9 @@ export default function PromptInputPermutations() {
<ScreenshotArea>
<PermutationsView
permutations={permutations}
render={permutation => (
render={(permutation, index) => (
<PromptInput
ariaLabel="Prompt input field"
ariaLabel={`Prompt input test ${index}`}
actionButtonAriaLabel="Action button aria label"
onChange={() => {
/*empty handler to suppress react controlled property warning*/
Expand Down
104 changes: 100 additions & 4 deletions pages/prompt-input/simple.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useEffect, useState } from 'react';

import { Box, TokenGroup } from '~components';
import ButtonGroup from '~components/button-group';
import Checkbox from '~components/checkbox';
import ColumnLayout from '~components/column-layout';
import FormField from '~components/form-field';
Expand All @@ -10,7 +12,7 @@ import SpaceBetween from '~components/space-between';

import AppContext, { AppContextType } from '../app/app-context';

const MAX_CHARS = 200;
const MAX_CHARS = 2000;

type DemoContext = React.Context<
AppContextType<{
Expand All @@ -19,6 +21,8 @@ type DemoContext = React.Context<
isInvalid: boolean;
hasWarning: boolean;
hasText: boolean;
hasSecondaryContent: boolean;
hasSecondaryActions: boolean;
}>
>;

Expand All @@ -29,7 +33,13 @@ export default function PromptInputPage() {
const [textareaValue, setTextareaValue] = useState('');
const { urlParams, setUrlParams } = useContext(AppContext as DemoContext);

const { isDisabled, isReadOnly, isInvalid, hasWarning, hasText } = urlParams;
const { isDisabled, isReadOnly, isInvalid, hasWarning, hasText, hasSecondaryActions, hasSecondaryContent } =
urlParams;
const [items, setItems] = React.useState([
{ label: 'Item 1', dismissLabel: 'Remove item 1', disabled: isDisabled },
{ label: 'Item 2', dismissLabel: 'Remove item 2', disabled: isDisabled },
{ label: 'Item 3', dismissLabel: 'Remove item 3', disabled: isDisabled },
]);

useEffect(() => {
if (hasText) {
Expand All @@ -44,6 +54,23 @@ export default function PromptInputPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textareaValue]);

useEffect(() => {
if (items.length === 0) {
ref.current?.focus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items]);

useEffect(() => {
const newItems = items.map(item => ({
label: item.label,
dismissLabel: item.dismissLabel,
disabled: isDisabled,
}));
setItems([...newItems]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDisabled]);

const ref = React.createRef<HTMLTextAreaElement>();

return (
Expand All @@ -63,17 +90,40 @@ export default function PromptInputPage() {
<Checkbox checked={hasWarning} onChange={() => setUrlParams({ hasWarning: !hasWarning })}>
Warning
</Checkbox>
<Checkbox
checked={hasSecondaryContent}
onChange={() =>
setUrlParams({
hasSecondaryContent: !hasSecondaryContent,
})
}
>
Secondary content
</Checkbox>
<Checkbox
checked={hasSecondaryActions}
onChange={() =>
setUrlParams({
hasSecondaryActions: !hasSecondaryActions,
})
}
>
Secondary actions
</Checkbox>
</FormField>
<button id="placeholder-text-button" onClick={() => setUrlParams({ hasText: true })}>
Fill with placeholder text
</button>

<button onClick={() => ref.current?.focus()}>Focus component</button>
<button id="focus-button" onClick={() => ref.current?.focus()}>
Focus component
</button>
<button onClick={() => ref.current?.select()}>Select all text</button>

<ColumnLayout columns={2}>
<FormField
errorText={textareaValue.length > MAX_CHARS && 'The query has too many characters.'}
errorText={(textareaValue.length > MAX_CHARS || isInvalid) && 'The query has too many characters.'}
warningText={hasWarning && 'This input has a warning'}
constraintText={
<>
This service is subject to some policy. Character count: {textareaValue.length}/{MAX_CHARS}
Expand All @@ -83,6 +133,7 @@ export default function PromptInputPage() {
i18nStrings={{ errorIconAriaLabel: 'Error' }}
>
<PromptInput
ariaLabel="Chat input"
actionButtonIconName="send"
actionButtonAriaLabel="Submit prompt"
value={textareaValue}
Expand All @@ -95,6 +146,51 @@ export default function PromptInputPage() {
invalid={isInvalid || textareaValue.length > MAX_CHARS}
warning={hasWarning}
ref={ref}
disableSecondaryActionsPaddings={true}
secondaryActions={
hasSecondaryActions ? (
<Box padding={{ left: 'xxs', top: 'xs' }}>
<ButtonGroup
ariaLabel="Chat actions"
items={[
{
type: 'icon-button',
id: 'copy',
iconName: 'upload',
text: 'Upload files',
disabled: isDisabled || isReadOnly,
},
{
type: 'icon-button',
id: 'expand',
iconName: 'expand',
text: 'Go full page',
disabled: isDisabled || isReadOnly,
},
{
type: 'icon-button',
id: 'remove',
iconName: 'remove',
text: 'Remove',
disabled: isDisabled || isReadOnly,
},
]}
variant="icon"
/>
</Box>
) : undefined
}
secondaryContent={
hasSecondaryContent ? (
<TokenGroup
onDismiss={({ detail: { itemIndex } }) => {
setItems([...items.slice(0, itemIndex), ...items.slice(itemIndex + 1)]);
}}
items={items}
readOnly={isReadOnly}
/>
) : undefined
}
/>
</FormField>
<div />
Expand Down
6 changes: 3 additions & 3 deletions pages/utils/permutations-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SpaceBetween from '~components/space-between';

interface PermutationsViewProps<T> {
permutations: ReadonlyArray<T>;
render: (props: T) => React.ReactElement;
render: (props: T, index?: number) => React.ReactElement;
}

function formatValue(key: string, value: any) {
Expand All @@ -24,11 +24,11 @@ function formatValue(key: string, value: any) {
export default function PermutationsView<T>({ permutations, render }: PermutationsViewProps<T>) {
return (
<SpaceBetween size="m">
{permutations.map(permutation => {
{permutations.map((permutation, index) => {
const id = JSON.stringify(permutation, formatValue);
return (
<div key={id} data-permutation={id}>
{render(permutation)}
{render(permutation, index)}
</div>
);
})}
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11938,6 +11938,18 @@ of the user's browser.",
"optional": true,
"type": "boolean",
},
{
"description": "Determines whether the secondary actions area of the input has padding. If true, removes the default padding from the secondary actions area.",
"name": "disableSecondaryActionsPaddings",
"optional": true,
"type": "boolean",
},
{
"description": "Determines whether the secondary content area of the input has padding. If true, removes the default padding from the secondary content area.",
"name": "disableSecondaryContentPaddings",
"optional": true,
"type": "boolean",
},
{
"description": "Specifies if the control is disabled, which prevents the
user from modifying the value and prevents the value from
Expand Down Expand Up @@ -12057,6 +12069,16 @@ In most cases, they aren't needed, as the \`svg\` element inherits styles from t
"isDefault": false,
"name": "actionButtonIconSvg",
},
{
"description": "Use this slot to add secondary actions to the prompt input.",
"isDefault": false,
"name": "secondaryActions",
},
{
"description": "Use this slot to add secondary content, such as file attachments, to the prompt input.",
"isDefault": false,
"name": "secondaryContent",
},
],
"releaseStatus": "stable",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ exports[`test-utils selectors 1`] = `
"prompt-input": [
"awsui_action-button_nr3gs",
"awsui_root_nr3gs",
"awsui_secondary-actions_nr3gs",
"awsui_secondary-content_nr3gs",
"awsui_textarea_nr3gs",
],
"property-filter": [
Expand Down
7 changes: 7 additions & 0 deletions src/internal/styles/forms/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,10 @@
block-size: $height;
inline-size: $width;
}

@mixin control-border-radius-full {
border-start-start-radius: constants.$control-border-radius;
border-start-end-radius: constants.$control-border-radius;
border-end-start-radius: constants.$control-border-radius;
border-end-end-radius: constants.$control-border-radius;
}
13 changes: 11 additions & 2 deletions src/prompt-input/__integ__/prompt-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class PromptInputPage extends BasePageObject {
const setupTest = (testFn: (page: PromptInputPage) => Promise<void>) => {
return useBrowser(async browser => {
const page = new PromptInputPage(browser);
await browser.url(`#/light/prompt-input/simple`);
await browser.url(`#/light/prompt-input/simple/?isReadOnly=true`);
await page.waitForVisible(promptInputWrapper.toSelector());
await testFn(page);
});
Expand All @@ -28,7 +28,16 @@ describe('Prompt input', () => {
setupTest(async page => {
await expect(page.getPromptInputHeight()).resolves.toEqual(32);
await page.click('#placeholder-text-button');
await expect(page.getPromptInputHeight()).resolves.toEqual(92);
await expect(page.getPromptInputHeight()).resolves.toEqual(96);
})
);

test(
'Action button should be focusable in read-only state',
setupTest(async page => {
await page.click('#focus-button');
await page.keys('Tab');
await expect(page.isFocused(promptInputWrapper.find('button').toSelector())).resolves.toBe(true);
})
);
});
Loading

0 comments on commit 680ece3

Please sign in to comment.