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

Commit

Permalink
Allow multiple selections in polls
Browse files Browse the repository at this point in the history
This adds an option to set the `maxSelections` parameter in the `createPollDialog`
and allows users to vote for more than one answer.
  • Loading branch information
owi92 committed Nov 14, 2022
1 parent 18c03da commit dda1a70
Show file tree
Hide file tree
Showing 12 changed files with 553 additions and 65 deletions.
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
@import "./views/elements/_Slider.pcss";
@import "./views/elements/_Spinner.pcss";
@import "./views/elements/_StyledCheckbox.pcss";
@import "./views/elements/_StyledPollCheckbox.pcss";
@import "./views/elements/_StyledRadioButton.pcss";
@import "./views/elements/_SyntaxHighlight.pcss";
@import "./views/elements/_TagComposer.pcss";
Expand Down
3 changes: 3 additions & 0 deletions res/css/views/dialogs/_PollCreateDialog.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ limitations under the License.

.mx_PollCreateDialog_addOption {
padding: 0;
}

.mx_PollCreateDialog_maxSelections {
margin-bottom: 40px; /* arbitrary to create scrollable area under the poll */
}

Expand Down
127 changes: 127 additions & 0 deletions res/css/views/elements/_StyledPollCheckbox.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2022 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.
*/

/**
* This component expects the parent to specify a positive padding and
* width.
* It is used for multiple selection polls and mostly copied
* from _StyledRadioButton.pcss to match it's style.
*/

.mx_StyledPollCheckbox {
$radio-circle-color: $quaternary-content;
$active-radio-circle-color: $accent;
position: relative;

display: flex;
align-items: baseline;
flex-grow: 1;

> .mx_StyledPollCheckbox_content {
flex-grow: 1;

display: flex;
flex-direction: column;

margin-left: 8px;
margin-right: 8px;
}

.mx_StyledPollCheckbox_spacer {
flex-shrink: 0;
flex-grow: 0;

height: $font-16px;
width: $font-16px;
}

input[type="checkbox"] {
/* Remove the OS's representation */
margin: 0;
padding: 0;
appearance: none;

+ div {
flex-shrink: 0;
flex-grow: 0;

display: flex;
align-items: center;
justify-content: center;

box-sizing: border-box;
height: $font-16px;
width: $font-16px;
margin-left: 2px; /* For the highlight on focus */

border: $font-1-5px solid $radio-circle-color;
border-radius: $font-16px;

> div {
box-sizing: border-box;

height: $font-8px;
width: $font-8px;

border-radius: $font-8px;
}
}

&.focus-visible {
& + div {
@mixin unreal-focus;
}
}

&:checked {
& + div {
border-color: $active-radio-circle-color;

& > div {
background: $active-radio-circle-color;
}
}
}

&:disabled {
& + div,
& + div + span {
opacity: 0.5;
cursor: not-allowed;
}

& + div {
border-color: $radio-circle-color;
}
}

&:checked:disabled {
& + div > div {
background-color: $radio-circle-color;
}
}

}
}

.mx_StyledPollCheckbox_outlined {
border: 1px solid $input-darker-bg-color;
border-radius: 8px;
}

.mx_StyledPollCheckbox_checked {
border-color: $accent;
}
20 changes: 14 additions & 6 deletions res/css/views/messages/_MPollBody.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,21 @@ limitations under the License.
max-width: 550px;
background-color: $background;

.mx_StyledRadioButton, .mx_MPollBody_endedOption {
.mx_StyledRadioButton,
.mx_StyledPollCheckbox,
.mx_MPollBody_endedOption {
margin-bottom: 8px;
}

.mx_StyledRadioButton_content, .mx_MPollBody_endedOption {
.mx_StyledRadioButton_content,
.mx_StyledPollCheckbox_content,
.mx_MPollBody_endedOption {
padding-top: 2px;
margin-right: 0px;
}

.mx_StyledRadioButton_spacer {
.mx_StyledRadioButton_spacer,
.mx_StyledPollCheckbox_spacer {
display: none;
}

Expand Down Expand Up @@ -110,12 +115,15 @@ limitations under the License.
}

/* options not actionable in these states */
.mx_MPollBody_option_checked, .mx_MPollBody_option_ended {
.mx_MPollBody_option_ended {
pointer-events: none;
}

.mx_StyledRadioButton_checked, .mx_MPollBody_endedOptionWinner {
input[type="radio"] + div {
.mx_StyledRadioButton_checked,
.mx_StyledPollCheckbox_checked,
.mx_MPollBody_endedOptionWinner {
input[type="radio"] + div,
input[type="checkbox"] + div {
border-width: 2px;
border-color: $accent;
background-color: $accent;
Expand Down
30 changes: 30 additions & 0 deletions src/components/views/elements/PollCreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
PollStartEvent,
} from "matrix-events-sdk";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { range } from "lodash";

import ScrollableBaseModal, { IScrollableBaseState } from "../dialogs/ScrollableBaseModal";
import { IDialogProps } from "../dialogs/IDialogProps";
Expand All @@ -51,6 +52,7 @@ interface IState extends IScrollableBaseState {
question: string;
options: string[];
busy: boolean;
max_selections?: number;
kind: KNOWN_POLL_KIND;
autoFocusTarget: FocusTarget;
}
Expand Down Expand Up @@ -85,6 +87,7 @@ function editingInitialState(editingMxEvent: MatrixEvent): IState {
question: poll.question.text,
options: poll.answers.map(ans => ans.text),
busy: false,
max_selections: poll.maxSelections,
kind: poll.kind,
autoFocusTarget: FocusTarget.Topic,
};
Expand Down Expand Up @@ -138,11 +141,25 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
});
};

private maxSelections = () => {
const count = range(1, this.state.options.length + 1);
const options = count.map((number) => {
return <option
key={number}
value={number}
>
{ number }
</option>;
});
return options;
};

private createEvent(): IPartialEvent<object> {
const pollStart = PollStartEvent.from(
this.state.question.trim(),
this.state.options.map(a => a.trim()).filter(a => !!a),
this.state.kind,
this.state.max_selections,
).serialize();

if (!this.props.editingMxEvent) {
Expand Down Expand Up @@ -270,6 +287,15 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
this.state.busy &&
<div className="mx_PollCreateDialog_busy"><Spinner /></div>
}
<h2>{ _t("Number of allowed selections") }</h2>
<Field
className="mx_PollCreateDialog_maxSelections"
element="select"
value={String(this.state.max_selections)}
onChange={this.onMaxSelectionsChange}
>
{ this.maxSelections() }
</Field>
</div>;
}

Expand All @@ -282,6 +308,10 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
),
});
};

onMaxSelectionsChange = (e: ChangeEvent<HTMLSelectElement>) => {
this.setState({ max_selections: Number(e.target.value) });
};
}

function pollTypeNotes(kind: KNOWN_POLL_KIND): string {
Expand Down
65 changes: 65 additions & 0 deletions src/components/views/elements/StyledPollCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2022 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.
*/

/* This component is mostly copied from StyledRadioButton.tsx to match it's appearance in polls */

import React from "react";
import classnames from 'classnames';

interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
inputRef?: React.RefObject<HTMLInputElement>;
outlined?: boolean;
}

interface IState {
}

export default class StyledPollCheckbox extends React.PureComponent<IProps, IState> {
public static readonly defaultProps = {
className: '',
};

public render() {
const { children, className, disabled, outlined, inputRef, ...otherProps } = this.props;
const _className = classnames(
'mx_StyledPollCheckbox',
className,
{
"mx_StyledPollCheckbox_disabled": disabled,
"mx_StyledPollCheckbox_enabled": !disabled,
"mx_StyledPollCheckbox_checked": this.props.checked,
"mx_StyledPollCheckbox_outlined": outlined,
});

const checkbox = <React.Fragment>
<input
// Pass through the ref - used for keyboard shortcut access to some buttons
ref={inputRef}
type='checkbox'
disabled={disabled}
{...otherProps}
/>
{ /* Used to render the radio button circle */ }
<div><div /></div>
</React.Fragment>;

return <label className={_className}>
{ checkbox }
<div className="mx_StyledPollCheckbox_content">{ children }</div>
<div className="mx_StyledPollCheckbox_spacer" />
</label>;
}
}
Loading

0 comments on commit dda1a70

Please sign in to comment.