Move the AutoHideScrollbar to shared components (#33777)

* First version of shared component

* Refactor as a functional component

* Add unit tests and more documentation

* Fix problem where wrappedRef was used by parent before assignment was performed.

* Use the shared component in app/web

* Clean up unused styling

* Added scrollbar-gutters as default styling in shared component

* Make sure the legacy mx_AutoHideScrollbar is set in app/web

* Updated snapshots

* Removing default style, scrollbar-gutter: stable;

* Updated snapshots

* useRef on wrapperRef to avoid loop in rendering

* scrollbar-width does not propagate

* Add AutoHideScrollbar to RoomListView

* Fix Prettier issue

* Updated snapshots

* Updated snapshot after merge

* Fix Sonar issue
This commit is contained in:
rbondesson
2026-06-11 09:08:24 +02:00
committed by GitHub
parent e9a89d9872
commit 8a21fdb127
39 changed files with 17217 additions and 16801 deletions
@@ -20,8 +20,6 @@ Please see LICENSE files in the repository root for full details.
.mx_EmojiPicker_body {
flex: 1;
overflow-y: scroll;
scrollbar-width: thin;
scrollbar-color: rgb(0, 0, 0, 0.2) transparent;
}
.mx_EmojiPicker_header {
@@ -173,6 +171,7 @@ Please see LICENSE files in the repository root for full details.
.mx_EmojiPicker_footer {
border-top: 1px solid $message-action-bar-border-color;
min-height: 72px;
max-height: 72px;
display: flex;
align-items: center;
@@ -1,70 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type HTMLAttributes, type JSX, type ReactNode, type WheelEvent } from "react";
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
JSX.IntrinsicElements[T] extends HTMLAttributes<object> ? DynamicElementProps<T> : DynamicElementProps<"div">;
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], "ref">>;
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<DynamicHtmlElementProps<T>, "onScroll"> & {
element: T;
className?: string;
onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties;
tabIndex?: number;
wrappedRef?: (ref: HTMLDivElement | null) => void;
children: ReactNode;
};
export default class AutoHideScrollbar<T extends keyof JSX.IntrinsicElements> extends React.Component<IProps<T>> {
public static defaultProps = {
element: "div" as keyof HTMLElementTagNameMap,
};
public readonly containerRef = React.createRef<HTMLDivElement>();
public componentDidMount(): void {
if (this.containerRef.current && this.props.onScroll) {
// Using the passive option to not block the main thread
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
}
this.props.wrappedRef?.(this.containerRef.current);
}
public componentWillUnmount(): void {
if (this.containerRef.current && this.props.onScroll) {
this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
}
this.props.wrappedRef?.(null);
}
public render(): React.ReactNode {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props;
return React.createElement(
element,
{
...otherProps,
ref: this.containerRef,
className: classNames("mx_AutoHideScrollbar", className),
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order by default.
tabIndex: tabIndex ?? -1,
},
children,
);
}
}
@@ -11,12 +11,12 @@ import React from "react";
import sanitizeHtml from "sanitize-html";
import classnames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../languageHandler";
import dis from "../../dispatcher/dispatcher";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { type ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions.ts";
@@ -121,6 +121,7 @@ export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
const isGuest = client ? client.isGuest() : true;
const className = this.props.className;
const classes = classnames(className, {
mx_AutoHideScrollbar: this.props.scrollbar,
[`${className}_guest`]: isGuest,
[`${className}_loggedIn`]: !!client,
});
@@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import { useContext, useState } from "react";
import { ChatSolidIcon, ExploreIcon, GroupIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { getHomePageUrl } from "../../utils/pages";
import { _t, _tDom } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
@@ -113,7 +113,7 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
}
return (
<AutoHideScrollbar className="mx_HomePage mx_HomePage_default" element="main">
<AutoHideScrollbar className="mx_AutoHideScrollbar mx_HomePage mx_HomePage_default" as="main">
<div className="mx_HomePage_default_wrapper">
{introSection}
<div className="mx_HomePage_default_buttons">
@@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, type JSX } from "react";
import React, { type JSX } from "react";
import { AutoHideScrollbar, type AutoHideScrollbarProps } from "@element-hq/web-shared-components";
import classNames from "classnames";
import AutoHideScrollbar, { type IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollbarProps<T>, "onWheel" | "element"> & {
@@ -32,7 +33,6 @@ export default class IndicatorScrollbar<T extends keyof JSX.IntrinsicElements> e
IProps<T>,
IState
> {
private autoHideScrollbar = createRef<AutoHideScrollbar<any>>();
private scrollElement?: HTMLDivElement;
private likelyTrackpadUser: boolean | null = null;
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
@@ -170,7 +170,7 @@ export default class IndicatorScrollbar<T extends keyof JSX.IntrinsicElements> e
public render(): React.ReactNode {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, className, ...otherProps } = this.props;
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
const rightIndicatorStyle = { right: this.state.rightIndicatorOffset };
@@ -184,7 +184,7 @@ export default class IndicatorScrollbar<T extends keyof JSX.IntrinsicElements> e
return (
<AutoHideScrollbar
{...otherProps}
ref={this.autoHideScrollbar}
className={classNames("mx_AutoHideScrollbar", className)}
wrappedRef={this.collectScroller}
onWheel={this.onMouseWheel}
>
@@ -329,7 +329,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
<IndicatorScrollbar
role="navigation"
aria-label={_t("a11y|recent_rooms")}
className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
className="mx_LeftPanel_breadcrumbsContainer"
verticalScrollsHorizontally={true}
>
<RoomBreadcrumbs />
@@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { createRef, type CSSProperties, type ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import SettingsStore from "../../settings/SettingsStore";
import Timer from "../../utils/Timer";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { SDKContext } from "../../contexts/SDKContext";
@@ -940,7 +940,7 @@ export default class ScrollPanel extends React.Component<IProps> {
<AutoHideScrollbar
wrappedRef={this.collectScroll}
onScroll={this.onScroll}
className={`mx_ScrollPanel ${this.props.className}`}
className={`mx_AutoHideScrollbar mx_ScrollPanel ${this.props.className}`}
style={this.props.style}
>
{this.props.fixedChildren}
@@ -10,9 +10,9 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX } from "react";
import classNames from "classnames";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../languageHandler";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { PosthogScreenTracker, type ScreenName } from "../../PosthogTrackers";
import { type NonEmptyArray } from "../../@types/common";
import { RovingAccessibleButton, RovingTabIndexProvider } from "../../accessibility/RovingTabIndex";
@@ -74,7 +74,9 @@ function TabPanel<T extends string>({ tab }: ITabPanelProps<T>): JSX.Element {
id={domIDForTabID(tab.id)}
aria-labelledby={`${domIDForTabID(tab.id)}_label`}
>
<AutoHideScrollbar className="mx_TabbedView_tabPanelContent">{tab.body}</AutoHideScrollbar>
<AutoHideScrollbar className="mx_AutoHideScrollbar mx_TabbedView_tabPanelContent">
{tab.body}
</AutoHideScrollbar>
</div>
);
}
@@ -13,6 +13,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { sleep } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { CheckIcon, ErrorIcon, RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t, _td } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
@@ -22,7 +23,6 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import { getDisplayAliasForRoom } from "../../../Rooms";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import DMRoomMap from "../../../utils/DMRoomMap";
import { calculateRoomVia } from "../../../utils/permalinks/Permalinks";
import StyledCheckbox from "../elements/StyledCheckbox";
@@ -142,7 +142,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
[cli, msc3946ProcessDynamicPredecessor],
);
const scrollRef = useRef<AutoHideScrollbar<"div">>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const [scrollState, setScrollState] = useState<IScrollState>({
// these are estimates which update as soon as it mounts
scrollTop: 0,
@@ -300,7 +300,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
}
const onScroll = (): void => {
const body = scrollRef.current?.containerRef.current;
const body = scrollRef.current;
if (!body) return;
setScrollState({
scrollTop: body.scrollTop,
@@ -309,6 +309,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
};
const wrappedRef = (body: HTMLDivElement | null): void => {
scrollRef.current = body;
if (!body) return;
setScrollState({
scrollTop: body.scrollTop,
@@ -329,10 +330,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
autoFocus={true}
/>
<AutoHideScrollbar
className="mx_AddExistingToSpace_content"
className="mx_AutoHideScrollbar mx_AddExistingToSpace_content"
onScroll={onScroll}
wrappedRef={wrappedRef}
ref={scrollRef}
>
{rooms.length > 0 && roomsRenderer
? roomsRenderer(rooms, selectedToAdd, roomsScrollState, onChange)
@@ -24,6 +24,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { CheckCircleIcon, CircleIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
@@ -34,7 +35,6 @@ import { avatarUrlForUser } from "../../../Avatar";
import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
@@ -384,7 +384,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
/>
)}
</RovingTabIndexContext.Consumer>
<AutoHideScrollbar className="mx_ForwardList_content">
<AutoHideScrollbar className="mx_AutoHideScrollbar mx_ForwardList_content">
{rooms.length > 0 ? (
<div className="mx_ForwardList_results">
<TruncatedList
@@ -10,6 +10,7 @@ import React, { useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { InfoSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import BaseDialog from "./BaseDialog";
@@ -17,7 +18,6 @@ import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { filterBoolean } from "../../../utils/arrays";
@@ -158,7 +158,7 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
onSearch={setQuery}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
<AutoHideScrollbar className="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content">
{filteredSpacesContainingRoom.length > 0 ? (
<div className="mx_ManageRestrictedJoinRuleDialog_section">
<h3>
@@ -10,10 +10,10 @@ Please see LICENSE files in the repository root for full details.
import React, { type Dispatch } from "react";
import { DATA_BY_CATEGORY, getEmojiFromUnicode, type Emoji as IEmoji } from "@matrix-org/emojibase-bindings";
import classNames from "classnames";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import * as recent from "../../../emojipicker/recent";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import Header from "./Header";
import Search from "./Search";
import Preview from "./Preview";
@@ -59,7 +59,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
private readonly categories: ICategory[];
private scrollRef = React.createRef<AutoHideScrollbar<"div">>();
private scrollElement: HTMLDivElement | null = null;
public constructor(props: IProps) {
super(props);
@@ -115,7 +115,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
}
private onScroll = (): void => {
const body = this.scrollRef.current?.containerRef.current;
const body = this.scrollElement;
if (!body) return;
this.setState({
scrollTop: body.scrollTop,
@@ -174,7 +174,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
};
private updateVisibility = (): void => {
const body = this.scrollRef.current?.containerRef.current;
const body = this.scrollElement;
if (!body) return;
const rect = body.getBoundingClientRect();
let firstVisibleFound = false;
@@ -213,9 +213,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
};
private scrollToCategory = (category: string): void => {
this.scrollRef.current?.containerRef.current
?.querySelector(`[data-category-id="${category}"]`)
?.scrollIntoView();
this.scrollElement?.querySelector(`[data-category-id="${category}"]`)?.scrollIntoView();
};
private onChangeFilter = (filter: string): void => {
@@ -293,9 +291,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
// Only select emoji if highlight is shown
if (!this.state.showHighlight) return;
const btn = this.scrollRef.current?.containerRef.current?.querySelector<HTMLButtonElement>(
'.mx_EmojiPicker_item_wrapper [tabindex="0"]',
);
const btn = this.scrollElement?.querySelector<HTMLButtonElement>('.mx_EmojiPicker_item_wrapper [tabindex="0"]');
btn?.click();
this.props.onFinished();
};
@@ -357,10 +353,12 @@ class EmojiPicker extends React.Component<IProps, IState> {
/>
<AutoHideScrollbar
id="mx_EmojiPicker_body"
className={classNames("mx_EmojiPicker_body", {
className={classNames("mx_AutoHideScrollbar mx_EmojiPicker_body", {
mx_EmojiPicker_body_showHighlight: this.state.showHighlight,
})}
ref={this.scrollRef}
wrappedRef={(ref) => {
this.scrollElement = ref;
}}
onScroll={this.onScroll}
>
{this.categories.map((category) => {
@@ -11,8 +11,8 @@ import classNames from "classnames";
import { IconButton, Text } from "@vector-im/compound-web";
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-left";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { _t } from "../../../languageHandler";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { backLabelForPhase } from "../../../stores/right-panel/RightPanelStorePhases";
@@ -100,7 +100,7 @@ const BaseCard: React.FC<IProps> = ({
}
if (!withoutScrollContainer) {
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
children = <AutoHideScrollbar className="mx_AutoHideScrollbar">{children}</AutoHideScrollbar>;
}
const shouldRenderHeader = header || !hideHeaderButtons;
@@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details.
import React, { type ReactNode } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import AppsDrawer from "./AppsDrawer";
import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { UIFeature } from "../../../settings/UIFeature";
import LegacyCallViewForRoom from "../voip/LegacyCallViewForRoom";
import { objectHasDiff } from "../../../utils/objects";
@@ -44,7 +44,7 @@ export default class AuxPanel extends React.Component<IProps> {
}
return (
<AutoHideScrollbar role="region" className="mx_AuxPanel">
<AutoHideScrollbar role="region" className="mx_AutoHideScrollbar mx_AuxPanel">
{this.props.children}
{appsDrawer}
{callView}
@@ -9,12 +9,12 @@ Please see LICENSE files in the repository root for full details.
import React, { type JSX, type PropsWithChildren } from "react";
import { type User } from "matrix-js-sdk/src/matrix";
import { Tooltip } from "@vector-im/compound-web";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import ReadReceiptMarker, { type IReadReceiptPosition } from "./ReadReceiptMarker";
import { type IReadReceiptProps } from "./EventTile";
import AccessibleButton from "../elements/AccessibleButton";
import MemberAvatar from "../avatars/MemberAvatar";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { formatDate } from "../../../DateUtils";
import { Action } from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher";
@@ -146,7 +146,7 @@ export function ReadReceiptGroup({
const buttonRect = button.current.getBoundingClientRect();
contextMenu = (
<ContextMenu menuClassName="mx_ReadReceiptGroup_popup" onFinished={closeMenu} {...aboveLeftOf(buttonRect)}>
<AutoHideScrollbar>
<AutoHideScrollbar className="mx_AutoHideScrollbar">
<SectionHeader className="mx_ReadReceiptGroup_title">
{_t("timeline|read_receipt_title", { count: readReceipts.length })}
</SectionHeader>
@@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details.
import React, { useEffect, useMemo, useState } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { AutoHideScrollbar } from "@element-hq/web-shared-components";
import { _t } from "../../../languageHandler";
import StyledRadioGroup from "../elements/StyledRadioGroup";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import SearchBox from "../../structures/SearchBox";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { Entry } from "../dialogs/AddExistingToSpaceDialog";
import { filterBoolean } from "../../../utils/arrays";
@@ -61,7 +61,7 @@ const SpecificChildrenPicker: React.FC<ISpecificChildrenPickerProps> = ({
onSearch={setQuery}
autoFocus={true}
/>
<AutoHideScrollbar>
<AutoHideScrollbar className="mx_AutoHideScrollbar">
{filteredRooms.map((room) => {
return (
<Entry
@@ -327,7 +327,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(
}
: undefined
}
element="ul"
as="ul"
role="tree"
aria-label={_t("common|spaces")}
>
@@ -3,7 +3,7 @@
exports[`MessagePanel should handle large numbers of hidden events quickly 1`] = `
<DocumentFragment>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel cls"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel cls"
tabindex="-1"
>
<div
@@ -22,7 +22,7 @@ exports[`MessagePanel should handle large numbers of hidden events quickly 1`] =
exports[`MessagePanel should handle lots of membership events quickly 1`] = `
<DocumentFragment>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel cls"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel cls"
tabindex="-1"
>
<div
@@ -137,7 +137,7 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
exports[`MessagePanel should handle lots of room creation events quickly 1`] = `
<DocumentFragment>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel cls"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel cls"
tabindex="-1"
>
<div
@@ -137,7 +137,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="mx_RoomView_timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -333,7 +333,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
class="mx_RoomView_timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -738,7 +738,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
class="mx_RoomView_timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -1306,7 +1306,7 @@ exports[`RoomView should hide the composer when hideComposer=true 1`] = `
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
@@ -1317,7 +1317,7 @@ exports[`RoomView should hide the composer when hideComposer=true 1`] = `
data-testid="timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -1376,7 +1376,7 @@ exports[`RoomView should hide the header when hideHeader=true 1`] = `
data-layout="group"
>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
@@ -1387,7 +1387,7 @@ exports[`RoomView should hide the header when hideHeader=true 1`] = `
data-testid="timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -1860,7 +1860,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
@@ -1871,7 +1871,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
data-testid="timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -2344,7 +2344,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
@@ -2355,7 +2355,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
data-testid="timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -2828,7 +2828,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
@@ -3066,7 +3066,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
@@ -3077,7 +3077,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
data-testid="timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -3665,7 +3665,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
class="mx_TimelineCard_timeline"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
@@ -103,7 +103,7 @@ exports[`<TabbedView /> renders tabs 1`] = `
id="mx_tabpanel_GENERAL"
>
<div
class="mx_AutoHideScrollbar mx_TabbedView_tabPanelContent"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_TabbedView_tabPanelContent"
tabindex="-1"
>
<div>
@@ -63,7 +63,7 @@ exports[`<ManageRestrictedJoinRuleDialog /> should list spaces which are not par
</div>
</div>
<div
class="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
tabindex="-1"
>
<div
@@ -276,7 +276,7 @@ exports[`<ManageRestrictedJoinRuleDialog /> should render empty state 1`] = `
</div>
</div>
<div
class="mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ManageRestrictedJoinRuleDialog_content"
tabindex="-1"
>
<span
@@ -25,7 +25,7 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
</h1>
</div>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
tabindex="-1"
>
<div
@@ -166,7 +166,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
</h1>
</div>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_ScrollPanel mx_MessageEditHistoryDialog_scrollPanel"
tabindex="-1"
>
<div
@@ -46,7 +46,7 @@ exports[`<BaseCard /> should close when clicking X button 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div>
@@ -46,7 +46,7 @@ exports[`<ExtensionsCard /> should render empty state 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<button
@@ -147,7 +147,7 @@ exports[`<ExtensionsCard /> should render widgets 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<button
@@ -46,7 +46,7 @@ exports[`<PinnedMessagesCard /> should show the empty state when there are no pi
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -128,7 +128,7 @@ exports[`<PinnedMessagesCard /> should show two pinned messages 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -356,7 +356,7 @@ exports[`<PinnedMessagesCard /> unpin all should not allow to unpinall 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -42,7 +42,7 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<header
@@ -781,7 +781,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<header
@@ -1478,7 +1478,7 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<header
@@ -46,7 +46,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -348,7 +348,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -46,7 +46,7 @@ exports[`<ThirdPartyMemberInfo /> should render invite 1`] = `
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -121,7 +121,7 @@ exports[`<ThirdPartyMemberInfo /> should render invite when room in not availabl
</button>
</div>
<div
class="mx_AutoHideScrollbar"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar"
tabindex="-1"
>
<div
@@ -90,7 +90,7 @@ exports[`<AddExistingToSpaceDialog /> looks as expected 1`] = `
</div>
</div>
<div
class="mx_AutoHideScrollbar mx_AddExistingToSpace_content"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_AddExistingToSpace_content"
tabindex="-1"
>
<span
@@ -54,7 +54,7 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
</div>
<ul
aria-label="Spaces"
class="mx_AutoHideScrollbar mx_SpaceTreeLevel"
class="_scrollbar_1d5jg_8 mx_AutoHideScrollbar mx_SpaceTreeLevel"
data-rbd-droppable-context-id="0"
data-rbd-droppable-id="top-level-spaces"
role="tree"
@@ -0,0 +1,71 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
.scrollbar {
overflow-x: hidden;
overflow-y: auto;
overflow-y: overlay; /* where supported */
-ms-overflow-style: -ms-autohiding-scrollbar;
&::-webkit-scrollbar {
background-color: transparent;
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
}
scrollbar-width: thin;
scrollbar-color: transparent transparent;
> * {
/* also set on first level children in case this scrollbar is used as container */
scrollbar-width: thin;
}
}
.scrollbar:hover {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
:global(.cpd-theme-dark) &,
:global(.cpd-theme-dark-hc) & {
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.2);
:global(.cpd-theme-dark) &,
:global(.cpd-theme-dark-hc) & {
background-color: rgba(255, 255, 255, 0.2);
}
}
}
@media (prefers-color-scheme: dark) {
.scrollbar:hover {
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
:global(.cpd-theme-light) &,
:global(.cpd-theme-light-hc) & {
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: rgba(255, 255, 255, 0.2);
:global(.cpd-theme-light) &,
:global(.cpd-theme-light-hc) & {
background-color: rgba(0, 0, 0, 0.2);
}
}
}
}
@@ -0,0 +1,76 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { AutoHideScrollbar } from "./AutoHideScrollbar";
const containerStyle: React.CSSProperties = {
border: "1px solid",
width: "200px",
height: "150px",
};
const meta = {
title: "Core/AutoHideScrollbar",
component: AutoHideScrollbar,
tags: ["autodocs", "skip-test"],
args: {
as: "div",
role: "container",
style: containerStyle,
children: (
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
<li>Item 7</li>
<li>Item 8</li>
<li>Item 9</li>
</ul>
),
},
} satisfies Meta<typeof AutoHideScrollbar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const NoOverflowY: Story = {
args: {
children: (
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
),
},
};
export const NoOverflowX: Story = {
args: {
children: (
<ul>
<li>Item 1 with a very long text</li>
<li>Item 2 with a very long text</li>
<li>Item 3 with a very long text</li>
<li>Item 4 with a very long text</li>
<li>Item 5 with a very long text</li>
<li>Item 6 with a very long text</li>
<li>Item 7 with a very long text</li>
<li>Item 8 with a very long text</li>
<li>Item 9 with a very long text</li>
</ul>
),
},
};
@@ -0,0 +1,141 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen, waitFor } from "@test-utils";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AutoHideScrollbar } from "./AutoHideScrollbar";
class RefUpdateProbe extends React.Component<any, { armed: boolean }> {
public state = { armed: false };
public wrappedRefCalls = 0;
private readonly wrappedRef = (node: HTMLDivElement | null): void => {
this.wrappedRefCalls += 1;
if (node && !this.state.armed) {
this.setState({ armed: true });
}
};
public render(): React.ReactNode {
return (
<>
<AutoHideScrollbar data-testid="scrollbar" wrappedRef={this.wrappedRef}>
<div>Item 1</div>
</AutoHideScrollbar>
<div data-testid="status">{this.state.armed ? "armed" : "idle"}</div>
</>
);
}
}
describe("AutoHideScrollbar", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("renders children with the default scrollbar props", () => {
render(
<AutoHideScrollbar data-testid="scrollbar">
<div>Item 1</div>
</AutoHideScrollbar>,
);
const scrollbar = screen.getByTestId("scrollbar");
expect(scrollbar).toHaveAttribute("tabindex", "-1");
expect(scrollbar.className).toContain("scrollbar");
expect(screen.getByText("Item 1")).toBeInTheDocument();
});
it("forwards props to the requested element", () => {
render(
<AutoHideScrollbar as="section" aria-label="Scrollable content" data-testid="scrollbar">
<div>Item 1</div>
</AutoHideScrollbar>,
);
const scrollbar = screen.getByTestId("scrollbar");
expect(scrollbar.tagName).toBe("SECTION");
expect(scrollbar).toHaveAttribute("aria-label", "Scrollable content");
});
it("attaches the scroll listener and reports the scroll container ref", async () => {
const onScroll = vi.fn<(event: Event) => void>();
const wrappedRef = vi.fn<(ref: HTMLDivElement | null) => void>();
const addEventListenerSpy = vi.spyOn(HTMLElement.prototype, "addEventListener");
const removeEventListenerSpy = vi.spyOn(HTMLElement.prototype, "removeEventListener");
const { unmount } = render(
<AutoHideScrollbar data-testid="scrollbar" onScroll={onScroll} wrappedRef={wrappedRef}>
<div style={{ height: 1000 }}>Scrollable content</div>
</AutoHideScrollbar>,
);
const scrollbar = screen.getByTestId("scrollbar");
await waitFor(() => expect(wrappedRef).toHaveBeenCalledWith(scrollbar));
expect(addEventListenerSpy).toHaveBeenCalledWith("scroll", onScroll, { passive: true });
fireEvent.scroll(scrollbar);
expect(onScroll).toHaveBeenCalledTimes(1);
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith("scroll", onScroll);
expect(wrappedRef).toHaveBeenLastCalledWith(null);
fireEvent.scroll(scrollbar);
expect(onScroll).toHaveBeenCalledTimes(1);
});
it("provides wrappedRef before parent componentDidMount runs", () => {
const calls: Array<string> = [];
class TimingProbe extends React.Component {
private scrollNode: HTMLDivElement | null = null;
public componentDidMount(): void {
calls.push("parent-did-mount");
expect(this.scrollNode).not.toBeNull();
}
public render(): React.ReactNode {
return (
<AutoHideScrollbar
data-testid="scrollbar"
wrappedRef={(node) => {
calls.push(node ? "wrapped-ref" : "wrapped-ref-null");
this.scrollNode = node;
}}
>
<div>Item 1</div>
</AutoHideScrollbar>
);
}
}
render(<TimingProbe />);
expect(calls).toEqual(["wrapped-ref", "parent-did-mount"]);
expect(screen.getByTestId("scrollbar")).toBeInTheDocument();
});
it("does not re-enter wrappedRef when the parent rerenders from the callback", async () => {
const probeRef = React.createRef<RefUpdateProbe>();
render(<RefUpdateProbe ref={probeRef} />);
await waitFor(() => expect(screen.getByTestId("status")).toHaveTextContent("armed"));
expect(probeRef.current?.wrappedRefCalls).toBe(1);
expect(screen.getByTestId("scrollbar")).toBeInTheDocument();
});
});
@@ -0,0 +1,94 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type HTMLAttributes, type JSX, type ReactNode, type WheelEvent } from "react";
import styles from "./AutoHideScrollbar.module.css";
type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
JSX.IntrinsicElements[T] extends HTMLAttributes<object> ? DynamicElementProps<T> : DynamicElementProps<"div">;
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], "ref">>;
/**
* Props for `AutoHideScrollbar`.
*/
export type AutoHideScrollbarProps<T extends keyof JSX.IntrinsicElements = "div"> = Omit<
DynamicHtmlElementProps<T>,
"onScroll"
> & {
/** The type of the HTML element. @default div*/
as?: T;
/** Additional class names to append to the scrollbar root. */
className?: string;
/** Inline styles applied to the root element. */
style?: React.CSSProperties;
/** Tab index override; defaults to `-1`. */
tabIndex?: number;
/** Receives the mounted scroll container element. */
wrappedRef?: (ref: HTMLDivElement | null) => void;
/** Native scroll handler attached with a passive listener. */
onScroll?: (event: Event) => void;
/** Optional wheel handler forwarded to the root element. */
onWheel?: (event: WheelEvent) => void;
/** Scrollable content rendered inside the container. */
children: ReactNode;
};
/**
* Scroll container that hides native scrollbars until hovered.
* Any overflow-x is hidden by default.
*/
export function AutoHideScrollbar<T extends keyof JSX.IntrinsicElements = "div">(
props: AutoHideScrollbarProps<T>,
): React.ReactNode {
const { as = "div" as T, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = props;
const containerRef = React.useRef<HTMLDivElement | null>(null);
const wrappedRefRef = React.useRef(wrappedRef);
wrappedRefRef.current = wrappedRef;
const collectContainer = React.useCallback((node: HTMLDivElement | null): void => {
containerRef.current = node;
}, []);
React.useLayoutEffect(() => {
wrappedRefRef.current?.(containerRef.current);
return (): void => {
wrappedRefRef.current?.(null);
};
}, []);
React.useLayoutEffect(() => {
const container = containerRef.current;
if (!container || !onScroll) {
return;
}
// Using the passive option to not block the main thread.
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
container.addEventListener("scroll", onScroll, { passive: true });
return (): void => {
container.removeEventListener("scroll", onScroll);
};
}, [onScroll]);
return React.createElement(
as,
{
...otherProps,
ref: collectContainer,
className: classNames(styles.scrollbar, className),
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order by default.
tabIndex: tabIndex ?? -1,
},
children,
);
}
@@ -0,0 +1,8 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export { AutoHideScrollbar, type AutoHideScrollbarProps } from "./AutoHideScrollbar";
+1
View File
@@ -74,6 +74,7 @@ export * from "./room-list/VirtualizedRoomListView/RoomListItemWrapper";
export * from "./core/utils/Box";
export * from "./core/utils/Flex";
export * from "./core/utils/LinkedText";
export * from "./core/utils/Scrollbar";
export * from "./core/VirtualizedList";
export * from "./resize";
@@ -9,3 +9,10 @@
position: relative;
flex: 1;
}
.scrollbar {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
}
@@ -17,6 +17,7 @@ import { type RoomListSectionHeaderViewModel } from "../VirtualizedRoomListView/
import { type ToastType, RoomListToast } from "./RoomListToast";
import styles from "./RoomListView.module.css";
import { Flex } from "../../core/utils/Flex";
import { AutoHideScrollbar } from "../../core/utils/Scrollbar";
export type RoomListSection = {
/** Unique identifier for the section */
@@ -120,8 +121,10 @@ export const RoomListView: React.FC<RoomListViewProps> = ({ vm, renderAvatar, on
/>
</div>
<Flex direction="column" className={styles.list}>
{listBody}
{snapshot.toast && <RoomListToast type={snapshot.toast} onClose={vm.closeToast} />}
<AutoHideScrollbar className={styles.scrollbar}>
{listBody}
{snapshot.toast && <RoomListToast type={snapshot.toast} onClose={vm.closeToast} />}
</AutoHideScrollbar>
</Flex>
</>
);