From 5cab787de63730e0d1e9e853f505fc98ad3e7e83 Mon Sep 17 00:00:00 2001 From: Ole Wieners Date: Thu, 20 Oct 2022 16:05:03 +0200 Subject: [PATCH 01/13] Allow multiple selections in polls This adds an option to set the `maxSelections` parameter in the `createPollDialog` and allows users to vote for more than one answer. --- res/css/_components.pcss | 1 + res/css/views/dialogs/_PollCreateDialog.pcss | 3 + .../views/elements/_StyledPollCheckbox.pcss | 126 +++++++++++++++ res/css/views/messages/_MPollBody.pcss | 20 ++- .../views/elements/PollCreateDialog.tsx | 30 ++++ .../views/elements/StyledPollCheckbox.tsx | 65 ++++++++ src/components/views/messages/MPollBody.tsx | 147 ++++++++++++++---- src/i18n/strings/en_EN.json | 4 + .../views/elements/PollCreateDialog-test.tsx | 35 ++++- .../PollCreateDialog-test.tsx.snap | 6 +- .../views/messages/MPollBody-test.tsx | 105 ++++++++++++- .../__snapshots__/MPollBody-test.tsx.snap | 39 +++++ 12 files changed, 534 insertions(+), 47 deletions(-) create mode 100644 res/css/views/elements/_StyledPollCheckbox.pcss create mode 100644 src/components/views/elements/StyledPollCheckbox.tsx diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 5a263aa1e96..3a0fcc7aeeb 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -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"; diff --git a/res/css/views/dialogs/_PollCreateDialog.pcss b/res/css/views/dialogs/_PollCreateDialog.pcss index b49fda4db06..7db8b01715c 100644 --- a/res/css/views/dialogs/_PollCreateDialog.pcss +++ b/res/css/views/dialogs/_PollCreateDialog.pcss @@ -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 */ } diff --git a/res/css/views/elements/_StyledPollCheckbox.pcss b/res/css/views/elements/_StyledPollCheckbox.pcss new file mode 100644 index 00000000000..cd050ec44a9 --- /dev/null +++ b/res/css/views/elements/_StyledPollCheckbox.pcss @@ -0,0 +1,126 @@ +/* +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; +} diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index 7eeb7e2518c..e90e176edc2 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -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; } @@ -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; diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index 3d4cc9826f2..af314b28f17 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -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"; @@ -51,6 +52,7 @@ interface IState extends IScrollableBaseState { question: string; options: string[]; busy: boolean; + max_selections?: number; kind: KNOWN_POLL_KIND; autoFocusTarget: FocusTarget; } @@ -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, }; @@ -138,11 +141,25 @@ export default class PollCreateDialog extends ScrollableBaseModal { + const count = range(1, this.state.options.length + 1); + const options = count.map((number) => { + return ; + }); + return options; + }; + private createEvent(): IPartialEvent { 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) { @@ -270,6 +287,15 @@ export default class PollCreateDialog extends ScrollableBaseModal } +

{ _t("Number of allowed selections") }

+ + { this.maxSelections() } + ; } @@ -282,6 +308,10 @@ export default class PollCreateDialog extends ScrollableBaseModal) => { + this.setState({ max_selections: Number(e.target.value) }); + }; } function pollTypeNotes(kind: KNOWN_POLL_KIND): string { diff --git a/src/components/views/elements/StyledPollCheckbox.tsx b/src/components/views/elements/StyledPollCheckbox.tsx new file mode 100644 index 00000000000..c71ca74bd14 --- /dev/null +++ b/src/components/views/elements/StyledPollCheckbox.tsx @@ -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 { + inputRef?: React.RefObject; + outlined?: boolean; +} + +interface IState { +} + +export default class StyledPollCheckbox extends React.PureComponent { + 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 = + + { /* Used to render the radio button circle */ } +
+ ; + + return