Skip to content

Commit 1e674f4

Browse files
committed
fix(chat): Improve content/input overlap
* Avoid overlap as much as possible * Fix scroll bar positioning * When input is floating, we add a `shadow` class Fixes #1855
1 parent ab2a338 commit 1e674f4

File tree

2 files changed

+54
-8
lines changed

2 files changed

+54
-8
lines changed

js/chat/chat.scss

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ shiny-chat-container {
33
--shiny-chat-user-message-bg: RGBA(var(--bs-primary-rgb, 0, 123, 194), 0.06);
44
--_chat-container-padding: 0.25rem;
55

6-
display: flex;
7-
flex-direction: column;
6+
display: grid;
7+
grid-template-columns: 1fr;
8+
grid-template-rows: 1fr auto;
89
margin: 0 auto;
9-
gap: 1rem;
10-
overflow: auto;
10+
gap: 0.625rem;
1111
padding: var(--_chat-container-padding);
1212
padding-bottom: 0; // Bottom padding is on input element
13+
overflow: clip;
1314

1415
p:last-child {
1516
margin-bottom: 0;
@@ -54,6 +55,12 @@ shiny-chat-messages {
5455
display: flex;
5556
flex-direction: column;
5657
gap: 2rem;
58+
overflow: auto;
59+
60+
// Make space for the scroll bar
61+
--_scroll-margin: 1rem;
62+
padding-right: var(--_scroll-margin);
63+
margin-right: calc(-1 * var(--_scroll-margin));
5764
}
5865

5966
shiny-chat-message {
@@ -96,13 +103,12 @@ shiny-chat-message {
96103
}
97104

98105
shiny-chat-input {
99-
--_input-padding-top: 1rem;
106+
--_input-padding-top: 0;
100107
--_input-padding-bottom: var(--_chat-container-padding, 0.25rem);
101108

102-
margin-top: auto;
109+
margin-top: calc(-1 * var(--_input-padding-top));
103110
position: sticky;
104-
bottom: 0;
105-
background: linear-gradient(to bottom, transparent, var(--bs-body-bg, white) calc(var(--_input-padding-top) - var(--_input-padding-bottom)));
111+
bottom: calc(-1 * var(--_input-padding-bottom));
106112
padding-block: var(--_input-padding-top) var(--_input-padding-bottom);
107113

108114
textarea {
@@ -133,6 +139,12 @@ shiny-chat-input {
133139
}
134140
}
135141

142+
shiny-chat-input-sentinel {
143+
// sentinel element with 0 height that lets us know when input element is floating
144+
height: 0;
145+
width: 100%;
146+
}
147+
136148
/*
137149
Disable the page-level pulse when the chat input is disabled
138150
(i.e., when a response is being generated and brought into the chat)

js/chat/chat.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const CHAT_MESSAGE_TAG = "shiny-chat-message";
4646
const CHAT_USER_MESSAGE_TAG = "shiny-user-message";
4747
const CHAT_MESSAGES_TAG = "shiny-chat-messages";
4848
const CHAT_INPUT_TAG = "shiny-chat-input";
49+
const CHAT_INPUT_SENTINEL_TAG = "shiny-chat-input-sentinel";
4950
const CHAT_CONTAINER_TAG = "shiny-chat-container";
5051

5152
const ICONS = {
@@ -262,6 +263,8 @@ class ChatInput extends LightElement {
262263
}
263264

264265
class ChatContainer extends LightElement {
266+
inputSentinel?: HTMLElement;
267+
inputSentinelObserver?: IntersectionObserver;
265268

266269
private get input(): ChatInput {
267270
return this.querySelector(CHAT_INPUT_TAG) as ChatInput;
@@ -280,6 +283,34 @@ class ChatContainer extends LightElement {
280283
return html``;
281284
}
282285

286+
connectedCallback(): void {
287+
super.connectedCallback();
288+
289+
// We use a sentinel element that we place just above the shiny-chat-input. When it
290+
// moves off-screen we know that the text area input is now floating, add shadow.
291+
let sentinel = this.querySelector<HTMLElement>(CHAT_INPUT_SENTINEL_TAG);
292+
if (!sentinel) {
293+
sentinel = createElement(CHAT_INPUT_SENTINEL_TAG, {}) as HTMLElement;
294+
this.input.insertAdjacentElement("beforebegin", sentinel);
295+
}
296+
297+
this.inputSentinel = sentinel;
298+
this.inputSentinelObserver = new IntersectionObserver(
299+
(entries) => {
300+
const inputTextarea = this.input.querySelector("textarea");
301+
if (!inputTextarea) return;
302+
const addShadow = entries[0]?.intersectionRatio === 0;
303+
inputTextarea.classList.toggle("shadow", addShadow);
304+
},
305+
{
306+
threshold: [0, 1],
307+
rootMargin: "0px",
308+
}
309+
);
310+
311+
this.inputSentinelObserver.observe(this.inputSentinel);
312+
}
313+
283314
firstUpdated(): void {
284315
// Don't attach event listeners until child elements are rendered
285316
if (!this.messages) return;
@@ -306,6 +337,9 @@ class ChatContainer extends LightElement {
306337
disconnectedCallback(): void {
307338
super.disconnectedCallback();
308339

340+
this.inputSentinelObserver?.disconnect();
341+
this.inputSentinel?.remove();
342+
309343
this.removeEventListener("shiny-chat-input-sent", this.#onInputSent);
310344
this.removeEventListener("shiny-chat-append-message", this.#onAppend);
311345
this.removeEventListener(

0 commit comments

Comments
 (0)