-
-
Notifications
You must be signed in to change notification settings - Fork 833
Draft: Add jump to date functionality to date headers in timeline #7317
Changes from 1 commit
594c7c7
23da673
9fa980c
e2d9751
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,21 +50,21 @@ limitations under the License. | |
} | ||
|
||
// round the top corners of the top button for the hover effect to be bounded | ||
&:first-child .mx_AccessibleButton:first-child { | ||
&:first-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid style clashes when trying to put a primary accessible button inside of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a reason not to do that, it won't be accessible for keyboard-only users. Context Menus have strict focus management requirements There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (new better flow suggestions welcome) 🙂 Popping off to a modal feels a little heavy. Tabbing to a date picker in the context menu seems in the realm of sane to handle for the keyboard. But the HTML5 date picker here isn't giving the best UX because ideally we could remove the "Go" button and "Go" automatically when a date is picked. This doesn't seem feasible though because there no way to distinguish a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tab in a context menu closes the context menu though as it is focus-locked There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can make this work. And seems acceptable under ARIA guidelines:
I had this mostly working in 9fa980c except for closing the menu, but I think I will try to adapt this to use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a The It also seems counter-intuitive to me that I'm unable to navigate menus by What's the goal of setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
See https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets Technique 1 It is so the app doesn't have a million tabstops, and instead each widget is a tabstop with sane and expected keyboard & focus management, e.g arrows. The roving & treeview stuff was written to match the wai-aria practices Switching to roving was asked for by a lot of our a11y community, who have only the keyboard to use the app with and if they have to press tab ten times to get through every menu that just makes any action they want to take that much slower. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #7336 fixes MAB, during a refactoring something got missed I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for looking into the fixes for the |
||
border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu | ||
} | ||
|
||
// round the bottom corners of the bottom button for the hover effect to be bounded | ||
&:last-child .mx_AccessibleButton:last-child { | ||
&:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):last-child { | ||
border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu | ||
} | ||
|
||
// round all corners of the only button for the hover effect to be bounded | ||
&:first-child:last-child .mx_AccessibleButton:first-child:last-child { | ||
&:first-child:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child:last-child { | ||
border-radius: 8px; // radius matches .mx_ContextualMenu | ||
} | ||
|
||
.mx_AccessibleButton { | ||
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { | ||
// pad the inside of the button so that the hover background is padded too | ||
padding-top: 12px; | ||
padding-bottom: 12px; | ||
|
@@ -130,7 +130,7 @@ limitations under the License. | |
} | ||
|
||
.mx_IconizedContextMenu_optionList_red { | ||
.mx_AccessibleButton { | ||
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { | ||
color: $alert !important; | ||
} | ||
|
||
|
@@ -148,7 +148,7 @@ limitations under the License. | |
} | ||
|
||
.mx_IconizedContextMenu_active { | ||
&.mx_AccessibleButton, .mx_AccessibleButton { | ||
&.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind), .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) { | ||
color: $accent !important; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,6 +127,7 @@ limitations under the License. | |
transform 0.25s ease-out 0s, | ||
background-color 0.25s ease-out 0s; | ||
font-size: $font-10px; | ||
line-height: normal; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reset |
||
transform: translateY(-13px); | ||
padding: 0 2px; | ||
background-color: $background; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,22 @@ import React from 'react'; | |
import { _t } from '../../../languageHandler'; | ||
import { formatFullDateNoTime } from '../../../DateUtils'; | ||
import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||
import { MatrixClientPeg } from '../../../MatrixClientPeg'; | ||
import { Direction } from 'matrix-js-sdk/src/models/event-timeline'; | ||
import dis from '../../../dispatcher/dispatcher'; | ||
import { Action } from '../../../dispatcher/actions'; | ||
|
||
import Field from "../elements/Field"; | ||
import Modal from '../../../Modal'; | ||
import ErrorDialog from '../dialogs/ErrorDialog'; | ||
import AccessibleButton from "../elements/AccessibleButton"; | ||
import { contextMenuBelow } from '../rooms/RoomTile'; | ||
import { ContextMenuTooltipButton } from "../../structures/ContextMenu"; | ||
import IconizedContextMenu, { | ||
IconizedContextMenuOption, | ||
IconizedContextMenuOptionList, | ||
IconizedContextMenuRadio, | ||
} from "../context_menus/IconizedContextMenu"; | ||
|
||
function getDaysArray(): string[] { | ||
return [ | ||
|
@@ -34,13 +50,26 @@ function getDaysArray(): string[] { | |
} | ||
|
||
interface IProps { | ||
roomId: string, | ||
ts: number; | ||
forExport?: boolean; | ||
} | ||
|
||
interface IState { | ||
dateValue: string, | ||
contextMenuPosition?: DOMRect | ||
} | ||
|
||
@replaceableComponent("views.messages.DateSeparator") | ||
export default class DateSeparator extends React.Component<IProps> { | ||
private getLabel() { | ||
export default class DateSeparator extends React.Component<IProps, IState> { | ||
constructor(props, context) { | ||
super(props, context); | ||
this.state = { | ||
dateValue: this.getDefaultDateValue() | ||
}; | ||
} | ||
|
||
private getLabel(): string { | ||
const date = new Date(this.props.ts); | ||
|
||
// During the time the archive is being viewed, a specific day might not make sense, so we return the full date | ||
|
@@ -62,12 +91,168 @@ export default class DateSeparator extends React.Component<IProps> { | |
} | ||
} | ||
|
||
private getDefaultDateValue(): string { | ||
const date = new Date(this.props.ts); | ||
const year = date.getFullYear(); | ||
const month = `${date.getMonth() + 1}`.padStart(2, "0") | ||
const day = `${date.getDate()}`.padStart(2, "0") | ||
|
||
return `${year}-${month}-${day}` | ||
} | ||
|
||
private pickDate = async (inputTimestamp): Promise<void> => { | ||
console.log('pickDate', inputTimestamp) | ||
|
||
const unixTimestamp = new Date(inputTimestamp).getTime(); | ||
|
||
const cli = MatrixClientPeg.get(); | ||
try { | ||
const roomId = this.props.roomId | ||
const { event_id, origin_server_ts } = await cli.timestampToEvent( | ||
roomId, | ||
unixTimestamp, | ||
Direction.Forward | ||
); | ||
console.log(`/timestamp_to_event: found ${event_id} (${origin_server_ts}) for timestamp=${unixTimestamp}`) | ||
|
||
dis.dispatch({ | ||
action: Action.ViewRoom, | ||
event_id, | ||
highlighted: true, | ||
room_id: roomId, | ||
}); | ||
} catch (e) { | ||
const code = e.errcode || e.statusCode; | ||
// only show the dialog if failing for something other than a network error | ||
// (e.g. no errcode or statusCode) as in that case the redactions end up in the | ||
// detached queue and we show the room status bar to allow retry | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Update comment, copy-paste artifact |
||
if (typeof code !== "undefined") { | ||
// display error message stating you couldn't delete this. | ||
Modal.createTrackedDialog('Unable to find event at that date', '', ErrorDialog, { | ||
title: _t('Error'), | ||
description: _t('Unable to find event at that date. (%(code)s)', { code }), | ||
}); | ||
} | ||
} | ||
}; | ||
|
||
private onDateValueChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>): void => { | ||
this.setState({ dateValue: e.target.value }); | ||
}; | ||
|
||
private onContextMenuOpenClick = (ev: React.MouseEvent): void => { | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
const target = ev.target as HTMLButtonElement; | ||
this.setState({ contextMenuPosition: target.getBoundingClientRect() }); | ||
}; | ||
|
||
private closeMenu = (): void => { | ||
this.setState({ | ||
contextMenuPosition: null, | ||
}); | ||
}; | ||
|
||
private onContextMenuCloseClick = (): void => { | ||
this.closeMenu(); | ||
}; | ||
|
||
private onLastWeekClicked = (): void => { | ||
const date = new Date(); | ||
// This just goes back 7 days. | ||
// FIXME: Do we want this to go back to the last Sunday? https://upokary.com/how-to-get-last-monday-or-last-friday-or-any-last-day-in-javascript/ | ||
date.setDate(date.getDate() - 7); | ||
this.pickDate(date); | ||
this.closeMenu(); | ||
} | ||
|
||
private onLastMonthClicked = (): void => { | ||
const date = new Date(); | ||
// Month numbers are 0 - 11 and `setMonth` handles the negative rollover | ||
date.setMonth(date.getMonth() - 1, 1); | ||
this.pickDate(date); | ||
this.closeMenu(); | ||
} | ||
|
||
private onTheBeginningClicked = (): void => { | ||
const date = new Date(0); | ||
this.pickDate(date); | ||
this.closeMenu(); | ||
} | ||
|
||
private onJumpToDateSubmit = (): void => { | ||
console.log('onJumpToDateSubmit') | ||
this.pickDate(this.state.dateValue); | ||
this.closeMenu(); | ||
} | ||
|
||
private renderNotificationsMenu(): React.ReactElement { | ||
let contextMenu: JSX.Element; | ||
if (this.state.contextMenuPosition) { | ||
contextMenu = <IconizedContextMenu | ||
{...contextMenuBelow(this.state.contextMenuPosition)} | ||
compact | ||
onFinished={this.onContextMenuCloseClick} | ||
> | ||
<IconizedContextMenuOptionList first> | ||
<IconizedContextMenuOption | ||
label={_t("Last week")} | ||
onClick={this.onLastWeekClicked} | ||
/> | ||
<IconizedContextMenuOption | ||
label={_t("Last month")} | ||
onClick={this.onLastMonthClicked} | ||
/> | ||
<IconizedContextMenuOption | ||
label={_t("The beginning of the room")} | ||
onClick={this.onTheBeginningClicked} | ||
/> | ||
</IconizedContextMenuOptionList> | ||
|
||
<IconizedContextMenuOptionList> | ||
<IconizedContextMenuOption | ||
className="mx_DateSeparator_jumpToDateMenuOption" | ||
label={_t("Jump to date")} | ||
onClick={() => {}} | ||
> | ||
<form className="mx_DateSeparator_datePickerForm" onSubmit={this.onJumpToDateSubmit}> | ||
<Field | ||
type="date" | ||
onChange={this.onDateValueChange} | ||
value={this.state.dateValue} | ||
className="mx_DateSeparator_datePicker" | ||
label={_t("Pick a date to jump to")} | ||
autoFocus={true} | ||
/> | ||
<AccessibleButton kind="primary" className="mx_DateSeparator_datePickerSubmitButton" onClick={this.onJumpToDateSubmit}> | ||
{ _t("Go") } | ||
</AccessibleButton> | ||
</form> | ||
</IconizedContextMenuOption> | ||
</IconizedContextMenuOptionList> | ||
</IconizedContextMenu>; | ||
} | ||
|
||
return ( | ||
<ContextMenuTooltipButton | ||
className="mx_DateSeparator_jumpToDateMenu" | ||
onClick={this.onContextMenuOpenClick} | ||
isExpanded={!!this.state.contextMenuPosition} | ||
title={_t("Jump to date")} | ||
> | ||
<div aria-hidden="true">{ this.getLabel() }</div> | ||
<div className="mx_DateSeparator_chevron" /> | ||
{ contextMenu } | ||
</ContextMenuTooltipButton> | ||
); | ||
} | ||
|
||
render() { | ||
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one | ||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers | ||
return <h2 className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={this.getLabel()}> | ||
return <h2 className="mx_DateSeparator" role="separator" aria-label={this.getLabel()}> | ||
<hr role="none" /> | ||
<div aria-hidden="true">{ this.getLabel() }</div> | ||
{ this.renderNotificationsMenu() } | ||
<hr role="none" /> | ||
</h2>; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HarHarLinks As separate iterations, the plan is to make the date headers sticky so one is always visible.
And add a slash command
/jumptodate 2021-12-10