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:
@@ -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
-3
@@ -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
|
||||
|
||||
+15
-15
@@ -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
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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";
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
+16731
-16646
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user