Correct accessibility labels for unread rooms in spotlight (#9003)

* Correct accessibility labels for unread rooms in spotlight
* Improve public room result accessibility
* Improve room result accessibility
This commit is contained in:
Janne Mareike Koschinski 2022-07-11 13:34:23 +02:00 committed by GitHub
parent 375ff265db
commit a9d6896502
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 206 additions and 98 deletions

View file

@ -49,9 +49,9 @@ import BaseAvatar from "../avatars/BaseAvatar";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { ButtonEvent } from "../elements/AccessibleButton"; import { ButtonEvent } from "../elements/AccessibleButton";
import { roomContextDetailsText } from "../../../utils/i18n-helpers";
import { isLocationEvent } from "../../../utils/EventUtils"; import { isLocationEvent } from "../../../utils/EventUtils";
import { isSelfLocation, locationEventGeoUri } from "../../../utils/location"; import { isSelfLocation, locationEventGeoUri } from "../../../utils/location";
import { RoomContextDetails } from "../rooms/RoomContextDetails";
const AVATAR_SIZE = 30; const AVATAR_SIZE = 30;
@ -130,8 +130,6 @@ const Entry: React.FC<IEntryProps> = ({ room, type, content, matrixClient: cli,
/>; />;
} }
const detailsText = roomContextDetailsText(room);
return <div className="mx_ForwardList_entry"> return <div className="mx_ForwardList_entry">
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_ForwardList_roomButton" className="mx_ForwardList_roomButton"
@ -141,9 +139,7 @@ const Entry: React.FC<IEntryProps> = ({ room, type, content, matrixClient: cli,
> >
<DecoratedRoomAvatar room={room} avatarSize={32} /> <DecoratedRoomAvatar room={room} avatarSize={32} />
<span className="mx_ForwardList_entry_name">{ room.name }</span> <span className="mx_ForwardList_entry_name">{ room.name }</span>
{ detailsText && <span className="mx_ForwardList_entry_detail"> <RoomContextDetails component="span" className="mx_ForwardList_entry_detail" room={room} />
{ detailsText }
</span> }
</AccessibleTooltipButton> </AccessibleTooltipButton>
<AccessibleTooltipButton <AccessibleTooltipButton
kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"} kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}

View file

@ -24,7 +24,14 @@ import { getDisplayAliasForRoom } from "../../../structures/RoomDirectory";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
export function PublicRoomResultDetails({ room }: { room: IPublicRoomsChunkRoom }): JSX.Element { interface Props {
room: IPublicRoomsChunkRoom;
labelId: string;
descriptionId: string;
detailsId: string;
}
export function PublicRoomResultDetails({ room, labelId, descriptionId, detailsId }: Props): JSX.Element {
let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room'); let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) { if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`; name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
@ -41,12 +48,12 @@ export function PublicRoomResultDetails({ room }: { room: IPublicRoomsChunkRoom
return ( return (
<div className="mx_SpotlightDialog_result_publicRoomDetails"> <div className="mx_SpotlightDialog_result_publicRoomDetails">
<div className="mx_SpotlightDialog_result_publicRoomHeader"> <div className="mx_SpotlightDialog_result_publicRoomHeader">
<span className="mx_SpotlightDialog_result_publicRoomName">{ name }</span> <span id={labelId} className="mx_SpotlightDialog_result_publicRoomName">{ name }</span>
<span className="mx_SpotlightDialog_result_publicRoomAlias"> <span id={descriptionId} className="mx_SpotlightDialog_result_publicRoomAlias">
{ room.canonical_alias ?? room.room_id } { room.canonical_alias ?? room.room_id }
</span> </span>
</div> </div>
<div className="mx_SpotlightDialog_result_publicRoomDescription"> <div id={detailsId} className="mx_SpotlightDialog_result_publicRoomDescription">
<span className="mx_SpotlightDialog_result_publicRoomMemberCount"> <span className="mx_SpotlightDialog_result_publicRoomMemberCount">
{ _t("%(count)s Members", { { _t("%(count)s Members", {
count: room.num_joined_members, count: room.num_joined_members,

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { WebSearch as WebSearchEvent } from "@matrix-org/analytics-events/types/typescript/WebSearch";
import classNames from "classnames"; import classNames from "classnames";
import { capitalize, sum } from "lodash"; import { capitalize, sum } from "lodash";
import { WebSearch as WebSearchEvent } from "@matrix-org/analytics-events/types/typescript/WebSearch";
import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
import { IPublicRoomsChunkRoom, MatrixClient, RoomMember, RoomType } from "matrix-js-sdk/src/matrix"; import { IPublicRoomsChunkRoom, MatrixClient, RoomMember, RoomType } from "matrix-js-sdk/src/matrix";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
@ -50,6 +50,7 @@ import { useDebouncedCallback } from "../../../../hooks/spotlight/useDebouncedCa
import { useRecentSearches } from "../../../../hooks/spotlight/useRecentSearches"; import { useRecentSearches } from "../../../../hooks/spotlight/useRecentSearches";
import { useProfileInfo } from "../../../../hooks/useProfileInfo"; import { useProfileInfo } from "../../../../hooks/useProfileInfo";
import { usePublicRoomDirectory } from "../../../../hooks/usePublicRoomDirectory"; import { usePublicRoomDirectory } from "../../../../hooks/usePublicRoomDirectory";
import { useFeatureEnabled } from "../../../../hooks/useSettings";
import { useSpaceResults } from "../../../../hooks/useSpaceResults"; import { useSpaceResults } from "../../../../hooks/useSpaceResults";
import { useUserDirectory } from "../../../../hooks/useUserDirectory"; import { useUserDirectory } from "../../../../hooks/useUserDirectory";
import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
@ -63,6 +64,7 @@ import SdkConfig from "../../../../SdkConfig";
import { SettingLevel } from "../../../../settings/SettingLevel"; import { SettingLevel } from "../../../../settings/SettingLevel";
import SettingsStore from "../../../../settings/SettingsStore"; import SettingsStore from "../../../../settings/SettingsStore";
import { BreadcrumbsStore } from "../../../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../../../stores/BreadcrumbsStore";
import { RoomNotificationState } from "../../../../stores/notifications/RoomNotificationState";
import { RoomNotificationStateStore } from "../../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../../stores/notifications/RoomNotificationStateStore";
import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import { RoomViewStore } from "../../../../stores/RoomViewStore"; import { RoomViewStore } from "../../../../stores/RoomViewStore";
@ -78,6 +80,7 @@ import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar";
import { SearchResultAvatar } from "../../avatars/SearchResultAvatar"; import { SearchResultAvatar } from "../../avatars/SearchResultAvatar";
import { NetworkDropdown } from "../../directory/NetworkDropdown"; import { NetworkDropdown } from "../../directory/NetworkDropdown";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../elements/AccessibleButton";
import LabelledCheckbox from "../../elements/LabelledCheckbox";
import Spinner from "../../elements/Spinner"; import Spinner from "../../elements/Spinner";
import NotificationBadge from "../../rooms/NotificationBadge"; import NotificationBadge from "../../rooms/NotificationBadge";
import BaseDialog from "../BaseDialog"; import BaseDialog from "../BaseDialog";
@ -85,10 +88,8 @@ import FeedbackDialog from "../FeedbackDialog";
import { IDialogProps } from "../IDialogProps"; import { IDialogProps } from "../IDialogProps";
import { Option } from "./Option"; import { Option } from "./Option";
import { PublicRoomResultDetails } from "./PublicRoomResultDetails"; import { PublicRoomResultDetails } from "./PublicRoomResultDetails";
import { RoomResultDetails } from "./RoomResultDetails"; import { RoomContextDetails } from "../../rooms/RoomContextDetails";
import { TooltipOption } from "./TooltipOption"; import { TooltipOption } from "./TooltipOption";
import LabelledCheckbox from "../../elements/LabelledCheckbox";
import { useFeatureEnabled } from "../../../../hooks/useSettings";
const MAX_RECENT_SEARCHES = 10; const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@ -259,6 +260,22 @@ const findVisibleRoomMembers = (cli: MatrixClient, filterDMs = true) => {
).filter(it => it.userId !== cli.getUserId()); ).filter(it => it.userId !== cli.getUserId());
}; };
const roomAriaUnreadLabel = (room: Room, notification: RoomNotificationState): string | undefined => {
if (notification.hasMentions) {
return _t("%(count)s unread messages including mentions.", {
count: notification.count,
});
} else if (notification.hasUnreadCount) {
return _t("%(count)s unread messages.", {
count: notification.count,
});
} else if (notification.isUnread) {
return _t("Unread messages.");
} else {
return undefined;
}
};
interface IDirectoryOpts { interface IDirectoryOpts {
limit: number; limit: number;
query: string; query: string;
@ -523,6 +540,12 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
if (trimmedQuery || filter !== null) { if (trimmedQuery || filter !== null) {
const resultMapper = (result: Result): JSX.Element => { const resultMapper = (result: Result): JSX.Element => {
if (isRoomResult(result)) { if (isRoomResult(result)) {
const notification = RoomNotificationStateStore.instance.getRoomState(result.room);
const unreadLabel = roomAriaUnreadLabel(result.room, notification);
const ariaProperties = {
"aria-label": unreadLabel ? `${result.room.name} ${unreadLabel}` : result.room.name,
"aria-details": `mx_SpotlightDialog_button_result_${result.room.roomId}_details`,
};
return ( return (
<Option <Option
id={`mx_SpotlightDialog_button_result_${result.room.roomId}`} id={`mx_SpotlightDialog_button_result_${result.room.roomId}`}
@ -530,11 +553,20 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
onClick={(ev) => { onClick={(ev) => {
viewRoom(result.room.roomId, true, ev?.type !== "click"); viewRoom(result.room.roomId, true, ev?.type !== "click");
}} }}
{...ariaProperties}
> >
<DecoratedRoomAvatar room={result.room} avatarSize={AVATAR_SIZE} tooltipProps={{ tabIndex: -1 }} /> <DecoratedRoomAvatar
room={result.room}
avatarSize={AVATAR_SIZE}
tooltipProps={{ tabIndex: -1 }}
/>
{ result.room.name } { result.room.name }
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(result.room)} /> <NotificationBadge notification={notification} />
<RoomResultDetails room={result.room} /> <RoomContextDetails
id={`mx_SpotlightDialog_button_result_${result.room.roomId}_details`}
className="mx_SpotlightDialog_result_details"
room={result.room}
/>
</Option> </Option>
); );
} }
@ -547,10 +579,17 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
startDm(cli, [result.member]); startDm(cli, [result.member]);
onFinished(); onFinished();
}} }}
aria-label={result.member instanceof RoomMember
? result.member.rawDisplayName
: result.member.name}
aria-describedby={`mx_SpotlightDialog_button_result_${result.member.userId}_details`}
> >
<SearchResultAvatar user={result.member} size={AVATAR_SIZE} /> <SearchResultAvatar user={result.member} size={AVATAR_SIZE} />
{ result.member instanceof RoomMember ? result.member.rawDisplayName : result.member.name } { result.member instanceof RoomMember ? result.member.rawDisplayName : result.member.name }
<div className="mx_SpotlightDialog_result_details"> <div
id={`mx_SpotlightDialog_button_result_${result.member.userId}_details`}
className="mx_SpotlightDialog_result_details"
>
{ result.member.userId } { result.member.userId }
</div> </div>
</Option> </Option>
@ -575,6 +614,9 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
> >
{ _t(clientRoom ? "View" : "Join") } { _t(clientRoom ? "View" : "Join") }
</AccessibleButton>} </AccessibleButton>}
aria-labelledby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_name`}
aria-describedby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
aria-details={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_details`}
> >
<BaseAvatar <BaseAvatar
className="mx_SearchResultAvatar" className="mx_SearchResultAvatar"
@ -586,7 +628,12 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
width={AVATAR_SIZE} width={AVATAR_SIZE}
height={AVATAR_SIZE} height={AVATAR_SIZE}
/> />
<PublicRoomResultDetails room={result.publicRoom} /> <PublicRoomResultDetails
room={result.publicRoom}
labelId={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_name`}
descriptionId={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
detailsId={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_details`}
/>
</Option> </Option>
); );
} }
@ -608,8 +655,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let peopleSection: JSX.Element; let peopleSection: JSX.Element;
if (results[Section.People].length) { if (results[Section.People].length) {
peopleSection = ( peopleSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group"> <div
<h4>{ _t("Recent Conversations") }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_results"
role="group"
aria-labelledby="mx_SpotlightDialog_section_people">
<h4 id="mx_SpotlightDialog_section_people">
{ _t("Recent Conversations") }
</h4>
<div> <div>
{ results[Section.People].slice(0, SECTION_LIMIT).map(resultMapper) } { results[Section.People].slice(0, SECTION_LIMIT).map(resultMapper) }
</div> </div>
@ -620,8 +672,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let suggestionsSection: JSX.Element; let suggestionsSection: JSX.Element;
if (results[Section.Suggestions].length && filter === Filter.People) { if (results[Section.Suggestions].length && filter === Filter.People) {
suggestionsSection = ( suggestionsSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group"> <div
<h4>{ _t("Suggestions") }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_results"
role="group"
aria-labelledby="mx_SpotlightDialog_section_suggestions">
<h4 id="mx_SpotlightDialog_section_suggestions">
{ _t("Suggestions") }
</h4>
<div> <div>
{ results[Section.Suggestions].slice(0, SECTION_LIMIT).map(resultMapper) } { results[Section.Suggestions].slice(0, SECTION_LIMIT).map(resultMapper) }
</div> </div>
@ -632,8 +689,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let roomsSection: JSX.Element; let roomsSection: JSX.Element;
if (results[Section.Rooms].length) { if (results[Section.Rooms].length) {
roomsSection = ( roomsSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group"> <div
<h4>{ _t("Rooms") }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_results"
role="group"
aria-labelledby="mx_SpotlightDialog_section_rooms">
<h4 id="mx_SpotlightDialog_section_rooms">
{ _t("Rooms") }
</h4>
<div> <div>
{ results[Section.Rooms].slice(0, SECTION_LIMIT).map(resultMapper) } { results[Section.Rooms].slice(0, SECTION_LIMIT).map(resultMapper) }
</div> </div>
@ -644,8 +706,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let spacesSection: JSX.Element; let spacesSection: JSX.Element;
if (results[Section.Spaces].length) { if (results[Section.Spaces].length) {
spacesSection = ( spacesSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group"> <div
<h4>{ _t("Spaces you're in") }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_results"
role="group"
aria-labelledby="mx_SpotlightDialog_section_spaces">
<h4 id="mx_SpotlightDialog_section_spaces">
{ _t("Spaces you're in") }
</h4>
<div> <div>
{ results[Section.Spaces].slice(0, SECTION_LIMIT).map(resultMapper) } { results[Section.Spaces].slice(0, SECTION_LIMIT).map(resultMapper) }
</div> </div>
@ -656,9 +723,14 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let publicRoomsSection: JSX.Element; let publicRoomsSection: JSX.Element;
if (filter === Filter.PublicRooms) { if (filter === Filter.PublicRooms) {
publicRoomsSection = ( publicRoomsSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group"> <div
className="mx_SpotlightDialog_section mx_SpotlightDialog_results"
role="group"
aria-labelledby="mx_SpotlightDialog_section_publicRooms">
<div className="mx_SpotlightDialog_sectionHeader"> <div className="mx_SpotlightDialog_sectionHeader">
<h4>{ _t("Suggestions") }</h4> <h4 id="mx_SpotlightDialog_section_publicRooms">
{ _t("Suggestions") }
</h4>
<div className="mx_SpotlightDialog_options"> <div className="mx_SpotlightDialog_options">
{ exploringPublicSpacesEnabled && <> { exploringPublicSpacesEnabled && <>
<LabelledCheckbox <LabelledCheckbox
@ -692,8 +764,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let spaceRoomsSection: JSX.Element; let spaceRoomsSection: JSX.Element;
if (spaceResults.length && activeSpace && filter === null) { if (spaceResults.length && activeSpace && filter === null) {
spaceRoomsSection = ( spaceRoomsSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_results" role="group"> <div
<h4>{ _t("Other rooms in %(spaceName)s", { spaceName: activeSpace.name }) }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_results"
role="group"
aria-labelledby="mx_SpotlightDialog_section_spaceRooms">
<h4 id="mx_SpotlightDialog_section_spaceRooms">
{ _t("Other rooms in %(spaceName)s", { spaceName: activeSpace.name }) }
</h4>
<div> <div>
{ spaceResults.slice(0, SECTION_LIMIT).map((room: IHierarchyRoom): JSX.Element => ( { spaceResults.slice(0, SECTION_LIMIT).map((room: IHierarchyRoom): JSX.Element => (
<Option <Option
@ -807,8 +884,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let groupChatSection: JSX.Element; let groupChatSection: JSX.Element;
if (filter === Filter.People) { if (filter === Filter.People) {
groupChatSection = ( groupChatSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group"> <div
<h4>{ _t('Other options') }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches"
role="group"
aria-labelledby="mx_SpotlightDialog_section_groupChat">
<h4 id="mx_SpotlightDialog_section_groupChat">
{ _t('Other options') }
</h4>
<Option <Option
id="mx_SpotlightDialog_button_startGroupChat" id="mx_SpotlightDialog_button_startGroupChat"
className="mx_SpotlightDialog_startGroupChat" className="mx_SpotlightDialog_startGroupChat"
@ -823,8 +905,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
let messageSearchSection: JSX.Element; let messageSearchSection: JSX.Element;
if (filter === null) { if (filter === null) {
messageSearchSection = ( messageSearchSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group"> <div
<h4>{ _t("Other searches") }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches"
role="group"
aria-labelledby="mx_SpotlightDialog_section_messageSearch">
<h4 id="mx_SpotlightDialog_section_messageSearch">
{ _t("Other searches") }
</h4>
<div className="mx_SpotlightDialog_otherSearches_messageSearchText"> <div className="mx_SpotlightDialog_otherSearches_messageSearchText">
{ _t( { _t(
"To search messages, look for this icon at the top of a room <icon/>", "To search messages, look for this icon at the top of a room <icon/>",
@ -859,36 +946,59 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
// Firefox sometimes makes this element focusable due to overflow, // Firefox sometimes makes this element focusable due to overflow,
// so force it out of tab order by default. // so force it out of tab order by default.
tabIndex={-1} tabIndex={-1}
aria-labelledby="mx_SpotlightDialog_section_recentSearches"
> >
<h4> <h4 id="mx_SpotlightDialog_section_recentSearches">
{ _t("Recent searches") } { _t("Recent searches") }
<AccessibleButton kind="link" onClick={clearRecentSearches}> <AccessibleButton kind="link" onClick={clearRecentSearches}>
{ _t("Clear") } { _t("Clear") }
</AccessibleButton> </AccessibleButton>
</h4> </h4>
<div> <div>
{ recentSearches.map(room => ( { recentSearches.map(room => {
<Option const notification = RoomNotificationStateStore.instance.getRoomState(room);
id={`mx_SpotlightDialog_button_recentSearch_${room.roomId}`} const unreadLabel = roomAriaUnreadLabel(room, notification);
key={room.roomId} const ariaProperties = {
onClick={(ev) => { "aria-label": unreadLabel ? `${room.name} ${unreadLabel}` : room.name,
viewRoom(room.roomId, true, ev?.type !== "click"); "aria-details": `mx_SpotlightDialog_button_recentSearch_${room.roomId}_details`,
}} };
> return (
<DecoratedRoomAvatar room={room} avatarSize={AVATAR_SIZE} tooltipProps={{ tabIndex: -1 }} /> <Option
{ room.name } id={`mx_SpotlightDialog_button_recentSearch_${room.roomId}`}
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(room)} /> key={room.roomId}
<RoomResultDetails room={room} /> onClick={(ev) => {
</Option> viewRoom(room.roomId, true, ev?.type !== "click");
)) } }}
{...ariaProperties}
>
<DecoratedRoomAvatar
room={room}
avatarSize={AVATAR_SIZE}
tooltipProps={{ tabIndex: -1 }}
/>
{ room.name }
<NotificationBadge notification={notification} />
<RoomContextDetails
id={`mx_SpotlightDialog_button_recentSearch_${room.roomId}_details`}
className="mx_SpotlightDialog_result_details"
room={room}
/>
</Option>
);
}) }
</div> </div>
</div> </div>
); );
} }
content = <> content = <>
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_recentlyViewed" role="group"> <div
<h4>{ _t("Recently viewed") }</h4> className="mx_SpotlightDialog_section mx_SpotlightDialog_recentlyViewed"
role="group"
aria-labelledby="mx_SpotlightDialog_section_recentlyViewed">
<h4 id="mx_SpotlightDialog_section_recentlyViewed">
{ _t("Recently viewed") }
</h4>
<div> <div>
{ BreadcrumbsStore.instance.rooms { BreadcrumbsStore.instance.rooms
.filter(r => r.roomId !== RoomViewStore.instance.getRoomId()) .filter(r => r.roomId !== RoomViewStore.instance.getRoomId())

View file

@ -22,11 +22,11 @@ import { MenuItem } from "../../structures/ContextMenu";
import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { RoomContextDetails } from "./RoomContextDetails";
import InteractiveTooltip, { Direction } from "../elements/InteractiveTooltip"; import InteractiveTooltip, { Direction } from "../elements/InteractiveTooltip";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { roomContextDetailsText } from "../../../utils/i18n-helpers";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
const RecentlyViewedButton = () => { const RecentlyViewedButton = () => {
@ -37,8 +37,6 @@ const RecentlyViewedButton = () => {
<h4>{ _t("Recently viewed") }</h4> <h4>{ _t("Recently viewed") }</h4>
<div> <div>
{ crumbs.map(crumb => { { crumbs.map(crumb => {
const contextDetails = roomContextDetailsText(crumb);
return <MenuItem return <MenuItem
key={crumb.roomId} key={crumb.roomId}
onClick={(ev) => { onClick={(ev) => {
@ -57,9 +55,7 @@ const RecentlyViewedButton = () => {
} }
<span className="mx_RecentlyViewedButton_entry_label"> <span className="mx_RecentlyViewedButton_entry_label">
<div>{ crumb.name }</div> <div>{ crumb.name }</div>
{ contextDetails && <div className="mx_RecentlyViewedButton_entry_spaces"> <RoomContextDetails className="mx_RecentlyViewedButton_entry_spaces" room={crumb} />
{ contextDetails }
</div> }
</span> </span>
</MenuItem>; </MenuItem>;
}) } }) }

View file

@ -14,18 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react";
import { Room } from "matrix-js-sdk/src/matrix"; import { Room } from "matrix-js-sdk/src/matrix";
import React, { ComponentPropsWithoutRef, ElementType } from "react";
import { roomContextDetailsText, spaceContextDetailsText } from "../../../../utils/i18n-helpers"; import { roomContextDetails } from "../../../utils/i18n-helpers";
export const RoomResultDetails = ({ room }: { room: Room }) => { type Props<T extends ElementType> = ComponentPropsWithoutRef<T> & {
const contextDetails = room.isSpaceRoom() ? spaceContextDetailsText(room) : roomContextDetailsText(room); component?: T;
room: Room;
};
export function RoomContextDetails<T extends ElementType>({ room, component: Component = "div", ...other }: Props<T>) {
const contextDetails = roomContextDetails(room);
if (contextDetails) { if (contextDetails) {
return <div className="mx_SpotlightDialog_result_details"> return <Component
{ contextDetails } {...other}
</div>; aria-label={contextDetails.ariaLabel}
>
{ contextDetails.details }
</Component>;
} }
return null; return null;
}; }

View file

@ -661,9 +661,13 @@
"about a day from now": "about a day from now", "about a day from now": "about a day from now",
"%(num)s days from now": "%(num)s days from now", "%(num)s days from now": "%(num)s days from now",
"%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s", "%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s",
"In spaces %(space1Name)s and %(space2Name)s.": "In spaces %(space1Name)s and %(space2Name)s.",
"%(spaceName)s and %(count)s others|other": "%(spaceName)s and %(count)s others", "%(spaceName)s and %(count)s others|other": "%(spaceName)s and %(count)s others",
"%(spaceName)s and %(count)s others|zero": "%(spaceName)s", "%(spaceName)s and %(count)s others|zero": "%(spaceName)s",
"%(spaceName)s and %(count)s others|one": "%(spaceName)s and %(count)s other", "%(spaceName)s and %(count)s others|one": "%(spaceName)s and %(count)s other",
"In %(spaceName)s and %(count)s other spaces.|other": "In %(spaceName)s and %(count)s other spaces.",
"In %(spaceName)s and %(count)s other spaces.|zero": "In space %(spaceName)s.",
"In %(spaceName)s and %(count)s other spaces.|one": "In %(spaceName)s and %(count)s other space.",
"%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
"Unexpected server error trying to leave the room": "Unexpected server error trying to leave the room", "Unexpected server error trying to leave the room": "Unexpected server error trying to leave the room",
"Can't leave Server Notices room": "Can't leave Server Notices room", "Can't leave Server Notices room": "Can't leave Server Notices room",

View file

@ -20,49 +20,36 @@ import SpaceStore from "../stores/spaces/SpaceStore";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import DMRoomMap from "./DMRoomMap"; import DMRoomMap from "./DMRoomMap";
export function spaceContextDetailsText(space: Room): string { export interface RoomContextDetails {
if (!space.isSpaceRoom()) return undefined; details: string;
ariaLabel?: string;
const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(space.roomId);
if (secondParent && !otherParents?.length) {
// exactly 2 edge case for improved i18n
return _t("%(space1Name)s and %(space2Name)s", {
space1Name: space.client.getRoom(parent)?.name,
space2Name: space.client.getRoom(secondParent)?.name,
});
} else if (parent) {
return _t("%(spaceName)s and %(count)s others", {
spaceName: space.client.getRoom(parent)?.name,
count: otherParents.length,
});
}
return space.getCanonicalAlias();
} }
export function roomContextDetailsText(room: Room): string { export function roomContextDetails(room: Room): RoomContextDetails | null {
if (room.isSpaceRoom()) return undefined;
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(room.roomId); const dmPartner = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
// if weve got more than 2 users, dont treat it like a regular DM // if weve got more than 2 users, dont treat it like a regular DM
const isGroupDm = room.getMembers().length > 2; const isGroupDm = room.getMembers().length > 2;
if (dmPartner && !isGroupDm) { if (!room.isSpaceRoom() && dmPartner && !isGroupDm) {
return dmPartner; return { details: dmPartner };
} }
const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId); const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId);
if (secondParent && !otherParents?.length) { if (secondParent && !otherParents?.length) {
// exactly 2 edge case for improved i18n // exactly 2 edge case for improved i18n
return _t("%(space1Name)s and %(space2Name)s", { const space1Name = room.client.getRoom(parent)?.name;
space1Name: room.client.getRoom(parent)?.name, const space2Name = room.client.getRoom(secondParent)?.name;
space2Name: room.client.getRoom(secondParent)?.name, return {
}); details: _t("%(space1Name)s and %(space2Name)s", { space1Name, space2Name }),
ariaLabel: _t("In spaces %(space1Name)s and %(space2Name)s.", { space1Name, space2Name }),
};
} else if (parent) { } else if (parent) {
return _t("%(spaceName)s and %(count)s others", { const spaceName = room.client.getRoom(parent)?.name;
spaceName: room.client.getRoom(parent)?.name, const count = otherParents.length;
count: otherParents.length, return {
}); details: _t("%(spaceName)s and %(count)s others", { spaceName, count }),
ariaLabel: _t("In %(spaceName)s and %(count)s other spaces.", { spaceName, count }),
};
} }
return room.getCanonicalAlias(); return { details: room.getCanonicalAlias() };
} }