diff --git a/packages/main/src/components/ObjectPage/CollapsedAvatar.module.css b/packages/main/src/components/ObjectPage/CollapsedAvatar.module.css new file mode 100644 index 00000000000..fc7e2faa09c --- /dev/null +++ b/packages/main/src/components/ObjectPage/CollapsedAvatar.module.css @@ -0,0 +1,27 @@ +.base { + align-self: center; + opacity: 0; + padding-inline-end: 1rem; +} + +.hidden { + opacity: 0; +} + +.visible { + transition: opacity 0.5s; + opacity: 1; +} + +.imageContainer { + display: inline-block; + vertical-align: middle; + max-height: 3rem; + width: 3rem; + max-width: 3rem; +} + +.image { + width: 100%; + height: 100%; +} diff --git a/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx b/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx index dc446f60ffc..f2333afcd97 100644 --- a/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx +++ b/packages/main/src/components/ObjectPage/CollapsedAvatar.tsx @@ -1,38 +1,9 @@ +import { useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties, ReactElement } from 'react'; import React, { cloneElement, useEffect, useMemo, useRef, useState } from 'react'; -import { createUseStyles } from 'react-jss'; import { AvatarSize } from '../../enums/index.js'; - -const styles = { - base: { - alignSelf: 'center', - opacity: 0, - paddingInlineEnd: '1rem' - }, - hidden: { - opacity: 0 - }, - visible: { - transition: 'opacity 0.5s', - opacity: 1 - }, - imageContainer: { - display: 'inline-block', - verticalAlign: 'middle', - maxHeight: '3rem', - width: '3rem', - maxWidth: '3rem' - }, - image: { - width: '100%', - height: '100%' - } -}; - -const useStyles = createUseStyles(styles, { - name: 'CollapsedAvatar' -}); +import { classNames, styleData } from './CollapsedAvatar.module.css.js'; export interface CollapsedAvatarPropTypes { image?: string | ReactElement; @@ -42,7 +13,7 @@ export interface CollapsedAvatarPropTypes { export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => { const { image, imageShapeCircle, style } = props; - const classes = useStyles(); + useStylesheet(styleData, CollapsedAvatar.displayName); const [isMounted, setIsMounted] = useState(false); const domRef = useRef(null); @@ -52,18 +23,18 @@ export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => { if (typeof image === 'string') { return ( - Object Page Image + Object Page Image ); } else { return cloneElement(image, { size: AvatarSize.S, className: image.props?.className - ? `${classes.imageContainer} ${image.props?.className}` - : classes.imageContainer + ? `${classNames.imageContainer} ${image.props?.className}` + : classNames.imageContainer } as unknown); } }, [image, imageShapeCircle]); @@ -72,7 +43,7 @@ export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => { setIsMounted(true); }, []); - const containerClasses = clsx(classes.base, isMounted ? classes.visible : classes.hidden); + const containerClasses = clsx(classNames.base, isMounted ? classNames.visible : classNames.hidden); return (
@@ -80,3 +51,5 @@ export const CollapsedAvatar = (props: CollapsedAvatarPropTypes) => {
); }; + +CollapsedAvatar.displayName = 'CollapsedAvatar'; diff --git a/packages/main/src/components/ObjectPage/ObjectPage.jss.ts b/packages/main/src/components/ObjectPage/ObjectPage.jss.ts deleted file mode 100644 index 1f724624c21..00000000000 --- a/packages/main/src/components/ObjectPage/ObjectPage.jss.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ThemingParameters } from '@ui5/webcomponents-react-base'; -import { ResponsiveContainerPadding } from '../../internal/ContainerQueries.js'; -import { DynamicPageCssVariables } from '../DynamicPage/utils.js'; - -export const ObjectPageCssVariables = { - anchorFloat: '--_ui5wcr_ObjectPage_actions_float', - anchorLeft: '--_ui5wcr_ObjectPage_actions_left', - anchorRight: '--_ui5wcr_ObjectPage_actions_right' -}; - -export const styles = { - objectPage: { - [DynamicPageCssVariables.headerDisplay]: 'block', - [DynamicPageCssVariables.titleFontSize]: ThemingParameters.sapObjectHeader_Title_FontSize, - container: 'objectPage / inline-size', - boxSizing: 'border-box', - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - maxHeight: '100vh', - position: 'relative', - whiteSpace: 'normal', - fontFamily: ThemingParameters.sapFontFamily, - backgroundColor: ThemingParameters.sapBackgroundColor, - overflowX: 'hidden', - overflowY: 'auto', - scrollBehavior: 'smooth', - '& section[id*="ObjectPageSection-"] > div[role="heading"]': { - display: 'none' - }, - // explanation why first-child selector is not sufficient here: - // https://stackoverflow.com/questions/7128406/css-select-the-first-child-from-elements-with-particular-attribute - '& section[id*="ObjectPageSection-"] ~ section[id*="ObjectPageSection-"] > div[role="heading"]': { - display: 'block' - } - }, - '@global html': { - [ObjectPageCssVariables.anchorFloat]: 'right', - [ObjectPageCssVariables.anchorRight]: '1.25rem', - [ObjectPageCssVariables.anchorLeft]: 'unset' - }, - '@global [dir="rtl"]': { - [ObjectPageCssVariables.anchorFloat]: 'left', - [ObjectPageCssVariables.anchorRight]: 'unset', - [ObjectPageCssVariables.anchorLeft]: '1.25rem' - }, - iconTabBarMode: { - '& section[data-component-name="ObjectPageSection"] > div[role="heading"]': { - display: 'none' - } - }, - headerCollapsed: { - [DynamicPageCssVariables.headerDisplay]: 'none', - [DynamicPageCssVariables.titleFontSize]: ThemingParameters.sapObjectHeader_Title_SnappedFontSize - }, - headerContainer: { - extend: ResponsiveContainerPadding, - marginBottom: '0.25rem', - backgroundColor: ThemingParameters.sapObjectHeader_Background, - display: 'grid', - gridAutoColumns: 'min-content calc(100% - 5rem - 2rem)' /*avatar size - padding */, - '& [data-component-name="ObjectPageHeaderContent"]': { - gridColumn: 2 - } - }, - headerHoverStyles: { - '&[data-not-clickable="true"]': { - cursor: 'unset' - }, - '&[data-not-clickable="false"]': { - backgroundColor: `${ThemingParameters.sapObjectHeader_Hover_Background}`, - '& [data-component-name="DynamicPageTitle"]': { - backgroundColor: ThemingParameters.sapObjectHeader_Hover_Background - } - } - }, - header: { - extend: ResponsiveContainerPadding, - [DynamicPageCssVariables.headerDisplay]: 'block', - boxSizing: 'border-box', - backgroundColor: ThemingParameters.sapObjectHeader_Background, - position: 'sticky', - top: 0, - zIndex: 2, - '& [data-component-name="DynamicPageTitle"]': { - gridColumn: 2, - paddingInline: 0 - }, - cursor: 'pointer' - }, - headerImage: { - minWidth: '5rem', - maxWidth: '5rem', - maxHeight: '5rem', - display: 'inline-block', - marginInlineEnd: '2rem' - }, - image: { - width: '100%', - height: '100%' - }, - anchorBar: { - position: 'sticky', - zIndex: 2 - }, - tabContainer: { - position: 'sticky', - zIndex: 1, - // each tab has inline padding of 1rem, so it needs to be subtracted from the default responsive padding - '@container objectPage (max-width: 599px)': { - '--_ui5wcr_ObjectPage_tab_bar_inline_padding': 0 - }, - '@container objectPage (min-width: 600px) and (max-width: 1023px)': { - '--_ui5wcr_ObjectPage_tab_bar_inline_padding': '1rem' - }, - '@container objectPage (min-width: 1024px) and (max-width: 1439px)': { - '--_ui5wcr_ObjectPage_tab_bar_inline_padding': '1rem' - }, - '@container objectPage (min-width: 1440px)': { - '--_ui5wcr_ObjectPage_tab_bar_inline_padding': '2rem' - } - }, - tabContainerComponent: { - '&::part(content)': { - display: 'none' - }, - '&::part(tabstrip)': { - // padding-inline is used here to ensure the same responsive padding behavior as for the rest of the component - padding: 0, - paddingInline: 'var(--_ui5wcr_ObjectPage_tab_bar_inline_padding)', - boxShadow: `inset 0 -0.0625rem ${ThemingParameters.sapPageHeader_BorderColor}, 0 0.125rem 0.25rem 0 rgb(0 0 0 / 8%)` - } - }, - content: { - extend: ResponsiveContainerPadding, - flexGrow: 1, - position: 'relative' - }, - footer: { - position: 'sticky', - bottom: '0.5rem', - margin: '0 0.5rem' - }, - footerSpacer: { height: '1rem', flexShrink: 0 }, - subSectionPopover: { - '&::part(content)': { - padding: 0 - } - }, - titleInHeader: { padding: 0 }, - snappedContent: { gridColumn: '1 / span 2' } -}; diff --git a/packages/main/src/components/ObjectPage/ObjectPage.module.css b/packages/main/src/components/ObjectPage/ObjectPage.module.css new file mode 100644 index 00000000000..4c21ecc9700 --- /dev/null +++ b/packages/main/src/components/ObjectPage/ObjectPage.module.css @@ -0,0 +1,180 @@ +.objectPage { + container: objectPage / inline-size; + --_ui5wcr_DynamicPage_header_display: block; + --_ui5wcr_DynamicPage_title_fontsize: var(--sapObjectHeader_Title_FontSize); + + box-sizing: border-box; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + max-height: 100vh; + position: relative; + white-space: normal; + font-family: var(--sapFontFamily); + background-color: var(--sapBackgroundColor); + overflow-x: hidden; + overflow-y: auto; + scroll-behavior: smooth; + section[id*='ObjectPageSection-'] > div[role='heading'] { + display: none; + } + + /* explanation why first-child selector is not sufficient here: + https://stackoverflow.com/questions/7128406/css-select-the-first-child-from-elements-with-particular-attribute + */ + section[id*='ObjectPageSection-'] ~ section[id*='ObjectPageSection-'] > div[role='heading'] { + display: block; + } +} + +.iconTabBarMode section[data-component-name='ObjectPageSection'] > div[role='heading'] { + display: none; +} + +/* Header */ +.header { + --_ui5wcr_DynamicPage_header_display: block; + box-sizing: border-box; + background-color: var(--sapObjectHeader_Background); + position: sticky; + inset-block-start: 0; + z-index: 2; + cursor: pointer; + + [data-component-name='DynamicPageTitle'] { + grid-column: 2; + padding-inline: 0; + } +} + +.headerCollapsed { + --_ui5wcr_DynamicPage_header_display: none; + --_ui5wcr_DynamicPage_title_fontsize: var(--sapObjectHeader_Title_SnappedFontSize); +} + +.headerContainer { + margin-block-end: 0.25rem; + background-color: var(--sapObjectHeader_Background); + display: grid; + grid-auto-columns: min-content calc(100% - 5rem - 2rem); + + [data-component-name='ObjectPageHeaderContent'] { + grid-column: 2; + } +} + +.headerHoverStyles { + &[data-not-clickable='true'] { + cursor: unset; + } + + &[data-not-clickable='false'] { + background-color: var(--sapObjectHeader_Hover_Background); + + [data-component-name='DynamicPageTitle'] { + background-color: var(--sapObjectHeader_Hover_Background); + } + } +} + +.headerImage { + min-width: 5rem; + max-width: 5rem; + max-height: 5rem; + display: inline-block; + margin-inline-end: 2rem; +} + +.image { + width: 100%; + height: 100%; +} + +.anchorBar { + position: sticky; + z-index: 2; +} + +.tabContainer { + position: sticky; + z-index: 1; +} + +.tabContainerComponent { + &::part(content) { + display: none; + } + + &::part(tabstrip) { + padding: 0; + padding-inline: var(--_ui5wcr_ObjectPage_tab_bar_inline_padding); + box-shadow: + inset 0 -0.0625rem var(--sapPageHeader_BorderColor), + 0 0.125rem 0.25rem 0 rgb(0 0 0 / 8%); + } +} + +.content { + flex-grow: 1; + position: relative; +} + +@container (max-width: 599px) { + .header, + .headerContainer, + .content { + padding-inline: 1rem; + } + + .tabContainer { + --_ui5wcr_ObjectPage_tab_bar_inline_padding: 0; + } +} + +@container (min-width: 600px) and (max-width: 1439px) { + .header, + .headerContainer, + .content { + padding-inline: 2rem; + } + + .tabContainer { + --_ui5wcr_ObjectPage_tab_bar_inline_padding: 1rem; + } +} + +@container (min-width: 1440px) { + .header, + .headerContainer, + .content { + padding-inline: 3rem; + } + + .tabContainer { + --_ui5wcr_ObjectPage_tab_bar_inline_padding: 2rem; + } +} + +.footer { + position: sticky; + inset-block-end: 0.5rem; + margin: 0 0.5rem; +} + +.footerSpacer { + height: 1rem; + flex-shrink: 0; +} + +.subSectionPopover::part(content) { + padding: 0; +} + +.titleInHeader { + padding: 0; +} + +.snappedContent { + grid-column: 1 / span 2; +} diff --git a/packages/main/src/components/ObjectPage/index.tsx b/packages/main/src/components/ObjectPage/index.tsx index 1300e0a7eae..7948a428a27 100644 --- a/packages/main/src/components/ObjectPage/index.tsx +++ b/packages/main/src/components/ObjectPage/index.tsx @@ -6,6 +6,7 @@ import { deprecationNotice, enrichEventWithDetails, ThemingParameters, + useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; @@ -20,7 +21,6 @@ import React, { useRef, useState } from 'react'; -import { createUseStyles } from 'react-jss'; import { AvatarSize, GlobalStyleClasses, ObjectPageMode } from '../../enums/index.js'; import { addCustomCSSWithScoping } from '../../internal/addCustomCSSWithScoping.js'; import { safeGetChildrenArray } from '../../internal/safeGetChildrenArray.js'; @@ -33,7 +33,7 @@ import { DynamicPageAnchorBar } from '../DynamicPageAnchorBar/index.js'; import { DynamicPageHeader } from '../DynamicPageHeader/index.js'; import type { ObjectPageSectionPropTypes } from '../ObjectPageSection/index.js'; import { CollapsedAvatar } from './CollapsedAvatar.js'; -import { styles } from './ObjectPage.jss.js'; +import { classNames, styleData } from './ObjectPage.module.css.js'; import { extractSectionIdFromHtmlId, getSectionById } from './ObjectPageUtils.js'; addCustomCSSWithScoping( @@ -173,8 +173,6 @@ export interface ObjectPagePropTypes extends Omit { onPinnedStateChange?: (pinned: boolean) => void; } -const useStyles = createUseStyles(styles, { name: 'ObjectPage' }); - /** * A component that allows apps to easily display information related to a business object. * @@ -206,7 +204,7 @@ const ObjectPage = forwardRef((props, ref) ...rest } = props; - const classes = useStyles(); + useStylesheet(styleData, ObjectPage.displayName); const firstSectionId: string | undefined = safeGetChildrenArray(children)[0]?.props?.id; @@ -304,19 +302,19 @@ const ObjectPage = forwardRef((props, ref) if (typeof image === 'string') { return ( - Company Logo + Company Logo ); } else { return cloneElement(image, { size: AvatarSize.L, - className: clsx(classes.headerImage, image.props?.className) + className: clsx(classNames.headerImage, image.props?.className) } as AvatarPropTypes); } - }, [image, classes.headerImage, classes.image, imageShapeCircle]); + }, [image, classNames.headerImage, classNames.image, imageShapeCircle]); const scrollToSectionById = (id?: string, isSubSection = false) => { const section = objectPageRef.current?.querySelector( @@ -500,11 +498,11 @@ const ObjectPage = forwardRef((props, ref) scrollTimeout.current = performance.now() + 500; if (!e.detail.visible) { setHeaderCollapsedInternal(true); - objectPageRef.current?.classList.add(classes.headerCollapsed); + objectPageRef.current?.classList.add(classNames.headerCollapsed); } else { setHeaderCollapsedInternal(false); setScrolledHeaderExpanded(true); - objectPageRef.current?.classList.remove(classes.headerCollapsed); + objectPageRef.current?.classList.remove(classNames.headerCollapsed); } }, []); @@ -530,10 +528,10 @@ const ObjectPage = forwardRef((props, ref) ); const objectPageClasses = clsx( - classes.objectPage, + classNames.objectPage, GlobalStyleClasses.sapScrollBar, className, - mode === ObjectPageMode.IconTabBar && classes.iconTabBarMode + mode === ObjectPageMode.IconTabBar && classNames.iconTabBarMode ); const { onScroll: _0, selectedSubSectionId: _1, ...propsWithoutOmitted } = rest; @@ -626,7 +624,7 @@ const ObjectPage = forwardRef((props, ref) const hasHeaderContent = !!headerContent; const renderTitleSection = useCallback( (inHeader = false) => { - const titleInHeaderClass = inHeader ? classes.titleInHeader : undefined; + const titleInHeaderClass = inHeader ? classNames.titleInHeader : undefined; if (headerTitle?.props && headerTitle.props?.showSubHeaderRight === undefined) { return cloneElement(headerTitle, { @@ -670,7 +668,7 @@ const ObjectPage = forwardRef((props, ref) headerPinned: headerPinned || scrolledHeaderExpanded, ref: componentRefHeaderContent, children: ( -
+
{avatar} {(headerContent.props.children || titleInHeader) && (
@@ -689,7 +687,7 @@ const ObjectPage = forwardRef((props, ref) headerPinned={headerPinned || scrolledHeaderExpanded} ref={componentRefHeaderContent} > -
+
{avatar}
{titleInHeader && renderTitleSection(true)}
@@ -759,7 +757,7 @@ const ObjectPage = forwardRef((props, ref) setIsAfterScroll(true); }, 100); if (!headerPinned || e.target.scrollTop === 0) { - objectPageRef.current?.classList.remove(classes.headerCollapsed); + objectPageRef.current?.classList.remove(classNames.headerCollapsed); } if (scrolledHeaderExpanded && e.target.scrollTop !== prevScrollTop.current) { if (e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight) { @@ -778,12 +776,12 @@ const ObjectPage = forwardRef((props, ref) const onHoverToggleButton = useCallback( (e) => { if (e?.type === 'mouseover') { - topHeaderRef.current?.classList.add(classes.headerHoverStyles); + topHeaderRef.current?.classList.add(classNames.headerHoverStyles); } else { - topHeaderRef.current?.classList.remove(classes.headerHoverStyles); + topHeaderRef.current?.classList.remove(classNames.headerHoverStyles); } }, - [classes.headerHoverStyles] + [classNames.headerHoverStyles] ); const objectPageStyles: CSSProperties = { @@ -811,7 +809,7 @@ const ObjectPage = forwardRef((props, ref) role={a11yConfig?.objectPageTopHeader?.role} data-not-clickable={titleHeaderNotClickable} aria-roledescription={a11yConfig?.objectPageTopHeader?.ariaRoledescription ?? 'Object Page header'} - className={classes.header} + className={classNames.header} onClick={onTitleClick} style={{ gridAutoColumns: `min-content ${ @@ -825,7 +823,7 @@ const ObjectPage = forwardRef((props, ref) )} {headerTitle && renderTitleSection()} {snappedHeaderInObjPage && ( -
+
{headerTitle.props.snappedContent}
)} @@ -835,7 +833,7 @@ const ObjectPage = forwardRef((props, ref)
((props, ref) {!placeholder && (
((props, ref) fixed onTabSelect={onTabItemSelect} data-component-name="ObjectPageTabContainer" - className={classes.tabContainerComponent} + className={classNames.tabContainerComponent} > {safeGetChildrenArray(children).map((section, index) => { if (!isValidElement(section) || !section.props) return null; @@ -917,16 +915,16 @@ const ObjectPage = forwardRef((props, ref)
)} -
+
{placeholder ? placeholder : sections}
{footer && mode === ObjectPageMode.IconTabBar && !sectionSpacer && ( -
+
)} {footer && ( -