Skip to content

Commit 6f543ef

Browse files
feat(ChatInput): widen the composer and tighten the feedback strip
1 parent 7d9beda commit 6f543ef

5 files changed

Lines changed: 134 additions & 78 deletions

File tree

packages/blade/src/components/ChatInput/ChatInput.web.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,13 @@ const _ChatInput: React.ForwardRefRenderFunction<BladeElementRef, ChatInputProps
431431
return (
432432
<Frame
433433
position="relative"
434+
/*
435+
* The composer is a writing surface, not a field. Below about this width a prompt wraps
436+
* after a handful of words and the feedback strip — a question, four faces and a submit on
437+
* one line — starts folding onto a second row. Placed before the spreads below so a
438+
* consumer's own styled props still win.
439+
*/
440+
minWidth="700px"
434441
{...frameProps}
435442
{...metaAttribute({ name: MetaConstants.ChatInput, testID })}
436443
{...getStyledProps(rest)}

packages/blade/src/components/ChatInput/ChatInputFeedback.web.tsx

Lines changed: 104 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import styled from 'styled-components';
33
import type { ChatInputFeedbackProps } from './types';
44
import BaseBox from '~components/Box/BaseBox';
55
import { ChatFeedback } from '~components/ChatFeedback';
6+
import { chatFeedbackMoodButtonSize } from '~components/ChatFeedback/chatFeedbackTokens';
67
import type { ChatFeedbackControls } from '~components/ChatFeedback';
78
import { Move } from '~components/Move';
8-
import { useTheme } from '~components/BladeProvider';
9-
import { makeSpace } from '~utils';
9+
import { makeSpace, makeSize, castWebType } from '~utils';
1010

1111
/**
1212
* An even 4px inset.
@@ -17,13 +17,67 @@ import { makeSpace } from '~utils';
1717
* inside it. The mood buttons are 32px tall and already give the row its height, so anything more
1818
* than this is air.
1919
*/
20+
/**
21+
* An even 4px inset.
22+
*
23+
* Deliberately *not* aligned with the composer's own 16px content padding: the prompt sits closer
24+
* to the edge of the surface than the placeholder below it does, which is what stops the two rows
25+
* reading as one list and keeps the prompt feeling like a header on the card rather than a line
26+
* inside it. The mood buttons already give the row its height, so anything more than this is air.
27+
*/
2028
const StripPadding = styled(BaseBox)(({ theme }) => ({
21-
padding: makeSpace(theme.spacing[2]),
29+
/*
30+
* Horizontal only. The surface around this already contributes 8px above and below, and the
31+
* strip adding its own on top of that made the row sit lower in the container than the composer
32+
* sits in it — the prompt read as floating rather than as a header on the card.
33+
*
34+
* The row's height is set by the mood buttons regardless, so there is nothing here for vertical
35+
* padding to protect.
36+
*/
37+
paddingTop: makeSpace(theme.spacing[0]),
38+
paddingBottom: makeSpace(theme.spacing[0]),
2239
// A touch more on the left, so the question clears the surface's rounded corner rather than
2340
// sitting tight against it. The right stays at 4px — the submit control needs no such relief.
2441
paddingLeft: makeSpace(theme.spacing[3]),
42+
paddingRight: makeSpace(theme.spacing[2]),
2543
}));
2644

45+
/**
46+
* Closes the frame around the composer, rather than letting the strip's height vanish with it.
47+
*
48+
* Ported from the prototype, where the wrapper's own box collapses while its contents fade. `Move`
49+
* fades and slides the strip but leaves the space it occupied at full height until the instant it
50+
* unmounts, so the composer held still through the whole animation and then snapped up 52px at the
51+
* end of it. Collapsing the height alongside the fade means the surface recedes with its contents.
52+
*
53+
* It sits *outside* `Move` on purpose. `BaseMotionEntryExit` does not render its child — it takes
54+
* the child's type and props and re-renders it as a motion element — and a transient prop does not
55+
* survive that round trip, so anything driven off `isVisible` has to own its own element.
56+
*/
57+
const StripCollapse = styled.div<{ $isVisible: boolean }>(({ theme, $isVisible }) => {
58+
/*
59+
* The strip's open height, named exactly rather than guessed.
60+
*
61+
* Every step is held to the mood row's height, so this is that row plus the inset above — which
62+
* is what lets a `max-height` transition have a target at all, since `none` cannot animate.
63+
*/
64+
const openHeight = chatFeedbackMoodButtonSize + theme.spacing[2] * 2;
65+
66+
/*
67+
* Locked to `Move`'s own timings: in on `xmoderate`/`entrance`, out on the faster `quick`/`exit`.
68+
* A collapse that outlasted the fade would be cut off part-closed by the unmount, and the
69+
* composer would finish the journey in a single jump — the exact thing this removes.
70+
*/
71+
const duration = $isVisible ? theme.motion.duration.xmoderate : theme.motion.duration.quick;
72+
const ease = castWebType($isVisible ? theme.motion.easing.entrance : theme.motion.easing.exit);
73+
74+
return {
75+
overflow: 'hidden',
76+
maxHeight: $isVisible ? makeSize(openHeight) : '0px',
77+
transition: `max-height ${duration}ms ${ease}`,
78+
};
79+
});
80+
2781
/**
2882
* The feedback prompt attached to the top of a `ChatInput`.
2983
*
@@ -45,8 +99,13 @@ const StripPadding = styled(BaseBox)(({ theme }) => ({
4599
*
46100
* The strip sits directly on top of the composer, so a step one pixel taller pushes the whole
47101
* composer down — at the exact moment the merchant is reading the strip. The content is held to
48-
* the height of the mood row (`spacing[8]`, the height of Blade's own mood button) so every step
49-
* occupies the same space and the swap changes *what* is on the strip and nothing else.
102+
* the height of the mood row, the tallest of the three steps, so every step occupies the same
103+
* space and the swap changes *what* is on the strip and nothing else.
104+
*
105+
* That height is imported rather than restated. It was a local `spacing[8]` until the mood glyph
106+
* grew and left it behind: the mood step became 44px while the tags step stayed at 38, and the
107+
* composer jumped 12px on every transition. Reading it from the same constant the button is
108+
* built from is what stops that from happening a second time.
50109
*/
51110
const ChatInputFeedback = ({
52111
isVisible = true,
@@ -70,8 +129,6 @@ const ChatInputFeedback = ({
70129
/** Lets the composer drive this flow — submit it, and release the tag on the way out. */
71130
controlsRef?: React.MutableRefObject<ChatFeedbackControls | null>;
72131
}): React.ReactElement => {
73-
const { theme } = useTheme();
74-
75132
/*
76133
* A fresh `Move` per showing.
77134
*
@@ -87,44 +144,50 @@ const ChatInputFeedback = ({
87144
wasVisible.current = isVisible;
88145

89146
return (
90-
<Move key={generation.current} isVisible={isVisible} shouldUnmountWhenHidden type="inout">
91-
{/*
147+
<StripCollapse $isVisible={isVisible}>
148+
<Move key={generation.current} isVisible={isVisible} shouldUnmountWhenHidden type="inout">
149+
{/*
92150
Claims a layer of its own. The error slot below sits at `zIndex: 0` and the input card at
93151
`zIndex: 1`; the strip joins the card rather than the slot, which is what keeps its
94152
controls reachable while an error is mounted but not shown.
95153
*/}
96-
<StripPadding position="relative" zIndex={1}>
97-
<BaseBox display="flex" alignItems="center" minHeight={makeSpace(theme.spacing[8])}>
98-
<ChatFeedback
99-
question={question}
100-
feedbackIcons={feedbackIcons}
101-
moodConfig={moodConfig}
102-
isDisabled={isDisabled}
103-
onMoodSelect={onMoodSelect}
104-
onTagsChange={onTagsChange}
105-
isSubmitHidden={isSubmitHidden}
106-
controlsRef={controlsRef}
107-
onSubmit={(payload) =>
108-
onSubmit?.({
109-
/*
110-
* A blank composer is not a comment. Trimmed rather than passed through, so
111-
* whitespace does not arrive as feedback someone has to read.
112-
*/
113-
...payload,
114-
comment: comment?.trim() ? comment.trim() : payload.comment,
115-
})
116-
}
117-
onDismiss={onDismiss}
118-
/*
119-
* The strip spans the composer, so each step spreads to the full width and the
120-
* trailing control lands above the send button. This is the case `isFullWidth` was
121-
* added for; the floating bar is the one that opts out.
122-
*/
123-
isFullWidth
124-
/>
125-
</BaseBox>
126-
</StripPadding>
127-
</Move>
154+
<StripPadding position="relative" zIndex={1}>
155+
<BaseBox
156+
display="flex"
157+
alignItems="center"
158+
minHeight={makeSize(chatFeedbackMoodButtonSize)}
159+
>
160+
<ChatFeedback
161+
question={question}
162+
feedbackIcons={feedbackIcons}
163+
moodConfig={moodConfig}
164+
isDisabled={isDisabled}
165+
onMoodSelect={onMoodSelect}
166+
onTagsChange={onTagsChange}
167+
isSubmitHidden={isSubmitHidden}
168+
controlsRef={controlsRef}
169+
onSubmit={(payload) =>
170+
onSubmit?.({
171+
/*
172+
* A blank composer is not a comment. Trimmed rather than passed through, so
173+
* whitespace does not arrive as feedback someone has to read.
174+
*/
175+
...payload,
176+
comment: comment?.trim() ? comment.trim() : payload.comment,
177+
})
178+
}
179+
onDismiss={onDismiss}
180+
/*
181+
* The strip spans the composer, so each step spreads to the full width and the
182+
* trailing control lands above the send button. This is the case `isFullWidth` was
183+
* added for; the floating bar is the one that opts out.
184+
*/
185+
isFullWidth
186+
/>
187+
</BaseBox>
188+
</StripPadding>
189+
</Move>
190+
</StripCollapse>
128191
);
129192
};
130193

packages/blade/src/components/ChatInput/__tests__/__snapshots__/ChatInput.ssr.test.tsx.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`<ChatInput /> should render ChatInput 1`] = `"<div id="root"><div data-blade-component="chat-input" class="BaseBox-bksIuc emXDue"><input type="file" multiple="" style="display:none" aria-hidden="true"/><div data-blade-component="base-box" class="BaseBox-bksIuc dhbiRH"><div data-blade-component="base-box" class="BaseBox-bksIuc"><div data-blade-component="base-box" class="BaseBox-bksIuc lhbwwB"><div class="BaseBox-bksIuc BaseInput__FocusRingWrapper-sc-177qd3e-0 kQXlnf dJZnYg focus-ring-wrapper" elevation="highRaised" data-blade-component="base-box"><div class="BaseBox-bksIuc AnimatedBaseInputWrapperweb__StyledBaseInputWrapper-e1vobd-0 AnimatedBaseInputWrapperweb__StyledAnimatedBaseInputWrapper-e1vobd-1 ikooGF kzXtQx __blade-base-input-wrapper" data-blade-component="base-box"><div data-blade-component="base-box" class="BaseBox-bksIuc lbOIzR"><div data-blade-component="base-box" class="BaseBox-bksIuc eCIwtI"><textarea type="text" rows="2" id="chat-input-undefined-input-undefined" placeholder="Ask a question..." data-blade-component="styled-base-input" aria-required="false" aria-disabled="false" aria-invalid="false" aria-describedby="" aria-label="Chat input" class="StyledBaseInputweb__StyledBaseNativeInput-hsusrk-0 ideLwR"></textarea></div><div data-blade-component="base-box" class="BaseBox-bksIuc jjHIMt"><div data-blade-component="base-box" class="BaseBox-bksIuc bZHJre"><button data-blade-component="link" type="button" class="StyledBaseLinkweb__StyledLink-sc-1yj1z8h-0 fbBTol" role="button" aria-disabled="false"><span class="BaseBox-bksIuc hbpipW content-container" data-blade-component="base-box"><span data-blade-component="base-box" class="BaseBox-bksIuc dmzGFD"><svg aria-hidden="true" data-blade-component="icon" height="12px" viewBox="0 0 24 24" width="12px" fill="none" class="Svgweb__StyledSvg-vcmjs8-0"><path d="M13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44772 11 5V11H5C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13H11V19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V13H19C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11H13V5Z" fill="hsla(0, 0%, 2%, 1)" data-blade-component="svg-path"></path></svg></span><span class="StyledBaseText-foUshD cglRbf" data-blade-component="base-text">Upload file</span></span></button></div><div data-blade-component="base-box" class="BaseBox-bksIuc"><button disabled="" type="button" data-blade-component="button" aria-label="Submit" role="button" class="StyledBaseButtonweb__StyledBaseButton-sc-26bt38-0 gxCno"><div data-blade-component="base-box" class="BaseBox-bksIuc AnimatedButtonContentweb__AnimatedButtonContent-sc-1fkx0t6-0 gYMLpr"><div data-blade-component="base-box" class="BaseBox-bksIuc BaseButton__ButtonContent-zf1huq-0 jbTreI cdAugm"><div data-blade-component="base-box" class="BaseBox-bksIuc juovFm"><svg aria-hidden="true" data-blade-component="icon" height="16px" viewBox="0 0 24 24" width="16px" fill="none" class="Svgweb__StyledSvg-vcmjs8-0"><path d="M12.7071 3.29289C12.3166 2.90237 11.6834 2.90237 11.2929 3.29289L5.29289 9.29289C4.90237 9.68342 4.90237 10.3166 5.29289 10.7071C5.68342 11.0976 6.31658 11.0976 6.70711 10.7071L11 6.41421V20C11 20.5523 11.4477 21 12 21C12.5523 21 13 20.5523 13 20V6.41421L17.2929 10.7071C17.6834 11.0976 18.3166 11.0976 18.7071 10.7071C19.0976 10.3166 19.0976 9.68342 18.7071 9.29289L12.7071 3.29289Z" fill="hsla(218, 89%, 51%, 0.32)" data-blade-component="svg-path"></path></svg></div></div></div></button></div></div></div></div></div></div></div></div><div pointer-events="none" data-blade-component="base-box" class="BaseBox-bksIuc bnKjZN"><div role="alert" style="opacity:0" class="BaseBox-bksIuc gSAviZ BaseMotionweb__StyledDiv-gsyvko-0 hrLUhl" data-blade-component="base-box"><svg aria-hidden="true" data-blade-component="icon" height="12px" viewBox="0 0 24 24" width="12px" fill="none" class="Svgweb__StyledSvg-vcmjs8-0"><path d="M12 11C12.5523 11 13 11.4477 13 12V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C11 11.4477 11.4477 11 12 11Z" fill="hsla(4, 85%, 44%, 1)" data-blade-component="svg-path"></path><path d="M12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9H12.01C12.5623 9 13.01 8.55228 13.01 8C13.01 7.44772 12.5623 7 12.01 7H12Z" fill="hsla(4, 85%, 44%, 1)" data-blade-component="svg-path"></path><path d="M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3Z" clip-rule="evenodd" fill="hsla(4, 85%, 44%, 1)" fill-rule="evenodd" data-blade-component="svg-path"></path></svg><p letter-spacing="50" class="StyledBaseText-foUshD jFszoz" data-blade-component="text"></p></div></div></div></div>"`;
3+
exports[`<ChatInput /> should render ChatInput 1`] = `"<div id="root"><div data-blade-component="chat-input" class="BaseBox-bksIuc jYaqTx"><input type="file" multiple="" style="display:none" aria-hidden="true"/><div data-blade-component="base-box" class="BaseBox-bksIuc dhbiRH"><div data-blade-component="base-box" class="BaseBox-bksIuc"><div data-blade-component="base-box" class="BaseBox-bksIuc lhbwwB"><div class="BaseBox-bksIuc BaseInput__FocusRingWrapper-sc-177qd3e-0 kQXlnf dJZnYg focus-ring-wrapper" elevation="highRaised" data-blade-component="base-box"><div class="BaseBox-bksIuc AnimatedBaseInputWrapperweb__StyledBaseInputWrapper-e1vobd-0 AnimatedBaseInputWrapperweb__StyledAnimatedBaseInputWrapper-e1vobd-1 ikooGF kzXtQx __blade-base-input-wrapper" data-blade-component="base-box"><div data-blade-component="base-box" class="BaseBox-bksIuc lbOIzR"><div data-blade-component="base-box" class="BaseBox-bksIuc eCIwtI"><textarea type="text" rows="2" id="chat-input-undefined-input-undefined" placeholder="Ask a question..." data-blade-component="styled-base-input" aria-required="false" aria-disabled="false" aria-invalid="false" aria-describedby="" aria-label="Chat input" class="StyledBaseInputweb__StyledBaseNativeInput-hsusrk-0 ideLwR"></textarea></div><div data-blade-component="base-box" class="BaseBox-bksIuc jjHIMt"><div data-blade-component="base-box" class="BaseBox-bksIuc bZHJre"><button data-blade-component="link" type="button" class="StyledBaseLinkweb__StyledLink-sc-1yj1z8h-0 fbBTol" role="button" aria-disabled="false"><span class="BaseBox-bksIuc hbpipW content-container" data-blade-component="base-box"><span data-blade-component="base-box" class="BaseBox-bksIuc dmzGFD"><svg aria-hidden="true" data-blade-component="icon" height="12px" viewBox="0 0 24 24" width="12px" fill="none" class="Svgweb__StyledSvg-vcmjs8-0"><path d="M13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44772 11 5V11H5C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13H11V19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V13H19C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11H13V5Z" fill="hsla(0, 0%, 2%, 1)" data-blade-component="svg-path"></path></svg></span><span class="StyledBaseText-foUshD cglRbf" data-blade-component="base-text">Upload file</span></span></button></div><div data-blade-component="base-box" class="BaseBox-bksIuc"><button disabled="" type="button" data-blade-component="button" aria-label="Submit" role="button" class="StyledBaseButtonweb__StyledBaseButton-sc-26bt38-0 gxCno"><div data-blade-component="base-box" class="BaseBox-bksIuc AnimatedButtonContentweb__AnimatedButtonContent-sc-1fkx0t6-0 gYMLpr"><div data-blade-component="base-box" class="BaseBox-bksIuc BaseButton__ButtonContent-zf1huq-0 jbTreI cdAugm"><div data-blade-component="base-box" class="BaseBox-bksIuc juovFm"><svg aria-hidden="true" data-blade-component="icon" height="16px" viewBox="0 0 24 24" width="16px" fill="none" class="Svgweb__StyledSvg-vcmjs8-0"><path d="M12.7071 3.29289C12.3166 2.90237 11.6834 2.90237 11.2929 3.29289L5.29289 9.29289C4.90237 9.68342 4.90237 10.3166 5.29289 10.7071C5.68342 11.0976 6.31658 11.0976 6.70711 10.7071L11 6.41421V20C11 20.5523 11.4477 21 12 21C12.5523 21 13 20.5523 13 20V6.41421L17.2929 10.7071C17.6834 11.0976 18.3166 11.0976 18.7071 10.7071C19.0976 10.3166 19.0976 9.68342 18.7071 9.29289L12.7071 3.29289Z" fill="hsla(218, 89%, 51%, 0.32)" data-blade-component="svg-path"></path></svg></div></div></div></button></div></div></div></div></div></div></div></div><div pointer-events="none" data-blade-component="base-box" class="BaseBox-bksIuc bnKjZN"><div role="alert" style="opacity:0" class="BaseBox-bksIuc gSAviZ BaseMotionweb__StyledDiv-gsyvko-0 hrLUhl" data-blade-component="base-box"><svg aria-hidden="true" data-blade-component="icon" height="12px" viewBox="0 0 24 24" width="12px" fill="none" class="Svgweb__StyledSvg-vcmjs8-0"><path d="M12 11C12.5523 11 13 11.4477 13 12V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C11 11.4477 11.4477 11 12 11Z" fill="hsla(4, 85%, 44%, 1)" data-blade-component="svg-path"></path><path d="M12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9H12.01C12.5623 9 13.01 8.55228 13.01 8C13.01 7.44772 12.5623 7 12.01 7H12Z" fill="hsla(4, 85%, 44%, 1)" data-blade-component="svg-path"></path><path d="M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3Z" clip-rule="evenodd" fill="hsla(4, 85%, 44%, 1)" fill-rule="evenodd" data-blade-component="svg-path"></path></svg><p letter-spacing="50" class="StyledBaseText-foUshD jFszoz" data-blade-component="text"></p></div></div></div></div>"`;
44

55
exports[`<ChatInput /> should render ChatInput 2`] = `
66
.c0.c0.c0.c0.c0 {
77
position: relative;
8+
min-width: 700px;
89
}
910
1011
.c1.c1.c1.c1.c1 {

packages/blade/src/components/ChatInput/__tests__/__snapshots__/ChatInput.web.test.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
exports[`<ChatInput /> should render ChatInput 1`] = `
44
.c0.c0.c0.c0.c0 {
55
position: relative;
6+
min-width: 700px;
67
}
78
89
.c1.c1.c1.c1.c1 {

0 commit comments

Comments
 (0)