Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 7fa27f5

Browse files
1 parent efa1667 commit 7fa27f5

File tree

17 files changed

+630
-44
lines changed

17 files changed

+630
-44
lines changed

res/css/_components.scss

+1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
@import "./views/messages/_CallEvent.scss";
184184
@import "./views/messages/_CreateEvent.scss";
185185
@import "./views/messages/_DateSeparator.scss";
186+
@import "./views/messages/_JumpToDatePicker.scss";
186187
@import "./views/messages/_EventTileBubble.scss";
187188
@import "./views/messages/_HiddenBody.scss";
188189
@import "./views/messages/_MEmoteBody.scss";

res/css/views/context_menus/_IconizedContextMenu.scss

+6-6
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,21 @@ limitations under the License.
5050
}
5151

5252
// round the top corners of the top button for the hover effect to be bounded
53-
&:first-child .mx_AccessibleButton:first-child {
53+
&:first-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child {
5454
border-radius: 8px 8px 0 0; // radius matches .mx_ContextualMenu
5555
}
5656

5757
// round the bottom corners of the bottom button for the hover effect to be bounded
58-
&:last-child .mx_AccessibleButton:last-child {
58+
&:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):last-child {
5959
border-radius: 0 0 8px 8px; // radius matches .mx_ContextualMenu
6060
}
6161

6262
// round all corners of the only button for the hover effect to be bounded
63-
&:first-child:last-child .mx_AccessibleButton:first-child:last-child {
63+
&:first-child:last-child .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind):first-child:last-child {
6464
border-radius: 8px; // radius matches .mx_ContextualMenu
6565
}
6666

67-
.mx_AccessibleButton {
67+
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
6868
// pad the inside of the button so that the hover background is padded too
6969
padding-top: 12px;
7070
padding-bottom: 12px;
@@ -130,7 +130,7 @@ limitations under the License.
130130
}
131131

132132
.mx_IconizedContextMenu_optionList_red {
133-
.mx_AccessibleButton {
133+
.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
134134
color: $alert !important;
135135
}
136136

@@ -148,7 +148,7 @@ limitations under the License.
148148
}
149149

150150
.mx_IconizedContextMenu_active {
151-
&.mx_AccessibleButton, .mx_AccessibleButton {
151+
&.mx_AccessibleButton:not(.mx_AccessibleButton_hasKind), .mx_AccessibleButton:not(.mx_AccessibleButton_hasKind) {
152152
color: $accent !important;
153153
}
154154

res/css/views/messages/_DateSeparator.scss

+15
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,18 @@ limitations under the License.
3333
margin: 0 25px;
3434
flex: 0 0 auto;
3535
}
36+
37+
.mx_DateSeparator_jumpToDateMenu {
38+
display: flex;
39+
}
40+
41+
.mx_DateSeparator_chevron {
42+
align-self: center;
43+
width: 16px;
44+
height: 16px;
45+
mask-position: center;
46+
mask-size: contain;
47+
mask-repeat: no-repeat;
48+
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
49+
background-color: $tertiary-content;
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_JumpToDatePicker_form {
18+
display: flex;
19+
}
20+
21+
.mx_JumpToDatePicker_label {
22+
align-self: center;
23+
font-size: $font-15px;
24+
}
25+
26+
.mx_JumpToDatePicker_datePicker {
27+
margin: 0;
28+
margin-left: 8px;
29+
30+
&, & > input {
31+
border-radius: 8px;
32+
}
33+
}
34+
35+
.mx_JumpToDatePicker_submitButton {
36+
margin-left: 8px;
37+
}

src/components/structures/MessagePanel.tsx

+10-6
Original file line numberDiff line numberDiff line change
@@ -721,8 +721,12 @@ export default class MessagePanel extends React.Component<IProps, IState> {
721721

722722
// do we need a date separator since the last event?
723723
const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate);
724-
if (wantsDateSeparator && !isGrouped) {
725-
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
724+
if (wantsDateSeparator && !isGrouped && this.props.room) {
725+
const dateSeparator = (
726+
<li key={ts1}>
727+
<DateSeparator key={ts1} roomId={this.props.room.roomId} ts={ts1} />
728+
</li>
729+
);
726730
ret.push(dateSeparator);
727731
}
728732

@@ -1118,7 +1122,7 @@ class CreationGrouper extends BaseGrouper {
11181122
if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
11191123
const ts = createEvent.getTs();
11201124
ret.push(
1121-
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
1125+
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={createEvent.getRoomId()} ts={ts} /></li>,
11221126
);
11231127
}
11241128

@@ -1231,7 +1235,7 @@ class RedactionGrouper extends BaseGrouper {
12311235
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
12321236
const ts = this.events[0].getTs();
12331237
ret.push(
1234-
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
1238+
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
12351239
);
12361240
}
12371241

@@ -1327,7 +1331,7 @@ class MemberGrouper extends BaseGrouper {
13271331
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
13281332
const ts = this.events[0].getTs();
13291333
ret.push(
1330-
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
1334+
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
13311335
);
13321336
}
13331337

@@ -1429,7 +1433,7 @@ class HiddenEventGrouper extends BaseGrouper {
14291433
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
14301434
const ts = this.events[0].getTs();
14311435
ret.push(
1432-
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
1436+
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
14331437
);
14341438
}
14351439

src/components/views/dialogs/MessageEditHistoryDialog.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
130130
const baseEventId = this.props.mxEvent.getId();
131131
allEvents.forEach((e, i) => {
132132
if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) {
133-
nodes.push(<li key={e.getTs() + "~"}><DateSeparator ts={e.getTs()} /></li>);
133+
nodes.push(<li key={e.getTs() + "~"}><DateSeparator roomId={e.getRoomId()} ts={e.getTs()} /></li>);
134134
}
135135
const isBaseEvent = e.getId() === baseEventId;
136136
nodes.push((

src/components/views/elements/Field.tsx

+46-21
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
17+
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject } from 'react';
1818
import classNames from 'classnames';
1919
import { debounce } from "lodash";
2020

2121
import * as sdk from '../../../index';
2222
import { IFieldState, IValidationResult } from "./Validation";
23+
import { ComponentClass } from "../../../@types/common";
2324

2425
// Invoke validation from user input (when typing, etc.) at most once every N ms.
2526
const VALIDATION_THROTTLE_MS = 200;
@@ -78,26 +79,45 @@ interface IProps {
7879
}
7980

8081
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
82+
// The ref pass through to the input
83+
inputRef?: RefObject<HTMLInputElement>;
8184
// The element to create. Defaults to "input".
8285
element?: "input";
86+
componentClass?: undefined;
8387
// The input's value. This is a controlled component, so the value is required.
8488
value: string;
8589
}
8690

8791
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
92+
// The ref pass through to the select
93+
inputRef?: RefObject<HTMLSelectElement>;
8894
// To define options for a select, use <Field><option ... /></Field>
8995
element: "select";
96+
componentClass?: undefined;
9097
// The select's value. This is a controlled component, so the value is required.
9198
value: string;
9299
}
93100

94101
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
102+
// The ref pass through to the textarea
103+
inputRef?: RefObject<HTMLTextAreaElement>;
95104
element: "textarea";
105+
componentClass?: undefined;
96106
// The textarea's value. This is a controlled component, so the value is required.
97107
value: string;
98108
}
99109

100-
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
110+
export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
111+
// The ref pass through to the input
112+
inputRef?: RefObject<HTMLInputElement>;
113+
element: "input";
114+
// The custom component to render
115+
componentClass: ComponentClass;
116+
// The input's value. This is a controlled component, so the value is required.
117+
value: string;
118+
}
119+
120+
type PropShapes = IInputProps | ISelectProps | ITextareaProps | INativeOnChangeInputProps;
101121

102122
interface IState {
103123
valid: boolean;
@@ -108,7 +128,7 @@ interface IState {
108128

109129
export default class Field extends React.PureComponent<PropShapes, IState> {
110130
private id: string;
111-
private input: HTMLInputElement;
131+
private inputRef: RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
112132

113133
public static readonly defaultProps = {
114134
element: "input",
@@ -146,7 +166,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
146166
}
147167

148168
public focus() {
149-
this.input.focus();
169+
this.inputRef.current?.focus();
150170
// programmatic does not fire onFocus handler
151171
this.setState({
152172
focused: true,
@@ -197,7 +217,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
197217
if (!this.props.onValidate) {
198218
return;
199219
}
200-
const value = this.input ? this.input.value : null;
220+
const value = this.inputRef.current?.value ?? null;
201221
const { valid, feedback } = await this.props.onValidate({
202222
value,
203223
focused,
@@ -228,13 +248,13 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
228248

229249
public render() {
230250
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
231-
const { element, prefixComponent, postfixComponent, className, onValidate, children,
251+
const { element, componentClass, inputRef, prefixComponent, postfixComponent, className, onValidate, children,
232252
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
233253
usePlaceholderAsHint, forceTooltipVisible,
234254
...inputProps } = this.props;
235255

236-
// Set some defaults for the <input> element
237-
const ref = input => this.input = input;
256+
this.inputRef = inputRef || React.createRef();
257+
238258
inputProps.placeholder = inputProps.placeholder || inputProps.label;
239259
inputProps.id = this.id; // this overwrites the id from props
240260

@@ -243,9 +263,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
243263
inputProps.onBlur = this.onBlur;
244264

245265
// Appease typescript's inference
246-
const inputProps_ = { ...inputProps, ref, list };
266+
const inputProps_ = { ...inputProps, ref: this.inputRef, list };
247267

248-
const fieldInput = React.createElement(this.props.element, inputProps_, children);
268+
const fieldInput = React.createElement(this.props.componentClass || this.props.element, inputProps_, children);
249269

250270
let prefixContainer = null;
251271
if (prefixComponent) {
@@ -257,17 +277,22 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
257277
}
258278

259279
const hasValidationFlag = forceValidity !== null && forceValidity !== undefined;
260-
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
261-
// If we have a prefix element, leave the label always at the top left and
262-
// don't animate it, as it looks a bit clunky and would add complexity to do
263-
// properly.
264-
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
265-
mx_Field_placeholderIsHint: usePlaceholderAsHint,
266-
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
267-
mx_Field_invalid: hasValidationFlag
268-
? !forceValidity
269-
: onValidate && this.state.valid === false,
270-
});
280+
const fieldClasses = classNames(
281+
"mx_Field",
282+
`mx_Field_${this.props.element}`,
283+
className,
284+
{
285+
// If we have a prefix element, leave the label always at the top left and
286+
// don't animate it, as it looks a bit clunky and would add complexity to do
287+
// properly.
288+
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
289+
mx_Field_placeholderIsHint: usePlaceholderAsHint,
290+
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
291+
mx_Field_invalid: hasValidationFlag
292+
? !forceValidity
293+
: onValidate && this.state.valid === false,
294+
},
295+
);
271296

272297
// Handle displaying feedback on validity
273298
// FIXME: Using an import will result in test failures
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
19+
import { useCombinedRefs } from "../../../hooks/useCombinedRefs";
20+
21+
interface IProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onInput'> {
22+
onChange?: (event: Event) => void;
23+
onInput?: (event: Event) => void;
24+
}
25+
26+
/**
27+
* This component restores the native 'onChange' and 'onInput' behavior of
28+
* JavaScript which have important differences for certain <input> types. This is
29+
* necessary because in React, the `onChange` handler behaves like the native
30+
* `oninput` handler and there is no way to tell the difference between an
31+
* `input` vs `change` event.
32+
*
33+
* via https://stackoverflow.com/a/62383569/796832 and
34+
* https://github.com/facebook/react/issues/9657#issuecomment-643970199
35+
*
36+
* See:
37+
* - https://reactjs.org/docs/dom-elements.html#onchange
38+
* - https://github.com/facebook/react/issues/3964
39+
* - https://github.com/facebook/react/issues/9657
40+
* - https://github.com/facebook/react/issues/14857
41+
*
42+
* Examples:
43+
*
44+
* We use this for the <input type="date"> date picker so we can distinguish from
45+
* a final date picker selection (onChange) vs navigating the months in the date
46+
* picker (onInput).
47+
*
48+
* This is also potentially useful for <input type="range" /> because the native
49+
* events behave in such a way that moving the slider around triggers an onInput
50+
* event and releasing it triggers onChange.
51+
*/
52+
const NativeOnChangeInput: React.FC<IProps> = React.forwardRef((props: IProps, ref) => {
53+
const registerCallbacks = (input: HTMLInputElement | null) => {
54+
if (input) {
55+
input.onchange = props.onChange;
56+
input.oninput = props.onInput;
57+
}
58+
};
59+
60+
return <input
61+
ref={useCombinedRefs(registerCallbacks, ref)}
62+
{...props}
63+
// These are just here so we don't get a read-only input warning from React
64+
onChange={() => {}}
65+
onInput={() => {}}
66+
/>;
67+
});
68+
69+
export default NativeOnChangeInput;

0 commit comments

Comments
 (0)