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

View file

@ -82,7 +82,7 @@ interface IProps {
alwaysVisible?: boolean; alwaysVisible?: boolean;
forceExpanded?: boolean; forceExpanded?: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
extraTiles?: ReactComponentElement<typeof ExtraTile>[]; extraTiles?: ReactComponentElement<typeof ExtraTile>[] | null;
onListCollapse?: (isExpanded: boolean) => void; 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); 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; 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>
`;