Skip to content

fix(chat): Adjust scroll bar positioning #1861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 24, 2025
Merged
22 changes: 14 additions & 8 deletions js/chat/chat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ shiny-chat-container {
--shiny-chat-user-message-bg: RGBA(var(--bs-primary-rgb, 0, 123, 194), 0.06);
--_chat-container-padding: 0.25rem;

display: flex;
flex-direction: column;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
margin: 0 auto;
gap: 1rem;
overflow: auto;
gap: 0;
padding: var(--_chat-container-padding);
padding-bottom: 0; // Bottom padding is on input element

Expand Down Expand Up @@ -54,6 +54,13 @@ shiny-chat-messages {
display: flex;
flex-direction: column;
gap: 2rem;
overflow: auto;
margin-bottom: 1rem;

// Make space for the scroll bar
--_scroll-margin: 1rem;
padding-right: var(--_scroll-margin);
margin-right: calc(-1 * var(--_scroll-margin));
}

shiny-chat-message {
Expand Down Expand Up @@ -96,13 +103,12 @@ shiny-chat-message {
}

shiny-chat-input {
--_input-padding-top: 1rem;
--_input-padding-top: 0;
--_input-padding-bottom: var(--_chat-container-padding, 0.25rem);

margin-top: auto;
margin-top: calc(-1 * var(--_input-padding-top));
position: sticky;
bottom: 0;
background: linear-gradient(to bottom, transparent, var(--bs-body-bg, white) calc(var(--_input-padding-top) - var(--_input-padding-bottom)));
bottom: calc(-1 * var(--_input-padding-bottom));
padding-block: var(--_input-padding-top) var(--_input-padding-bottom);

textarea {
Expand Down
36 changes: 36 additions & 0 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const CHAT_MESSAGE_TAG = "shiny-chat-message";
const CHAT_USER_MESSAGE_TAG = "shiny-user-message";
const CHAT_MESSAGES_TAG = "shiny-chat-messages";
const CHAT_INPUT_TAG = "shiny-chat-input";
const CHAT_INPUT_SENTINEL_TAG = "shiny-chat-input-sentinel";
const CHAT_CONTAINER_TAG = "shiny-chat-container";

const ICONS = {
Expand Down Expand Up @@ -262,6 +263,8 @@ class ChatInput extends LightElement {
}

class ChatContainer extends LightElement {
inputSentinel?: HTMLElement;
inputSentinelObserver?: IntersectionObserver;

private get input(): ChatInput {
return this.querySelector(CHAT_INPUT_TAG) as ChatInput;
Expand All @@ -280,6 +283,36 @@ class ChatContainer extends LightElement {
return html``;
}

connectedCallback(): void {
super.connectedCallback();

// We use a sentinel element that we place just above the shiny-chat-input. When it
// moves off-screen we know that the text area input is now floating, add shadow.
let sentinel = this.querySelector<HTMLElement>(CHAT_INPUT_SENTINEL_TAG);
if (!sentinel) {
sentinel = createElement(CHAT_INPUT_SENTINEL_TAG, {
style: "width: 100%; height: 0",
}) as HTMLElement;
this.input.insertAdjacentElement("afterend", sentinel);
}

this.inputSentinel = sentinel;
this.inputSentinelObserver = new IntersectionObserver(
(entries) => {
const inputTextarea = this.input.querySelector("textarea");
if (!inputTextarea) return;
const addShadow = entries[0]?.intersectionRatio === 0;
inputTextarea.classList.toggle("shadow", addShadow);
},
{
threshold: [0, 1],
rootMargin: "0px",
}
);

this.inputSentinelObserver.observe(this.inputSentinel);
}

firstUpdated(): void {
// Don't attach event listeners until child elements are rendered
if (!this.messages) return;
Expand All @@ -306,6 +339,9 @@ class ChatContainer extends LightElement {
disconnectedCallback(): void {
super.disconnectedCallback();

this.inputSentinelObserver?.disconnect();
this.inputSentinel?.remove();

this.removeEventListener("shiny-chat-input-sent", this.#onInputSent);
this.removeEventListener("shiny-chat-append-message", this.#onAppend);
this.removeEventListener(
Expand Down
2 changes: 1 addition & 1 deletion shiny/www/py-shiny/chat/chat.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions shiny/www/py-shiny/chat/chat.css.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading