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
2 changes: 1 addition & 1 deletion .github/workflows/verify-js-built.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: |
if [[ `git status --porcelain` ]]; then
git diff
echo "Uncommitted changes found. Please commit any changes that result from 'npm run build'."
echo "Uncommitted changes found. Please commit any changes that result from 'npm ci && npm run build'."
exit 1
else
echo "No uncommitted changes found."
Expand Down
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
33 changes: 33 additions & 0 deletions js/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ class ChatInput extends LightElement {
}

class ChatContainer extends LightElement {
inputSentinelObserver?: IntersectionObserver;

private get input(): ChatInput {
return this.querySelector(CHAT_INPUT_TAG) as ChatInput;
Expand All @@ -280,6 +281,35 @@ 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>("div");
if (!sentinel) {
sentinel = createElement("div", {
style: "width: 100%; height: 0;",
}) as HTMLElement;
this.input.insertAdjacentElement("afterend", 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(sentinel);
}

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

this.inputSentinelObserver?.disconnect();
this.inputSentinelObserver = undefined;

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