Refactor ExtraTile to use functional components (#10191)

This commit is contained in:
Germain 2023-02-23 11:57:37 +00:00 committed by GitHub
parent 9349526d94
commit 8f7f855ad4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 73 deletions

View file

@ -1,5 +1,5 @@
/*
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Copyright 2020 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,8 +21,9 @@ import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../
import NotificationBadge from "./NotificationBadge";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import { ButtonEvent } from "../elements/AccessibleButton";
import useHover from "../../../hooks/useHover";
interface IProps {
interface ExtraTileProps {
isMinimized: boolean;
isSelected: boolean;
displayName: string;
@ -31,83 +32,68 @@ interface IProps {
onClick: (ev: ButtonEvent) => void;
}
interface IState {
hover: boolean;
}
export default function ExtraTile({
isSelected,
isMinimized,
notificationState,
displayName,
onClick,
avatar,
}: ExtraTileProps): JSX.Element {
const [, { onMouseOver, onMouseLeave }] = useHover(() => false);
export default class ExtraTile extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
// XXX: We copy classes because it's easier
const classes = classNames({
mx_ExtraTile: true,
mx_RoomTile: true,
mx_RoomTile_selected: isSelected,
mx_RoomTile_minimized: isMinimized,
});
this.state = {
hover: false,
};
let badge: JSX.Element | null = null;
if (notificationState) {
badge = <NotificationBadge notification={notificationState} forceCount={false} />;
}
private onTileMouseEnter = (): void => {
this.setState({ hover: true });
};
let name = displayName;
if (typeof name !== "string") name = "";
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
private onTileMouseLeave = (): void => {
this.setState({ hover: false });
};
const nameClasses = classNames({
mx_RoomTile_title: true,
mx_RoomTile_titleHasUnreadEvents: notificationState?.isUnread,
});
public render(): React.ReactElement {
// XXX: We copy classes because it's easier
const classes = classNames({
mx_ExtraTile: true,
mx_RoomTile: true,
mx_RoomTile_selected: this.props.isSelected,
mx_RoomTile_minimized: this.props.isMinimized,
});
let nameContainer: JSX.Element | null = (
<div className="mx_RoomTile_titleContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
</div>
</div>
);
if (isMinimized) nameContainer = null;
let badge;
if (this.props.notificationState) {
badge = <NotificationBadge notification={this.props.notificationState} forceCount={false} />;
}
let Button = RovingAccessibleButton;
if (isMinimized) {
Button = RovingAccessibleTooltipButton;
}
let name = this.props.displayName;
if (typeof name !== "string") name = "";
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({
mx_RoomTile_title: true,
mx_RoomTile_titleHasUnreadEvents: this.props.notificationState?.isUnread,
});
let nameContainer = (
<div className="mx_RoomTile_titleContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
return (
<Button
className={classes}
onMouseEnter={onMouseOver}
onMouseLeave={onMouseLeave}
onClick={onClick}
role="treeitem"
title={isMinimized ? name : undefined}
>
<div className="mx_RoomTile_avatarContainer">{avatar}</div>
<div className="mx_RoomTile_details">
<div className="mx_RoomTile_primaryDetails">
{nameContainer}
<div className="mx_RoomTile_badgeContainer">{badge}</div>
</div>
</div>
);
if (this.props.isMinimized) nameContainer = null;
let Button = RovingAccessibleButton;
if (this.props.isMinimized) {
Button = RovingAccessibleTooltipButton;
}
return (
<React.Fragment>
<Button
className={classes}
onMouseEnter={this.onTileMouseEnter}
onMouseLeave={this.onTileMouseLeave}
onClick={this.props.onClick}
role="treeitem"
title={this.props.isMinimized ? name : undefined}
>
<div className="mx_RoomTile_avatarContainer">{this.props.avatar}</div>
<div className="mx_RoomTile_details">
<div className="mx_RoomTile_primaryDetails">
{nameContainer}
<div className="mx_RoomTile_badgeContainer">{badge}</div>
</div>
</div>
</Button>
</React.Fragment>
);
}
</Button>
);
}

View file

@ -82,7 +82,7 @@ interface IProps {
alwaysVisible?: boolean;
forceExpanded?: boolean;
resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
extraTiles?: ReactComponentElement<typeof ExtraTile>[] | null;
onListCollapse?: (isExpanded: boolean) => void;
}
@ -170,7 +170,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
}
private static calcNumTiles(rooms: Room[], extraTiles: any[]): number {
private static calcNumTiles(rooms: Room[], extraTiles?: any[] | null): number {
return (rooms || []).length + (extraTiles || []).length;
}

View file

@ -0,0 +1,61 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { getByRole, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React, { ComponentProps } from "react";
import ExtraTile from "../../../../src/components/views/rooms/ExtraTile";
describe("ExtraTile", () => {
function renderComponent(props: Partial<ComponentProps<typeof ExtraTile>> = {}) {
const defaultProps: ComponentProps<typeof ExtraTile> = {
isMinimized: false,
isSelected: false,
displayName: "test",
avatar: <React.Fragment />,
onClick: () => {},
};
return render(<ExtraTile {...defaultProps} {...props} />);
}
it("renders", () => {
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("hides text when minimized", () => {
const { container } = renderComponent({
isMinimized: true,
displayName: "testDisplayName",
});
expect(container).not.toHaveTextContent("testDisplayName");
});
it("registers clicks", async () => {
const onClick = jest.fn();
const { container } = renderComponent({
onClick,
});
const btn = getByRole(container, "treeitem");
await userEvent.click(btn);
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExtraTile renders 1`] = `
<DocumentFragment>
<div
class="mx_AccessibleButton mx_ExtraTile mx_RoomTile"
role="treeitem"
tabindex="-1"
>
<div
class="mx_RoomTile_avatarContainer"
/>
<div
class="mx_RoomTile_details"
>
<div
class="mx_RoomTile_primaryDetails"
>
<div
class="mx_RoomTile_titleContainer"
>
<div
class="mx_RoomTile_title"
dir="auto"
tabindex="-1"
title="test"
>
test
</div>
</div>
<div
class="mx_RoomTile_badgeContainer"
/>
</div>
</div>
</div>
</DocumentFragment>
`;