Skip to content

Commit

Permalink
fix: Prompt input has incorrect height on first render when expanding…
Browse files Browse the repository at this point in the history
… split panel (#2947)
  • Loading branch information
YueyingLu authored Oct 30, 2024
1 parent 1a8e5d9 commit 8f5431a
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 128 deletions.
1 change: 1 addition & 0 deletions pages/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function isAppLayoutPage(pageId?: string) {
'expandable-rows-test',
'container/sticky-permutations',
'copy-to-clipboard/scenario-split-panel',
'prompt-input/simple',
];
return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match));
}
Expand Down
275 changes: 153 additions & 122 deletions pages/prompt-input/simple.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useEffect, useState } from 'react';

import { Box, TokenGroup } from '~components';
import { AppLayout, Box, SplitPanel, TokenGroup } from '~components';
import ButtonGroup from '~components/button-group';
import Checkbox from '~components/checkbox';
import ColumnLayout from '~components/column-layout';
Expand All @@ -11,6 +11,7 @@ import PromptInput from '~components/prompt-input';
import SpaceBetween from '~components/space-between';

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

const MAX_CHARS = 2000;

Expand All @@ -31,6 +32,7 @@ const placeholderText =

export default function PromptInputPage() {
const [textareaValue, setTextareaValue] = useState('');
const [valueInSplitPanel, setValueInSplitPanel] = useState('');
const { urlParams, setUrlParams } = useContext(AppContext as DemoContext);

const { isDisabled, isReadOnly, isInvalid, hasWarning, hasText, hasSecondaryActions, hasSecondaryContent } =
Expand Down Expand Up @@ -74,128 +76,157 @@ export default function PromptInputPage() {
const ref = React.createRef<HTMLTextAreaElement>();

return (
<div style={{ padding: 10 }}>
<h1>PromptInput demo</h1>
<SpaceBetween size="xl">
<FormField label="Settings">
<Checkbox checked={isDisabled} onChange={() => setUrlParams({ isDisabled: !isDisabled })}>
Disabled
</Checkbox>
<Checkbox checked={isReadOnly} onChange={() => setUrlParams({ isReadOnly: !isReadOnly })}>
Read-only
</Checkbox>
<Checkbox checked={isInvalid} onChange={() => setUrlParams({ isInvalid: !isInvalid })}>
Invalid
</Checkbox>
<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>
<AppLayout
ariaLabels={labels}
content={
<div style={{ padding: 10 }}>
<h1>PromptInput demo</h1>
<SpaceBetween size="xl">
<FormField label="Settings">
<Checkbox checked={isDisabled} onChange={() => setUrlParams({ isDisabled: !isDisabled })}>
Disabled
</Checkbox>
<Checkbox checked={isReadOnly} onChange={() => setUrlParams({ isReadOnly: !isReadOnly })}>
Read-only
</Checkbox>
<Checkbox checked={isInvalid} onChange={() => setUrlParams({ isInvalid: !isInvalid })}>
Invalid
</Checkbox>
<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 id="focus-button" onClick={() => ref.current?.focus()}>
Focus component
</button>
<button onClick={() => ref.current?.select()}>Select all text</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 || 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}
</>
}
label={<span>User prompt</span>}
i18nStrings={{ errorIconAriaLabel: 'Error' }}
>
<PromptInput
ariaLabel="Chat input"
actionButtonIconName="send"
actionButtonAriaLabel="Submit prompt"
value={textareaValue}
onChange={(event: any) => setTextareaValue(event.detail.value)}
onAction={event => window.alert(`Submitted the following: ${event.detail.value}`)}
placeholder="Ask a question"
maxRows={4}
disabled={isDisabled}
readOnly={isReadOnly}
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 />
</ColumnLayout>
</SpaceBetween>
</div>
<ColumnLayout columns={2}>
<FormField
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}
</>
}
label={<span>User prompt</span>}
i18nStrings={{ errorIconAriaLabel: 'Error' }}
>
<PromptInput
data-testid="prompt-input"
ariaLabel="Chat input"
actionButtonIconName="send"
actionButtonAriaLabel="Submit prompt"
value={textareaValue}
onChange={(event: any) => setTextareaValue(event.detail.value)}
onAction={event => window.alert(`Submitted the following: ${event.detail.value}`)}
placeholder="Ask a question"
maxRows={4}
disabled={isDisabled}
readOnly={isReadOnly}
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 />
</ColumnLayout>
</SpaceBetween>
</div>
}
splitPanel={
<SplitPanel
header="Split panel header"
i18nStrings={{
preferencesTitle: 'Preferences',
preferencesPositionLabel: 'Split panel position',
preferencesPositionDescription: 'Choose the default split panel position for the service.',
preferencesPositionSide: 'Side',
preferencesPositionBottom: 'Bottom',
preferencesConfirm: 'Confirm',
preferencesCancel: 'Cancel',
closeButtonAriaLabel: 'Close panel',
openButtonAriaLabel: 'Open panel',
resizeHandleAriaLabel: 'Slider',
}}
>
<PromptInput
data-testid="Prompt-input-in-split-panel"
value={valueInSplitPanel}
onChange={event => setValueInSplitPanel(event.detail.value)}
/>
</SplitPanel>
}
/>
);
}
18 changes: 13 additions & 5 deletions src/prompt-input/__integ__/prompt-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';

import createWrapper from '../../../lib/components/test-utils/selectors/index.js';

const promptInputWrapper = createWrapper().findPromptInput();
const getPromptInputWrapper = (testid = 'prompt-input') => createWrapper().findPromptInput(`[data-testid="${testid}"]`);

class PromptInputPage extends BasePageObject {
async getPromptInputHeight() {
const { height } = await this.getBoundingBox(promptInputWrapper.toSelector());
async getPromptInputHeight(testid = 'prompt-input') {
const { height } = await this.getBoundingBox(getPromptInputWrapper(testid).toSelector());
return height;
}
}
Expand All @@ -18,7 +18,7 @@ const setupTest = (testFn: (page: PromptInputPage) => Promise<void>) => {
return useBrowser(async browser => {
const page = new PromptInputPage(browser);
await browser.url(`#/light/prompt-input/simple/?isReadOnly=true`);
await page.waitForVisible(promptInputWrapper.toSelector());
await page.waitForVisible(getPromptInputWrapper().toSelector());
await testFn(page);
});
};
Expand All @@ -37,7 +37,15 @@ describe('Prompt input', () => {
setupTest(async page => {
await page.click('#focus-button');
await page.keys('Tab');
await expect(page.isFocused(promptInputWrapper.find('button').toSelector())).resolves.toBe(true);
await expect(page.isFocused(getPromptInputWrapper().find('button').toSelector())).resolves.toBe(true);
})
);

test(
'Should has one row height in Split Panel',
setupTest(async page => {
await page.click(createWrapper().findAppLayout().findSplitPanelOpenButton().toSelector());
await expect(page.getPromptInputHeight('Prompt-input-in-split-panel')).resolves.toEqual(32);
})
);
});
3 changes: 2 additions & 1 deletion src/prompt-input/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ const InternalPromptInput = React.forwardRef(
textareaRef.current.style.height = 'auto';
const maxRowsHeight = `calc(${maxRows <= 0 ? 3 : maxRows} * (${LINE_HEIGHT} + ${PADDING} / 2) + ${PADDING})`;
const scrollHeight = `calc(${textareaRef.current.scrollHeight}px)`;
textareaRef.current.style.height = `min(${scrollHeight}, ${maxRowsHeight})`;
const minTextareaHeight = `calc(${LINE_HEIGHT} + ${tokens.spaceScaledXxs} * 2)`; // the min height of Textarea with 1 row
textareaRef.current.style.height = `min(max(${scrollHeight}, ${minTextareaHeight}), ${maxRowsHeight})`;
}
}, [maxRows, LINE_HEIGHT, PADDING]);

Expand Down

0 comments on commit 8f5431a

Please sign in to comment.