Merge pull request #18255 from Scroody/I-17515

Enhancement: Breakout modal UI improved.
This commit is contained in:
Ramón Souza 2023-07-10 10:55:42 -03:00 committed by GitHub
commit eb26f0f0b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 234 additions and 181 deletions

View File

@ -173,6 +173,10 @@ const intlMessages = defineMessages({
id: 'app.createBreakoutRoom.movedUserLabel',
description: 'screen reader alert when users are moved to rooms',
},
manageRooms: {
id: 'app.createBreakoutRoom.manageRoomsLabel',
description: 'Label for manage rooms',
},
sendInvitationToMods: {
id: 'app.createBreakoutRoom.sendInvitationToMods',
description: 'label for checkbox send invitation to moderators',
@ -203,7 +207,7 @@ const setPresentationVisibility = (state) => {
if (presentationInnerWrapper) {
presentationInnerWrapper.style.display = state;
}
}
};
class BreakoutRoom extends PureComponent {
constructor(props) {
@ -223,7 +227,7 @@ class BreakoutRoom extends PureComponent {
this.renderUserItemByRoom = this.renderUserItemByRoom.bind(this);
this.renderRoomsGrid = this.renderRoomsGrid.bind(this);
this.renderBreakoutForm = this.renderBreakoutForm.bind(this);
this.renderCheckboxes = this.renderCheckboxes.bind(this);
this.renderCheckboxes = this.renderUnassingUsers.bind(this);
this.renderRoomSortList = this.renderRoomSortList.bind(this);
this.renderDesktop = this.renderDesktop.bind(this);
this.renderMobile = this.renderMobile.bind(this);
@ -251,7 +255,7 @@ class BreakoutRoom extends PureComponent {
roomNamesChanged: [],
roomSelected: 0,
preventClosing: true,
leastOneUserIsValid: true,
leastOneUserIsValid: null,
numberOfRoomsIsValid: true,
roomNameDuplicatedIsValid: true,
roomNameEmptyIsValid: true,
@ -324,6 +328,11 @@ class BreakoutRoom extends PureComponent {
}
}
const unassignedUsers = document.getElementById('breakoutBox-0');
if (unassignedUsers) {
unassignedUsers.addEventListener('keydown', this.handleMoveEvent, true);
}
const { numberOfRooms } = this.state;
const { users } = this.props;
const { users: prevUsers } = prevProps;
@ -348,6 +357,10 @@ class BreakoutRoom extends PureComponent {
roomList.removeEventListener('keydown', this.handleMoveEvent, true);
}
}
const unassignedUsers = document.getElementById('breakoutBox-0');
if (unassignedUsers) {
unassignedUsers.removeEventListener('keydown', this.handleMoveEvent, true);
}
}
handleShiftUser(activeListSibling) {
@ -361,6 +374,12 @@ class BreakoutRoom extends PureComponent {
this.changeUserRoom(u.userId, users[index].room);
}
});
} else {
users.forEach((u, index) => {
if (`roomUserItem-${u.userId}` === document.activeElement.id) {
this.changeUserRoom(u.userId, 0);
}
});
}
}
@ -556,6 +575,7 @@ class BreakoutRoom extends PureComponent {
onAssignRandomly() {
const { numberOfRooms } = this.state;
this.setState({ hasUsersAssign: false });
const { users } = this.state;
// We only want to assign viewers so filter out the moderators. We also want to get
// all users each run so that clicking the button again will reshuffle
@ -576,6 +596,7 @@ class BreakoutRoom extends PureComponent {
onAssignReset() {
const { users } = this.state;
this.setState({ hasUsersAssign: true });
users.forEach((u) => {
if (u.room !== null && u.room > 0) {
@ -841,6 +862,43 @@ class BreakoutRoom extends PureComponent {
});
}
renderContent() {
const { intl } = this.props;
const {
leastOneUserIsValid,
allowDrop,
} = this.state;
const drop = (room) => (ev) => {
ev.preventDefault();
const data = ev.dataTransfer.getData('text');
this.changeUserRoom(data, room);
this.setState({ selectedId: '' });
};
return (
<Styled.ContentContainer>
<Styled.Alert valid={leastOneUserIsValid} role="alert">
<Styled.FreeJoinLabel>
<Styled.BreakoutNameInput
type="text"
readOnly
value={
intl.formatMessage(intlMessages.notAssigned, { 0: this.getUserByRoom(0).length })
}
/>
</Styled.FreeJoinLabel>
<Styled.BreakoutBox hundred id="breakoutBox-0" onDrop={drop(0)} onDragOver={allowDrop} tabIndex={0}>
{this.renderUserItemByRoom(0)}
</Styled.BreakoutBox>
<Styled.SpanWarn data-test="warningNoUserAssigned" valid={leastOneUserIsValid}>
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
</Styled.SpanWarn>
</Styled.Alert>
{this.renderRoomsGrid()}
</Styled.ContentContainer>
);
}
renderRoomsGrid() {
const { intl, isUpdate } = this.props;
const {
@ -876,23 +934,6 @@ class BreakoutRoom extends PureComponent {
return (
<Styled.BoxContainer key="rooms-grid-" ref={(r) => { this.listOfUsers = r; }} data-test="roomGrid">
<Styled.Alert valid={leastOneUserIsValid} role="alert">
<Styled.FreeJoinLabel>
<Styled.BreakoutNameInput
type="text"
readOnly
value={
intl.formatMessage(intlMessages.notAssigned, { 0: this.getUserByRoom(0).length })
}
/>
</Styled.FreeJoinLabel>
<Styled.BreakoutBox id="breakoutBox-0" onDrop={drop(0)} onDragOver={allowDrop} tabIndex={0}>
{this.renderUserItemByRoom(0)}
</Styled.BreakoutBox>
<Styled.SpanWarn data-test="warningNoUserAssigned" valid={leastOneUserIsValid}>
{intl.formatMessage(intlMessages.leastOneWarnBreakout)}
</Styled.SpanWarn>
</Styled.Alert>
{
range(1, rooms + 1).map((value) => (
<div key={`room-${value}`}>
@ -937,12 +978,18 @@ class BreakoutRoom extends PureComponent {
const {
intl,
isUpdate,
isBreakoutRecordable,
} = this.props;
const {
numberOfRooms,
durationTime,
numberOfRoomsIsValid,
durationIsValid,
freeJoin,
record,
captureNotes,
captureSlides,
inviteMods,
} = this.state;
if (isUpdate) return null;
@ -980,42 +1027,6 @@ class BreakoutRoom extends PureComponent {
aria-label={intl.formatMessage(intlMessages.duration)}
data-test="durationTime"
/>
<Styled.HoldButtonWrapper
key="decrease-breakout-time"
exec={this.decreaseDurationTime}
minBound={MIN_BREAKOUT_ROOMS}
value={durationTime}
>
<Button
label={intl.formatMessage(intlMessages.minusRoomTime)}
aria-label={
`${intl.formatMessage(intlMessages.minusRoomTime)} ${intl.formatMessage(intlMessages.roomTime, { 0: durationTime - 1 })}`
}
icon="substract"
onClick={() => { }}
hideLabel
circle
size="sm"
data-test="decreaseBreakoutTime"
/>
</Styled.HoldButtonWrapper>
<Styled.HoldButtonWrapper
key="increase-breakout-time"
exec={this.increaseDurationTime}
>
<Button
label={intl.formatMessage(intlMessages.addRoomTime)}
aria-label={
`${intl.formatMessage(intlMessages.addRoomTime)} ${intl.formatMessage(intlMessages.roomTime, { 0: durationTime + 1 })}`
}
icon="add"
onClick={() => { }}
hideLabel
circle
size="sm"
data-test="increaseBreakoutTime"
/>
</Styled.HoldButtonWrapper>
</Styled.DurationArea>
<Styled.SpanWarn valid={durationIsValid}>
{
@ -1026,67 +1037,6 @@ class BreakoutRoom extends PureComponent {
}
</Styled.SpanWarn>
</Styled.DurationLabel>
<Styled.AssignBtnsContainer>
<Styled.AssignBtns
data-test="randomlyAssign"
label={intl.formatMessage(intlMessages.randomlyAssign)}
aria-describedby="randomlyAssignDesc"
onClick={this.onAssignRandomly}
size="sm"
color="default"
disabled={!numberOfRoomsIsValid}
/>
<Styled.AssignBtns
data-test="resetAssignments"
label={intl.formatMessage(intlMessages.resetAssignments)}
aria-describedby="resetAssignmentsDesc"
onClick={this.onAssignReset}
size="sm"
color="default"
disabled={!numberOfRoomsIsValid}
/>
</Styled.AssignBtnsContainer>
</Styled.BreakoutSettings>
<Styled.SpanWarn valid={numberOfRoomsIsValid}>
{intl.formatMessage(intlMessages.numberOfRoomsIsValid)}
</Styled.SpanWarn>
<span aria-hidden id="randomlyAssignDesc" className="sr-only">
{intl.formatMessage(intlMessages.randomlyAssignDesc)}
</span>
</React.Fragment>
);
}
renderSelectUserScreen() {
const {
users,
roomSelected,
} = this.state;
return (
<SortList
confirm={() => this.setState({ formFillLevel: 2 })}
users={users}
room={roomSelected}
onCheck={this.changeUserRoom}
onUncheck={(userId) => this.changeUserRoom(userId, 0)}
/>
);
}
renderCheckboxes() {
const {
intl, isUpdate, isBreakoutRecordable,
} = this.props;
if (isUpdate) return null;
const {
freeJoin,
record,
captureNotes,
captureSlides,
inviteMods,
} = this.state;
return (
<Styled.CheckBoxesContainer key="breakout-checkboxes">
<Styled.FreeJoinLabel htmlFor="freeJoinCheckbox" key="free-join-breakouts">
<Styled.FreeJoinCheckbox
@ -1146,7 +1096,6 @@ class BreakoutRoom extends PureComponent {
</Styled.FreeJoinLabel>
) : null
}
{
<Styled.FreeJoinLabel htmlFor="sendInvitationToAssignedModeratorsCheckbox" key="send-invitation-to-assigned-moderators-breakouts">
<Styled.FreeJoinCheckbox
id="sendInvitationToAssignedModeratorsCheckbox"
@ -1159,8 +1108,74 @@ class BreakoutRoom extends PureComponent {
{intl.formatMessage(intlMessages.sendInvitationToMods)}
</span>
</Styled.FreeJoinLabel>
}
</Styled.CheckBoxesContainer>
</Styled.BreakoutSettings>
<Styled.SpanWarn valid={numberOfRoomsIsValid}>
{intl.formatMessage(intlMessages.numberOfRoomsIsValid)}
</Styled.SpanWarn>
<span aria-hidden id="randomlyAssignDesc" className="sr-only">
{intl.formatMessage(intlMessages.randomlyAssignDesc)}
</span>
<Styled.Separator />
</React.Fragment>
);
}
renderSelectUserScreen() {
const {
users,
roomSelected,
} = this.state;
return (
<SortList
confirm={() => this.setState({ formFillLevel: 2 })}
users={users}
room={roomSelected}
onCheck={this.changeUserRoom}
onUncheck={(userId) => this.changeUserRoom(userId, 0)}
/>
);
}
renderUnassingUsers() {
const {
intl,
isUpdate,
} = this.props;
if (isUpdate) return null;
const {
numberOfRoomsIsValid,
leastOneUserIsValid,
} = this.state;
return (
<Styled.AssignBtnsContainer>
<Styled.LabelText bold aria-hidden>
{intl.formatMessage(intlMessages.manageRooms)}
</Styled.LabelText>
{leastOneUserIsValid ? (
<Styled.AssignBtns
data-test="resetAssignments"
label={intl.formatMessage(intlMessages.resetAssignments)}
aria-describedby="resetAssignmentsDesc"
onClick={this.onAssignReset}
size="sm"
color="default"
disabled={!numberOfRoomsIsValid}
/>
) : (
<Styled.AssignBtns
random
data-test="randomlyAssign"
label={intl.formatMessage(intlMessages.randomlyAssign)}
aria-describedby="randomlyAssignDesc"
onClick={this.onAssignRandomly}
size="sm"
color="default"
/>
)}
</Styled.AssignBtnsContainer>
);
}
@ -1288,8 +1303,8 @@ class BreakoutRoom extends PureComponent {
renderDesktop() {
return [
this.renderBreakoutForm(),
this.renderCheckboxes(),
this.renderRoomsGrid(),
this.renderUnassingUsers(),
this.renderContent(),
];
}
@ -1313,7 +1328,7 @@ class BreakoutRoom extends PureComponent {
return [
this.renderErrorMessages(),
this.renderBreakoutForm(),
this.renderCheckboxes(),
this.renderUnassingUsers(),
this.renderButtonSetLevel(2, intl.formatMessage(intlMessages.nextLabel)),
];
}

View File

@ -25,14 +25,23 @@ import {
const BoxContainer = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
grid-gap: 1.6rem 1rem;
box-sizing: border-box;
padding-bottom: 1rem;
`;
const ContentContainer = styled.div`
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-areas: "sidebar content";
grid-gap: 1.5rem;
`;
const Alert = styled.div`
${({ valid }) => !valid && `
grid-area: sidebar;
margin-bottom: 2.5rem;
${({ valid }) => valid === false && `
position: relative;
& > * {
@ -61,15 +70,16 @@ const FreeJoinLabel = styled.label`
const BreakoutNameInput = styled.input`
width: 100%;
text-align: left;
text-align: center;
font-weight: 600;
padding: .25rem .25rem .25rem 0;
margin: 0;
border: none;
&::placeholder {
color: ${colorGray};
opacity: 1;
}
border: 1px solid ${colorGrayLightest};
margin-bottom: 1rem;
${({ readOnly }) => readOnly && `
cursor: default;
@ -78,10 +88,14 @@ const BreakoutNameInput = styled.input`
const BreakoutBox = styled(ScrollboxVertical)`
width: 100%;
height: 21rem;
height: 10rem;
border: 1px solid ${colorGrayLightest};
border-radius: ${borderRadius};
padding: ${lgPaddingY} 0;
${({ hundred }) => hundred && `
height: 100%;
`}
`;
const SpanWarn = styled.span`
@ -111,9 +125,9 @@ const RoomName = styled(BreakoutNameInput)`
const BreakoutSettings = styled.div`
display: grid;
grid-template-columns: 2fr 2fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr;
grid-gap: 1rem;
grid-gap: 4rem;
@media ${smallOnly} {
grid-template-columns: 1fr ;
@ -160,6 +174,11 @@ const LabelText = styled.p`
color: ${colorGray};
white-space: nowrap;
margin-bottom: .5rem;
${({ bold }) => bold && `
font-weight: bold;
font-size: 1.5rem;
`}
`;
const DurationArea = styled.div`
@ -173,13 +192,13 @@ const DurationInput = styled.input`
color: ${colorGray};
border: 1px solid ${colorGrayLighter};
border-radius: ${borderRadius};
width: 50%;
text-align: center;
width: 100%;
text-align: left;
padding: .25rem;
&::placeholder {
color: ${colorGray};
opacity: 1;
}
`;
@ -197,21 +216,36 @@ const HoldButtonWrapper = styled(HoldButton)`
`;
const AssignBtnsContainer = styled.div`
justify-items: center;
display: flex;
flex-flow: row;
align-items: baseline;
margin-top: auto;
`;
const AssignBtns = styled(Button)`
color: ${colorPrimary};
color: ${colorDanger};
font-size: ${fontSizeSmall};
white-space: nowrap;
margin: 3px auto;
align-self: flex-end;
width: 100%;
margin-bottom: 0.5rem;
${({ random }) => random && `
color: ${colorPrimary};
`}
`;
const CheckBoxesContainer = styled(FlexRow)`
margin-top: 2rem;
margin-bottom: 0.25rem;
white-space: nowrap;
display: flex;
flex-flow: column;
justify-content: flex-end;
`;
const Separator = styled.div`
width: 100%;
height: 1px;
margin: 1rem 0;
border: 1px solid ${colorGrayLightest};
`;
const FreeJoinCheckbox = styled.input`
@ -331,6 +365,7 @@ export default {
AssignBtnsContainer,
AssignBtns,
CheckBoxesContainer,
Separator,
FreeJoinCheckbox,
RoomUserItem,
LockIcon,
@ -341,4 +376,5 @@ export default {
WithError,
SubTitle,
Content,
ContentContainer,
};

View File

@ -22,12 +22,12 @@ const FullscreenModal = styled(Styled.BaseModal)`
outline-style: solid;
display: flex;
flex-direction: column;
height: 100%;
align-self: flex-start;
padding: calc(${lineHeightComputed} / 2) ${lineHeightComputed};
@media ${smallOnly} {
width: 100%;
height: 100%;
}
`;

View File

@ -7,6 +7,7 @@
"app.chat.locked": "Chat is locked, messages can't be sent",
"app.chat.inputLabel": "Message input for chat {0}",
"app.chat.emojiButtonLabel": "Emoji Picker",
"app.chat.loadMoreButtonLabel": "Load More",
"app.chat.inputPlaceholder": "Message {0}",
"app.chat.titlePublic": "Public Chat",
"app.chat.titlePrivate": "Private Chat with {0}",
@ -1175,7 +1176,7 @@
"app.createBreakoutRoom.durationInMinutes": "Duration (minutes)",
"app.createBreakoutRoom.randomlyAssign": "Randomly assign",
"app.createBreakoutRoom.randomlyAssignDesc": "Assigns users randomly to breakout rooms",
"app.createBreakoutRoom.resetAssignments": "Reset assignments",
"app.createBreakoutRoom.resetAssignments": "Unassign Users",
"app.createBreakoutRoom.resetAssignmentsDesc": "Reset all user room assignments",
"app.createBreakoutRoom.endAllBreakouts": "End all breakout rooms",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "all rooms",
@ -1186,13 +1187,14 @@
"app.createBreakoutRoom.minusRoomTime": "Decrease breakout room time to",
"app.createBreakoutRoom.addRoomTime": "Increase breakout room time to",
"app.createBreakoutRoom.addParticipantLabel": "+ Add participant",
"app.createBreakoutRoom.freeJoin": "Allow users to choose a breakout room to join",
"app.createBreakoutRoom.captureNotes": "Capture shared notes when breakout rooms end",
"app.createBreakoutRoom.freeJoin": "Allow users to choose rooms",
"app.createBreakoutRoom.manageRoomsLabel": "Manage Rooms",
"app.createBreakoutRoom.captureNotes": "Save shared notes",
"app.createBreakoutRoom.captureSlides": "Save whiteboard",
"app.createBreakoutRoom.sendInvitationToMods": "Send invitation to assigned moderators",
"app.createBreakoutRoom.captureSlides": "Capture whiteboard when breakout rooms end",
"app.createBreakoutRoom.leastOneWarnBreakout": "You must place at least one user in a breakout room.",
"app.createBreakoutRoom.minimumDurationWarnBreakout": "Minimum duration for a breakout room is {0} minutes.",
"app.createBreakoutRoom.modalDesc": "Tip: You can drag-and-drop a user's name to assign them to a specific breakout room.",
"app.createBreakoutRoom.modalDesc": "Complete the steps below to create breakout rooms in your session. To add participants to a room, simply drag their name to the desired room.",
"app.createBreakoutRoom.roomTime": "{0} minutes",
"app.createBreakoutRoom.numberOfRoomsError": "The number of rooms is invalid.",
"app.createBreakoutRoom.duplicatedRoomNameError": "Room name can't be duplicated.",