From 82bf81da5bd698e6f3debc2f0d5eae42e2d9f88e Mon Sep 17 00:00:00 2001 From: Dmitry Yudakov Date: Fri, 10 Feb 2023 16:33:49 +0200 Subject: [PATCH] feat: align elements in different screen sections We support several screen elements that need to be aligned together: maximized elements on top-left (like video/screenshare/plugins), top-right elements (voice and text chats) and previews on bottom-right (video/plugins/screencast). Video/screencast/plugins can also move between top-left and bottom-right - the important thing here is to not make them detach from parent when moving between screen sections - and it's pretty challenging to satisfy all this. Current solution uses flexbox for SectionedScreen component and SectionedScreenPortal elements (used as Portal for all abovementioned). SectionedScreenPortal appends a child to SectionedScreen using proper class based on target section. Different sections' classes have slightly different styles/order and additional logic is applied to force the flexbox break into 2 rows if both top-right and bottom-right elements are detected. We use ":has" selector that currently has relatively limited support - it currently works on Chrome/Brave, but on Firefox you still need to activate it with config. There's a fallback option allowing almost same functionality without the ability to use full height of the screen when only top-left and/or top-right elements are present - they're at 60% height then. Using flexbox is a working though not perfect solution as it's not really intended for such layout. I tried display:grid but it becomes even more complex to manipulate it in flexible enough manner based on content. My original approach was with 2 nested containers - one for top/bottom row and one left/right for each row - it was working nicely as a layout but moving between sections triggers child detach, which causes interruptions with iframe videos, etc. It's probably possibly to use shadow DOM for translating screen element into different sections but I doubt it's possible using only css. Another planned approach was having React Context used by SectionedScreen and SectionedScreenPortals for sharing info between them and applying different styles and helper elements based on that, but it seems css with :has selector allows to satisfy all our current requirements. Some relatively new css selectors like :only-child, :only-of-type, :nth-of-type could also be of use in future. Some useful links: https://ishadeed.com/article/conditional-css/ https://tobiasahlin.com/blog/flexbox-break-to-new-row/ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Aligning_Items_in_a_Flex_Container https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/Basic_Concepts_of_Grid_Layout Shadow DOM: https://github.com/whatwg/html/issues/5484#issuecomment-620481794 - nice and simple example https://stackoverflow.com/questions/42274721/shadow-dom-and-reactjs https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow --- packages/app/src/scenes/AppLayers.tsx | 4 +- .../ObjectPluginPage/ObjectPluginPage.tsx | 16 +- .../ScreenShareWidget/ScreenShareWidget.tsx | 10 +- .../TextChatWidget/TextChatWidget.styled.ts | 13 +- .../pages/TextChatWidget/TextChatWidget.tsx | 66 ++++----- .../VoiceChatWidget/VoiceChatWidget.styled.ts | 3 +- .../pages/VoiceChatWidget/VoiceChatWidget.tsx | 42 +++--- packages/app/src/static/styles/main.scss | 9 ++ packages/app/src/static/styles/variables.css | 1 - packages/ui-kit/src/atoms/Portal/Portal.ts | 3 + .../SectionedScreen/SectionedScreen.styled.ts | 138 ++++++------------ .../atoms/SectionedScreen/SectionedScreen.tsx | 108 +------------- .../SectionedScreenPortal.tsx | 25 ++++ .../src/atoms/SectionedScreenPortal/index.ts | 1 + packages/ui-kit/src/atoms/index.ts | 1 + packages/ui-kit/src/enums/index.ts | 1 + .../ui-kit/src/enums/screenSections.enum.ts | 5 + .../WindowPanel/WindowPanel.styled.ts | 21 ++- 18 files changed, 194 insertions(+), 273 deletions(-) create mode 100644 packages/ui-kit/src/atoms/SectionedScreenPortal/SectionedScreenPortal.tsx create mode 100644 packages/ui-kit/src/atoms/SectionedScreenPortal/index.ts create mode 100644 packages/ui-kit/src/enums/screenSections.enum.ts diff --git a/packages/app/src/scenes/AppLayers.tsx b/packages/app/src/scenes/AppLayers.tsx index 369558d7c..fe451b948 100644 --- a/packages/app/src/scenes/AppLayers.tsx +++ b/packages/app/src/scenes/AppLayers.tsx @@ -28,7 +28,9 @@ const AppLayers: FC = (props) => {
- +
+ +
{renderUnity && }
{children}
diff --git a/packages/app/src/scenes/object/pages/ObjectPluginPage/ObjectPluginPage.tsx b/packages/app/src/scenes/object/pages/ObjectPluginPage/ObjectPluginPage.tsx index 4e01bb9ba..d65500af7 100644 --- a/packages/app/src/scenes/object/pages/ObjectPluginPage/ObjectPluginPage.tsx +++ b/packages/app/src/scenes/object/pages/ObjectPluginPage/ObjectPluginPage.tsx @@ -1,7 +1,13 @@ import {FC, useEffect} from 'react'; import {observer} from 'mobx-react-lite'; import {useTheme} from 'styled-components'; -import {ErrorBoundary, SectionPortal, Text, WindowPanel} from '@momentum-xyz/ui-kit'; +import { + ErrorBoundary, + ScreenSectionsEnum, + SectionedScreenPortal, + Text, + WindowPanel +} from '@momentum-xyz/ui-kit'; import {useTranslation} from 'react-i18next'; import {toast} from 'react-toastify'; import { @@ -98,9 +104,11 @@ const PluginInnerWrapper = ({ const {content, objectView} = plugin.usePlugin(pluginProps); return !pluginLoader.isError ? ( - {content ? ( @@ -121,7 +129,7 @@ const PluginInnerWrapper = ({ ) : ( )} - + ) : ( ); diff --git a/packages/app/src/scenes/widgets/pages/ScreenShareWidget/ScreenShareWidget.tsx b/packages/app/src/scenes/widgets/pages/ScreenShareWidget/ScreenShareWidget.tsx index eb155f379..6ebe2603e 100644 --- a/packages/app/src/scenes/widgets/pages/ScreenShareWidget/ScreenShareWidget.tsx +++ b/packages/app/src/scenes/widgets/pages/ScreenShareWidget/ScreenShareWidget.tsx @@ -1,6 +1,6 @@ import React, {FC, useCallback, useEffect} from 'react'; import {observer} from 'mobx-react-lite'; -import {SectionPortal, WindowPanel} from '@momentum-xyz/ui-kit'; +import {ScreenSectionsEnum, SectionedScreenPortal, WindowPanel} from '@momentum-xyz/ui-kit'; import {useTranslation} from 'react-i18next'; import {useStore} from 'shared/hooks'; @@ -41,9 +41,11 @@ const ScreenShareWidget: FC = () => { }; return ( - { )} - + ); }; diff --git a/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.styled.ts b/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.styled.ts index 87e410b63..7fac31589 100644 --- a/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.styled.ts +++ b/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.styled.ts @@ -1,22 +1,13 @@ import {rgba} from 'polished'; import styled from 'styled-components'; -export const Modal = styled.div` - display: flex; - // margin: 10px; - // box-sizing: border-box; - // position: absolute; - // right: 0; - // top: 0; - // margin: 20px; -`; - export const Container = styled.div` background: ${(props) => props.theme.bg && rgba(props.theme.bg, 0.75)}; border-radius: 10px; overflow: hidden; width: 280px; - height: 584px; + max-height: 584px; + height: 100%; display: flex; flex-direction: column; diff --git a/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.tsx b/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.tsx index f40c5286a..810512fe3 100644 --- a/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.tsx +++ b/packages/app/src/scenes/widgets/pages/TextChatWidget/TextChatWidget.tsx @@ -1,5 +1,11 @@ import React, {FC, useEffect} from 'react'; -import {Heading, IconSvg, SectionPortal, SvgButton} from '@momentum-xyz/ui-kit'; +import { + Heading, + IconSvg, + ScreenSectionsEnum, + SectionedScreenPortal, + SvgButton +} from '@momentum-xyz/ui-kit'; import {observer} from 'mobx-react-lite'; import {useTranslation} from 'react-i18next'; @@ -11,10 +17,7 @@ import * as styled from './TextChatWidget.styled'; const TextChatWidget: FC = () => { const {widgetsStore, sessionStore, unityStore} = useStore(); const {unityInstanceStore} = unityStore; - const { - textChatStore - // voiceChatStore - } = widgetsStore; + const {textChatStore} = widgetsStore; const {streamChat} = textChatStore; const {t} = useTranslation(); @@ -28,35 +31,30 @@ const TextChatWidget: FC = () => { }, [sessionStore.user, sessionStore.userId, streamChat, unityStore.worldId]); return ( - - {/* FIXME: Design discussion in order to avoid relation to VoiceChatStore */} - - - - - - - - - - - - - - {streamChat.client && streamChat.currentChannel && ( - unityInstanceStore.changeKeyboardControl(false)} - onBlur={() => unityInstanceStore.changeKeyboardControl(true)} - /> - )} - - - - + + + + + + + + + + + + + + {streamChat.client && streamChat.currentChannel && ( + unityInstanceStore.changeKeyboardControl(false)} + onBlur={() => unityInstanceStore.changeKeyboardControl(true)} + /> + )} + + + ); }; diff --git a/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.styled.ts b/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.styled.ts index 87e410b63..99ba73f84 100644 --- a/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.styled.ts +++ b/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.styled.ts @@ -16,7 +16,8 @@ export const Container = styled.div` border-radius: 10px; overflow: hidden; width: 280px; - height: 584px; + max-height: 584px; + height: 100%; display: flex; flex-direction: column; diff --git a/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.tsx b/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.tsx index ebc0574f7..34b960368 100644 --- a/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.tsx +++ b/packages/app/src/scenes/widgets/pages/VoiceChatWidget/VoiceChatWidget.tsx @@ -1,7 +1,13 @@ import React, {FC, useCallback, useEffect} from 'react'; import {observer} from 'mobx-react-lite'; import {useTranslation} from 'react-i18next'; -import {Heading, IconSvg, SectionPortal, SvgButton} from '@momentum-xyz/ui-kit'; +import { + Heading, + IconSvg, + ScreenSectionsEnum, + SectionedScreenPortal, + SvgButton +} from '@momentum-xyz/ui-kit'; import {useStore} from 'shared/hooks'; @@ -28,24 +34,22 @@ const VoiceChatWidget: FC = () => { }, [agoraStore, agoraVoiceChatStore.hasJoined, voiceChatStore.dialog]); return ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); }; diff --git a/packages/app/src/static/styles/main.scss b/packages/app/src/static/styles/main.scss index c9d9b0e26..c7bdf2f42 100644 --- a/packages/app/src/static/styles/main.scss +++ b/packages/app/src/static/styles/main.scss @@ -25,6 +25,15 @@ body { z-index: 1; } +#sectioned-screen-container { + width: 100%; + height: calc(100% - 50px); + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + .noScrollIndicator { -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ diff --git a/packages/app/src/static/styles/variables.css b/packages/app/src/static/styles/variables.css index f9663b781..c9afea5ba 100644 --- a/packages/app/src/static/styles/variables.css +++ b/packages/app/src/static/styles/variables.css @@ -95,7 +95,6 @@ --overlay-z-index: 40; --base-z-index: 1; - /* MUSIC PLAYER */ --player-default-color: rgba(255, 255, 255, 0.5); diff --git a/packages/ui-kit/src/atoms/Portal/Portal.ts b/packages/ui-kit/src/atoms/Portal/Portal.ts index c756c272a..00e52eec5 100644 --- a/packages/ui-kit/src/atoms/Portal/Portal.ts +++ b/packages/ui-kit/src/atoms/Portal/Portal.ts @@ -17,6 +17,9 @@ const Portal: FC = ({children, className, parentId, maximized}) if (maximized) { // set flex-grow to 1 to make it fill the parent domContainer.current.style.flexGrow = '1'; + } else { + // set flex-grow to 0 to make it not fill the parent + domContainer.current.style.flexGrow = '0'; } useEffect(() => { diff --git a/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.styled.ts b/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.styled.ts index 0c9773cd8..835766295 100644 --- a/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.styled.ts +++ b/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.styled.ts @@ -1,113 +1,71 @@ import styled from 'styled-components'; -// TODO try css masonry layout +// this should allow debugging the layout even on prod - just set DEBUG=1 in sessionStorage and refresh +const DEBUG = sessionStorage.getItem('DEBUG') !== null; export const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; - height: calc(100% - 50px); - position: absolute; - top: 0; - left: 0; - z-index: 1; pointer-events: none; -`; - -export const InnerContainer = styled.div` - display: flex; - // width: 100%; - // height: 100%; -`; -export const Section = styled.div` display: flex; - // flex: 1 0 auto; - position: relative; - padding: 10px; + flex-wrap: wrap; + justify-content: flex-end; + align-items: flex-start; gap: 10px; -`; - -export const ContainerG = styled.div` - display: grid; - grid-template-columns: repeat(6, minmax(0, auto)); - grid-template-rows: minmax(0, auto) minmax(0, auto); - grid-gap: 10px; - grid-template-areas: - 'left-top left-top left-top right-top right-top right-top' - 'left-bottom left-bottom left-bottom right-bottom right-bottom right-bottom'; + padding: 10px; width: 100%; - height: calc(100% - 50px); - position: absolute; - top: 0; - left: 0; - z-index: 1; - pointer-events: none; + height: 100%; - .section-screen-item { + .sectioned-screen-top-left { + flex-grow: 1; + order: 1; + } + .sectioned-screen-top-right { + order: 2; + justify-content: end; + } + .sectioned-screen-bottom-right { display: flex; justify-content: end; + order: 10; + align-self: flex-end; } - .left-top:nth-child(1), - .left-bottom:nth-child(1) { - grid-column: 1 / 2; + .sectioned-screen-top-left, + .sectioned-screen-top-right { + height: 60%; } - .left-top:nth-child(2), - .left-bottom:nth-child(2) { - grid-column: 2 / 3; - } - .left-top:nth-child(3), - .left-bottom:nth-child(3) { - grid-column: 3 / 4; + .sectioned-screen-bottom-right { + height: 38%; } - .right-top:nth-child(1), - .right-bottom:nth-child(1) { - grid-column: 6 / 7; - } - .right-top:nth-child(2), - .right-bottom:nth-child(2) { - grid-column: 5 / 6; - } - .right-top:nth-child(3), - .right-bottom:nth-child(3) { - grid-column: 4 / 5; + .sectioned-screen-section-break { + flex-basis: 100%; + height: ${() => (DEBUG ? '2px' : '0')}; + background: ${() => (DEBUG ? 'red' : 'transparent')}; + margin: -5px 0; + order: 5; } - .left-top { - grid-area: left-top; - flex-grow: 1; - } - .right-top { - grid-area: right-top; - } - .left-bottom { - grid-area: left-bottom; + @supports (selector(:has(div))) { + .sectioned-screen-section-break { + display: none; + } + .sectioned-screen-top-left, + .sectioned-screen-top-right { + height: 100%; + } } - .right-bottom { - // grid-column: 2 / 3; - // grid-row: 2 / 3; - grid-area: right-bottom; - } -`; -export const SectionG = styled.div` - // &.top-left { - // grid-column: 1 / 2; - // grid-row: 1 / 2; - // } - // &.top-right { - // grid-column: 2 / 3; - // grid-row: 1 / 2; - // } - // &.bottom-left { - // grid-column: 1 / 2; - // grid-row: 2 / 3; - // } - // &.bottom-right { - // grid-column: 2 / 3; - // grid-row: 2 / 3; - // } + // right now we're ok with having top-left and bottom-right sections on the same row but alighed differently + // if we have top-right and bottom-right, then we need to split them into two rows and limit the top row height + &:has(.sectioned-screen-bottom-right):has(.sectioned-screen-top-right) { + .sectioned-screen-section-break { + display: block; + } + .sectioned-screen-top-left, + .sectioned-screen-top-right { + height: 60%; + } + } `; diff --git a/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.tsx b/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.tsx index babb5e5cb..7ab27143c 100644 --- a/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.tsx +++ b/packages/ui-kit/src/atoms/SectionedScreen/SectionedScreen.tsx @@ -1,109 +1,17 @@ import {FC} from 'react'; -import cn from 'classnames'; - -import {Portal} from '../Portal'; import * as styled from './SectionedScreen.styled'; -// this should allow debugging the layout even on prod - just set DEBUG=1 in sessionStorage and refresh -// const DEBUG = sessionStorage.getItem('DEBUG') !== null; - -export const SectionedScreen: FC = () => { - return ( - - {/* - - - */} - - // - // - // - // - // - // - // - // - // - // - ); -}; +export const SECTIONED_SCREEN_ID = 'sectioned-screen'; -interface SectionPortalPropsInterface { - section: string; - maximized?: boolean; +export interface PropsInterface { + id?: string; } -export const SectionPortal: FC = ({children, section, maximized}) => { + +export const SectionedScreen: FC = ({id = SECTIONED_SCREEN_ID}) => { return ( - - {children} - {/* {children} */} - + +
+ ); }; diff --git a/packages/ui-kit/src/atoms/SectionedScreenPortal/SectionedScreenPortal.tsx b/packages/ui-kit/src/atoms/SectionedScreenPortal/SectionedScreenPortal.tsx new file mode 100644 index 000000000..8a5632809 --- /dev/null +++ b/packages/ui-kit/src/atoms/SectionedScreenPortal/SectionedScreenPortal.tsx @@ -0,0 +1,25 @@ +import {FC} from 'react'; + +import {ScreenSectionsEnum} from '../../enums'; +import {Portal} from '../Portal'; +import {SECTIONED_SCREEN_ID} from '../SectionedScreen/SectionedScreen'; + +interface SectionedScreenPortalPropsInterface { + section: ScreenSectionsEnum; + maximized?: boolean; +} +export const SectionedScreenPortal: FC = ({ + children, + section, + maximized +}) => { + return ( + + {children} + + ); +}; diff --git a/packages/ui-kit/src/atoms/SectionedScreenPortal/index.ts b/packages/ui-kit/src/atoms/SectionedScreenPortal/index.ts new file mode 100644 index 000000000..4578e19b1 --- /dev/null +++ b/packages/ui-kit/src/atoms/SectionedScreenPortal/index.ts @@ -0,0 +1 @@ +export * from './SectionedScreenPortal'; diff --git a/packages/ui-kit/src/atoms/index.ts b/packages/ui-kit/src/atoms/index.ts index c50380531..205d6baba 100644 --- a/packages/ui-kit/src/atoms/index.ts +++ b/packages/ui-kit/src/atoms/index.ts @@ -19,3 +19,4 @@ export * from './ErrorBoundary'; export * from './SpacePage'; export * from './LoaderFallback'; export * from './SectionedScreen'; +export * from './SectionedScreenPortal'; diff --git a/packages/ui-kit/src/enums/index.ts b/packages/ui-kit/src/enums/index.ts index 8f6cca7ec..0c0c26b56 100644 --- a/packages/ui-kit/src/enums/index.ts +++ b/packages/ui-kit/src/enums/index.ts @@ -1,3 +1,4 @@ export * from './userStatus.enum'; export * from './imageSize.enum'; export * from './errors.enum'; +export * from './screenSections.enum'; diff --git a/packages/ui-kit/src/enums/screenSections.enum.ts b/packages/ui-kit/src/enums/screenSections.enum.ts new file mode 100644 index 000000000..14883eefe --- /dev/null +++ b/packages/ui-kit/src/enums/screenSections.enum.ts @@ -0,0 +1,5 @@ +export enum ScreenSectionsEnum { + TOP_LEFT = 'top-left', + TOP_RIGHT = 'top-right', + BOTTOM_RIGHT = 'bottom-right' +} diff --git a/packages/ui-kit/src/molecules/WindowPanel/WindowPanel.styled.ts b/packages/ui-kit/src/molecules/WindowPanel/WindowPanel.styled.ts index d234a1bed..07105f5b4 100644 --- a/packages/ui-kit/src/molecules/WindowPanel/WindowPanel.styled.ts +++ b/packages/ui-kit/src/molecules/WindowPanel/WindowPanel.styled.ts @@ -4,21 +4,26 @@ import styled from 'styled-components'; export const Container = styled.div` display: flex; flex-direction: column; - // flex: 1 0 auto; ?? overflow: hidden; pointer-events: auto; - width: 680px; - height: 420px; + background: ${(props) => props.theme.bg && rgba(props.theme.bg, 0.9)}; border-radius: 10px 10px 0px 0px; - &.expanded { - // margin-top: 20px; - width: 100%; - height: 100%; - // height: calc(100% - 20px); + &:not(.expanded) { + max-height: 420px; + // max-height: 40vh; + aspect-ratio: 16 / 9; + // @media (max-height: 680px) { + // max-height: 35vh; + // } } + // &.expanded { + width: 100%; + height: 100%; + // } + transition: all 0.2s ease-in-out; `;