Merge branch 'develop' into refactoring/global-colors

This commit is contained in:
Viktor Fomin 2024-06-17 14:50:29 +03:00
commit 3d089d9752
62 changed files with 971 additions and 2288 deletions

View File

@ -32,7 +32,7 @@ import { withTranslation } from "react-i18next";
import { IconButton } from "@docspace/shared/components/icon-button";
import { Text } from "@docspace/shared/components/text";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import {
isDesktop as isDesktopUtils,
isMobile as isMobileUtils,
@ -75,7 +75,7 @@ const InfoPanelHeaderContent = (props) => {
selection?.isFolder && selection?.id === selection?.rootFolderId;
const isSeveralItems = selection && Array.isArray(selection);
const withSubmenu =
const withTabs =
!isRoot && !isSeveralItems && !isGallery && !isAccounts && !isTrash;
useEffect(() => {
@ -102,7 +102,7 @@ const InfoPanelHeaderContent = (props) => {
//const isArchiveRoot = rootFolderType === FolderType.Archive;
const submenuData = [
const tabsData = [
{
id: "info_members",
name: t("Common:Members"),
@ -123,12 +123,12 @@ const InfoPanelHeaderContent = (props) => {
},
];
const roomsSubmenu = [...submenuData];
const roomsTabs = [...tabsData];
const personalSubmenu = [submenuData[1], submenuData[2]];
const personalTabs = [tabsData[1], tabsData[2]];
if (selection?.canShare) {
personalSubmenu.unshift({
personalTabs.unshift({
id: "info_share",
name: t("Common:Share"),
onClick: setShare,
@ -148,7 +148,7 @@ const InfoPanelHeaderContent = (props) => {
}
};
const submenuItem = {
const tabsItem = {
id: `info_plugin-${item.key}`,
name: item.value.subMenu.name,
onClick,
@ -156,14 +156,14 @@ const InfoPanelHeaderContent = (props) => {
};
if (!item.value.filesType) {
roomsSubmenu.push(submenuItem);
personalSubmenu.push(submenuItem);
roomsTabs.push(tabsItem);
personalTabs.push(tabsItem);
return;
}
if (isRoom && item.value.filesType.includes(PluginFileType.Rooms)) {
roomsSubmenu.push(submenuItem);
personalSubmenu.push(submenuItem);
roomsTabs.push(tabsItem);
personalTabs.push(tabsItem);
return;
}
@ -175,14 +175,14 @@ const InfoPanelHeaderContent = (props) => {
return;
}
roomsSubmenu.push(submenuItem);
personalSubmenu.push(submenuItem);
roomsTabs.push(tabsItem);
personalTabs.push(tabsItem);
return;
}
if (item.value.filesType.includes(PluginFileType.Folders)) {
roomsSubmenu.push(submenuItem);
personalSubmenu.push(submenuItem);
roomsTabs.push(tabsItem);
personalTabs.push(tabsItem);
return;
}
});
@ -193,7 +193,7 @@ const InfoPanelHeaderContent = (props) => {
selection?.rootFolderType === FolderType.Archive;
return (
<StyledInfoPanelHeader isTablet={isTablet} withSubmenu={withSubmenu}>
<StyledInfoPanelHeader isTablet={isTablet} withTabs={withTabs}>
<div className="main">
<Text className="header-text" fontSize="21px" fontWeight="700">
{t("Common:Info")}
@ -214,19 +214,19 @@ const InfoPanelHeaderContent = (props) => {
)}
</div>
{withSubmenu && (
<div className="submenu">
{withTabs && (
<div className="tabs">
{isRoomsType ? (
<Submenu
<Tabs
style={{ width: "100%" }}
data={roomsSubmenu}
forsedActiveItemId={roomsView}
items={roomsTabs}
selectedItemId={roomsView}
/>
) : (
<Submenu
<Tabs
style={{ width: "100%" }}
data={personalSubmenu}
forsedActiveItemId={fileView}
items={personalTabs}
selectedItemId={fileView}
/>
)}
</div>

View File

@ -28,9 +28,9 @@ import styled, { css } from "styled-components";
import { Base } from "@docspace/shared/themes";
import { tablet } from "@docspace/shared/utils";
const getHeaderHeight = ({ withSubmenu, isTablet }) => {
const getHeaderHeight = ({ withTabs, isTablet }) => {
let res = isTablet ? 53 : 69;
if (withSubmenu) res += 32;
if (withTabs) res += 32;
return `${res}px`;
};
@ -53,9 +53,7 @@ const StyledInfoPanelHeader = styled.div`
display: flex;
flex-direction: column;
border-bottom: ${(props) =>
props.withSubmenu
? "none"
: `1px solid ${props.theme.infoPanel.borderColor}`};
props.withTabs ? "none" : `1px solid ${props.theme.infoPanel.borderColor}`};
.main {
height: ${(props) => getMainHeight(props)};
min-height: ${(props) => getMainHeight(props)};
@ -91,16 +89,13 @@ const StyledInfoPanelHeader = styled.div`
`}
}
.submenu {
.tabs {
display: flex;
width: 100%;
justify-content: center;
.sticky {
display: flex;
flex-direction: column;
align-items: center;
.bottom-line {
background-color: ${(props) => props.theme.infoPanel.borderColor};
.scroll-body > div {
justify-content: center;
}
}
}

View File

@ -49,11 +49,11 @@ class FilesTableHeader extends React.Component {
columnStorageName,
columnInfoPanelStorageName,
isPublicRoom,
isFrame,
isRecentTab,
isDefaultRoomsQuotaSet,
showStorageInfo,
isArchiveFolder,
tableStorageName,
} = this.props;
const defaultColumns = [];
@ -358,7 +358,7 @@ class FilesTableHeader extends React.Component {
}
let columns = getColumns(defaultColumns);
const storageColumns = localStorage.getItem(this.props.tableStorageName);
const storageColumns = localStorage.getItem(tableStorageName);
const splitColumns = storageColumns && storageColumns.split(",");
const resetColumnsSize =
(splitColumns && splitColumns.length !== columns.length) || !splitColumns;
@ -370,6 +370,7 @@ class FilesTableHeader extends React.Component {
this.setState({
columns,
resetColumnsSize,
tableStorageName,
columnStorageName,
columnInfoPanelStorageName,
});
@ -377,6 +378,7 @@ class FilesTableHeader extends React.Component {
this.state = {
columns,
resetColumnsSize,
tableStorageName,
columnStorageName,
columnInfoPanelStorageName,
};
@ -541,12 +543,12 @@ class FilesTableHeader extends React.Component {
setHideColumns,
isFrame,
showSettings,
tableStorageName,
} = this.props;
const {
columns,
resetColumnsSize,
tableStorageName,
columnStorageName,
columnInfoPanelStorageName,
} = this.state;

View File

@ -32,7 +32,6 @@ import Error520 from "../../../../components/Error520Wrapper";
import { inject, observer } from "mobx-react";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import config from "PACKAGE_FILE";
import { Submenu } from "@docspace/shared/components/submenu";
import PersonalSettings from "./CommonSettings";
import GeneralSettings from "./AdminSettings";
import { tablet } from "@docspace/shared/utils";

View File

@ -30,7 +30,7 @@ import { inject, observer } from "mobx-react";
import { SectionSubmenuSkeleton } from "@docspace/shared/skeletons/sections";
import { useTranslation } from "react-i18next";
const AccountsSubmenu = ({
const AccountsTabs = ({
showBodyLoader,
setPeopleSelection,
setGroupsSelection,
@ -62,10 +62,10 @@ const AccountsSubmenu = ({
if (showBodyLoader) return <SectionSubmenuSkeleton />;
return (
<Styled.AccountsSubmenu
<Styled.AccountsTabs
className="accounts-tabs"
forsedActiveItemId={isPeople ? "people" : "groups"}
data={[
selectedItemId={isPeople ? "people" : "groups"}
items={[
{
id: "people",
name: t("Common:People"),
@ -89,4 +89,4 @@ export default inject(({ peopleStore, clientLoadingStore }) => ({
setPeopleBufferSelection: peopleStore.selectionStore.setBufferSelection,
setGroupsSelection: peopleStore.groupsStore.setSelection,
setGroupsBufferSelection: peopleStore.groupsStore.setBufferSelection,
}))(observer(AccountsSubmenu));
}))(observer(AccountsTabs));

View File

@ -24,11 +24,11 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import { tablet } from "@docspace/shared/utils/device";
import styled from "styled-components";
export const AccountsSubmenu = styled(Submenu)`
export const AccountsTabs = styled(Tabs)`
/* width: 100%;
@media ${tablet} {

View File

@ -27,12 +27,12 @@
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import { SectionSubmenuSkeleton } from "@docspace/shared/skeletons/sections";
import FilesFilter from "@docspace/shared/api/files/filter";
import { getObjectByLocation } from "@docspace/shared/utils/common";
const MyDocumentsSubmenu = ({
const MyDocumentsTabs = ({
isPersonalRoom,
isRecentTab,
setFilter,
@ -41,7 +41,7 @@ const MyDocumentsSubmenu = ({
}) => {
const { t } = useTranslation(["Common", "Files"]);
const submenu = [
const tabs = [
{
id: "my",
name: t("Common:MyDocuments"),
@ -68,14 +68,17 @@ const MyDocumentsSubmenu = ({
window.DocSpace.navigate(`${url}?${filter.toUrlParams()}`);
};
const showSubmenu = (isPersonalRoom || isRecentTab) && isRoot;
const startSelect =
getObjectByLocation(window.DocSpace.location)?.folder === "recent" ? 1 : 0;
const showTabs = (isPersonalRoom || isRecentTab) && isRoot;
const startSelectId =
tabs.length &&
getObjectByLocation(window.DocSpace.location)?.folder === "recent"
? tabs[1].id
: tabs[0].id;
if (showSubmenu && showBodyLoader) return <SectionSubmenuSkeleton />;
if (showTabs && showBodyLoader) return <SectionSubmenuSkeleton />;
return showSubmenu ? (
<Submenu data={submenu} startSelect={startSelect} onSelect={onSelect} />
return showTabs ? (
<Tabs items={tabs} selectedItemId={startSelectId} onSelect={onSelect} />
) : null;
};
@ -93,4 +96,4 @@ export default inject(
isRoot,
};
},
)(observer(MyDocumentsSubmenu));
)(observer(MyDocumentsTabs));

View File

@ -24,8 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import AccountsSubmenu from "./AccountsSubmenu";
import MyDocumentsSubmenu from "./MyDocumentsSubmenu";
import AccountsTabs from "./AccountsTabs";
import MyDocumentsTabs from "./MyDocumentsTabs";
import { inject, observer } from "mobx-react";
import { useLocation } from "react-router-dom";
@ -33,8 +33,8 @@ const SectionSubmenuContent = ({ isPersonalRoom, isRecentTab }) => {
const location = useLocation();
const isAccounts = location.pathname.includes("/accounts");
if (isPersonalRoom || isRecentTab) return <MyDocumentsSubmenu />;
if (isAccounts) return <AccountsSubmenu />;
if (isPersonalRoom || isRecentTab) return <MyDocumentsTabs />;
if (isAccounts) return <AccountsTabs />;
return null;
};

View File

@ -31,4 +31,4 @@ export { default as SettingsSectionBodyContent } from "./SettingsBody";
export { default as SectionFilterContent } from "./Filter";
export { default as SectionPagingContent } from "./Paging";
export { default as SectionWarningContent } from "./Warning";
export { default as SectionSubmenuContent } from "./Submenu";
export { default as SectionSubmenuContent } from "./Tabs";

View File

@ -32,7 +32,7 @@ import { inject, observer } from "mobx-react";
import { Button } from "@docspace/shared/components/button";
import { Tooltip } from "@docspace/shared/components/tooltip";
import { Text } from "@docspace/shared/components/text";
import { TabsContainer } from "@docspace/shared/components/tabs-container";
import { Tabs, TabsTypes } from "@docspace/shared/components/tabs";
import Preview from "./Appearance/preview";
import { saveToSessionStorage, getFromSessionStorage } from "../../utils";
import ColorSchemeDialog from "./sub-components/colorSchemeDialog";
@ -138,12 +138,11 @@ const Appearance = (props) => {
const [theme, setTheme] = useState(appearanceTheme);
const array_items = useMemo(
const arrayItems = useMemo(
() => [
{
id: "light-theme",
key: "0",
title: t("Profile:LightTheme"),
name: t("Profile:LightTheme"),
content: (
<Preview
appliedColorAccent={appliedColorAccent}
@ -156,8 +155,7 @@ const Appearance = (props) => {
},
{
id: "dark-theme",
key: "1",
title: t("Profile:DarkTheme"),
name: t("Profile:DarkTheme"),
content: (
<Preview
appliedColorAccent={appliedColorAccent}
@ -799,7 +797,7 @@ const Appearance = (props) => {
onSaveColorSchemeDialog={onSaveColorSchemeDialog}
/>
<div className="header preview-header">{t("Common:Preview")}</div>
<TabsContainer elements={array_items} />
<Tabs items={arrayItems} type={TabsTypes.Secondary} />
<div className="buttons-container">
<Button

View File

@ -25,7 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect } from "react";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import { useNavigate } from "react-router-dom";
import { withTranslation } from "react-i18next";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
@ -35,15 +35,14 @@ import Customization from "./customization";
import Branding from "./branding";
import Appearance from "./appearance";
import withLoading from "SRC_DIR/HOCs/withLoading";
import LoaderSubmenu from "./sub-components/loaderSubmenu";
import LoaderTabs from "./sub-components/loaderTabs";
import { resetSessionStorage } from "../../utils";
import { DeviceType } from "@docspace/shared/enums";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const SubmenuCommon = (props) => {
const TabsCommon = (props) => {
const {
t,
tReady,
setIsLoadedSubmenu,
loadBaseInfo,
@ -103,22 +102,22 @@ const SubmenuCommon = (props) => {
);
};
const getCurrentTab = () => {
const getCurrentTabId = () => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
return currentTab !== -1 ? currentTab : 0;
const currentTab = data.find((item) => path.includes(item.id));
return currentTab !== -1 && data.length ? currentTab.id : data[0].id;
};
const currentTab = getCurrentTab();
const currentTabId = getCurrentTabId();
if (!isLoadedSubmenu) return <LoaderSubmenu />;
if (!isLoadedSubmenu) return <LoaderTabs />;
return (
<Submenu
data={data}
startSelect={currentTab}
<Tabs
items={data}
selectedItemId={currentTabId}
onSelect={(e) => onSelect(e)}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
stickyTop={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};
@ -146,4 +145,4 @@ export default inject(({ settingsStore, common }) => {
currentDeviceType,
isMobileView,
};
})(withLoading(withTranslation("Settings")(observer(SubmenuCommon))));
})(withLoading(withTranslation("Settings")(observer(TabsCommon))));

View File

@ -47,7 +47,7 @@ const StyledLoader = styled.div`
}
`;
const LoaderSubmenu = () => {
const LoaderTabs = () => {
return (
<StyledLoader>
<RectangleSkeleton width="100px" height="28px" className="loader" />
@ -56,4 +56,4 @@ const LoaderSubmenu = () => {
);
};
export default LoaderSubmenu;
export default LoaderTabs;

View File

@ -31,7 +31,7 @@ import { combineUrl } from "@docspace/shared/utils/combineUrl";
import { DeviceType } from "@docspace/shared/enums";
import ManualBackup from "./manual-backup";
import AutoBackup from "./auto-backup";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import config from "PACKAGE_FILE";
const Backup = ({ t, buttonSize, isNotPaidPeriod }) => {
@ -63,7 +63,11 @@ const Backup = ({ t, buttonSize, isNotPaidPeriod }) => {
return isNotPaidPeriod ? (
<ManualBackup buttonSize={buttonSize} />
) : (
<Submenu data={data} startSelect={data[0]} onSelect={(e) => onSelect(e)} />
<Tabs
items={data}
selectedItemId={data[0].id}
onSelect={(e) => onSelect(e)}
/>
);
};

View File

@ -25,14 +25,14 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
import { withTranslation, Trans } from "react-i18next";
import { inject, observer } from "mobx-react";
import { useTheme } from "styled-components";
import HelpReactSvgUrl from "PUBLIC_DIR/images/help.react.svg?url";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import { Link } from "@docspace/shared/components/link";
import { Text } from "@docspace/shared/components/text";
import { Box } from "@docspace/shared/components/box";
@ -58,8 +58,9 @@ const DataManagementWrapper = (props) => {
} = props;
const navigate = useNavigate();
const location = useLocation();
const [currentTab, setCurrentTab] = useState(0);
const [currentTabId, setCurrentTabId] = useState();
const [isLoading, setIsLoading] = useState(false);
const { interfaceDirection } = useTheme();
@ -120,18 +121,18 @@ const DataManagementWrapper = (props) => {
useEffect(() => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
if (currentTab !== -1) setCurrentTab(currentTab);
const currentTab = data.find((item) => path.includes(item.id));
if (currentTab !== -1 && data.length) setCurrentTabId(currentTab.id);
setIsLoading(true);
}, []);
}, [location.pathname]);
const onSelect = (e) => {
const url = isManagement()
? `/backup/${e.id}`
: `/portal-settings/backup/${e.id}`;
navigate(combineUrl(window.ClientConfig?.proxy?.url, config.homepage, url));
setCurrentTabId(e.id);
};
if (!isLoading) return <AppLoader />;
@ -139,11 +140,11 @@ const DataManagementWrapper = (props) => {
return isNotPaidPeriod ? (
<ManualBackup buttonSize={buttonSize} renderTooltip={renderTooltip} />
) : (
<Submenu
data={data}
startSelect={currentTab}
<Tabs
items={data}
selectedItemId={currentTabId}
onSelect={(e) => onSelect(e)}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
stickyTop={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -26,8 +26,8 @@
import React, { useState, useEffect } from "react";
import { withTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { Submenu } from "@docspace/shared/components/submenu";
import { useNavigate, useLocation } from "react-router-dom";
import { Tabs } from "@docspace/shared/components/tabs";
import { inject, observer } from "mobx-react";
import PortalDeactivationSection from "./portalDeactivation";
import PortalDeletionSection from "./portalDeletion";
@ -40,8 +40,9 @@ const DeleteData = (props) => {
const { t, isNotPaidPeriod, tReady } = props;
const navigate = useNavigate();
const location = useLocation();
const [currentTab, setCurrentTab] = useState(0);
const [currentTabId, setCurrentTabId] = useState();
const [isLoading, setIsLoading] = useState(false);
const data = [
@ -59,10 +60,11 @@ const DeleteData = (props) => {
useEffect(() => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
if (currentTab !== -1) setCurrentTab(currentTab);
const currentTab = data.find((item) => path.includes(item.id));
if (currentTab !== -1 && data.length) setCurrentTabId(currentTab.id);
setIsLoading(true);
}, []);
}, [location.pathname]);
const onSelect = (e) => {
navigate(
@ -72,15 +74,16 @@ const DeleteData = (props) => {
`/portal-settings/delete-data/${e.id}`,
),
);
setCurrentTabId(e.id);
};
if (!isLoading || !tReady) return <DeleteDataLoader />;
return isNotPaidPeriod ? (
<PortalDeletionSection />
) : (
<Submenu
data={data}
startSelect={currentTab}
<Tabs
items={data}
selectedItemId={currentTabId}
onSelect={(e) => onSelect(e)}
/>
);

View File

@ -146,7 +146,7 @@ const PortalIntegration = (props) => {
title: PRODUCT_NAME,
description: t("PortalDescription", { productName: PRODUCT_NAME }),
image: theme.isBase ? PortalImg : PortalImgDark,
handleOnClick: navigateToDocspace,
handleOnClick: navigateToPortal,
},
{
title: t("Common:PublicRoom"),

View File

@ -39,8 +39,9 @@ export const SDKContainer = styled(Box)`
width: 100%;
`}
.tabs_body {
.tabs-body {
height: calc(100lvh - 260px);
display: block;
}
.linkHelp {

View File

@ -26,7 +26,7 @@
import { useState, useEffect } from "react";
import { objectToGetParams } from "@docspace/shared/utils/common";
import { TabsContainer } from "@docspace/shared/components/tabs-container";
import { Tabs, TabsTypes } from "@docspace/shared/components/tabs";
import { CodeToInsert } from "./CodeToInsert";
import { GetCodeBlock } from "./GetCodeBlock";
@ -56,13 +56,13 @@ export const PreviewBlock = ({
);
const dataTabs = [
{
key: "preview",
title: t("Common:Preview"),
id: "preview",
name: t("Common:Preview"),
content: preview,
},
{
key: "code",
title: t("Code"),
id: "code",
name: t("Code"),
content: code,
},
];
@ -82,9 +82,10 @@ export const PreviewBlock = ({
return showPreview ? (
<Preview>
<TabsContainer
<Tabs
type={TabsTypes.Secondary}
onSelect={loadCurrentFrame}
elements={dataTabs}
items={dataTabs}
isDisabled={isDisabled}
/>
</Preview>

View File

@ -26,7 +26,7 @@
import React from "react";
import styled from "styled-components";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import RequestDetails from "./RequestDetails";
import ResponseDetails from "./ResponseDetails";
@ -34,7 +34,7 @@ import { useTranslation } from "react-i18next";
import { isMobile } from "@docspace/shared/utils";
import { inject, observer } from "mobx-react";
const SubmenuWrapper = styled.div`
const TabsWrapper = styled.div`
.sticky {
z-index: 3;
@ -61,9 +61,9 @@ const MessagesDetails = ({ eventDetails }) => {
}
return (
<SubmenuWrapper>
<Submenu data={menuData} startSelect={0} />
</SubmenuWrapper>
<TabsWrapper>
<Tabs items={menuData} />
</TabsWrapper>
);
};

View File

@ -26,14 +26,14 @@
import React, { useEffect, useState, useTransition, Suspense } from "react";
import styled, { css } from "styled-components";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import { Box } from "@docspace/shared/components/box";
import { inject, observer } from "mobx-react";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import config from "PACKAGE_FILE";
import { useNavigate } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
import JavascriptSDK from "./JavascriptSDK";
import Webhooks from "./Webhooks";
@ -48,17 +48,13 @@ import PluginSDK from "./PluginSDK";
import { Badge } from "@docspace/shared/components/badge";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const StyledSubmenu = styled(Submenu)`
.sticky {
z-index: 201;
}
`;
const DeveloperToolsWrapper = (props) => {
const { loadBaseInfo, currentDeviceType } = props;
const navigate = useNavigate();
const location = useLocation();
const [isLoading, setIsLoading] = useState(false);
const [currentTabId, setCurrentTabId] = useState();
const { t, ready } = useTranslation([
"JavascriptSdk",
@ -113,21 +109,19 @@ const DeveloperToolsWrapper = (props) => {
},
];
const [currentTab, setCurrentTab] = useState(
data.findIndex((item) => location.pathname.includes(item.id)),
);
const load = async () => {
//await loadBaseInfo();
};
useEffect(() => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
if (currentTab !== -1) {
setCurrentTab(currentTab);
const currentTab = data.find((item) => path.includes(item.id));
if (currentTab !== -1 && data.length) {
setCurrentTabId(currentTab.id);
}
}, []);
setIsLoading(true);
}, [location.pathname]);
useEffect(() => {
ready && startTransition(load);
@ -141,19 +135,18 @@ const DeveloperToolsWrapper = (props) => {
`/portal-settings/developer-tools/${e.id}`,
),
);
setCurrentTabId(e.id);
};
const loaders = [<SSOLoader />, <AppLoader />];
if (!isLoading) return <SSOLoader />;
return (
<Suspense fallback={loaders[currentTab] || <AppLoader />}>
<StyledSubmenu
data={data}
startSelect={currentTab}
onSelect={onSelect}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
</Suspense>
<Tabs
items={data}
selectedItemId={currentTabId}
onSelect={onSelect}
stickyTop={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -25,7 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useState } from "react";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import { useNavigate } from "react-router-dom";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
@ -113,13 +113,13 @@ const IntegrationWrapper = (props) => {
});
}
const getCurrentTab = () => {
const getCurrentTabId = () => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
return currentTab !== -1 ? currentTab : 0;
const currentTab = data.find((item) => path.includes(item.id));
return currentTab !== -1 && data.length ? currentTab.id : data[0].id;
};
const currentTab = getCurrentTab();
const currentTabId = getCurrentTabId();
const onSelect = (e) => {
navigate(
@ -132,9 +132,9 @@ const IntegrationWrapper = (props) => {
};
return (
<Submenu
data={data}
startSelect={currentTab}
<Tabs
items={data}
selectedItemId={currentTabId}
onSelect={onSelect}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>

View File

@ -25,8 +25,8 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useState } from "react";
import { Submenu } from "@docspace/shared/components/submenu";
import { useNavigate } from "react-router-dom";
import { Tabs } from "@docspace/shared/components/tabs";
import { useNavigate, useLocation } from "react-router-dom";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
@ -45,9 +45,10 @@ import { PRODUCT_NAME } from "@docspace/shared/constants";
const SecurityWrapper = (props) => {
const { t, loadBaseInfo, resetIsInit, currentDeviceType } = props;
const [currentTab, setCurrentTab] = useState(0);
const [currentTabId, setCurrentTabId] = useState();
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const data = [
{
@ -81,11 +82,11 @@ const SecurityWrapper = (props) => {
useEffect(() => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
if (currentTab !== -1) setCurrentTab(currentTab);
const currentTab = data.find((item) => path.includes(item.id));
if (currentTab !== -1 && data.length) setCurrentTabId(currentTab.id);
load();
}, []);
}, [location.pathname]);
const onSelect = (e) => {
navigate(
@ -95,10 +96,11 @@ const SecurityWrapper = (props) => {
`/portal-settings/security/${e.id}`,
),
);
setCurrentTabId(e.id);
};
if (!isLoading)
return currentTab === 0 ? (
if (!isLoading && data.length)
return currentTabId === data[0].id ? (
currentDeviceType !== DeviceType.desktop ? (
<MobileSecurityLoader />
) : (
@ -107,12 +109,13 @@ const SecurityWrapper = (props) => {
) : (
<AccessLoader />
);
return (
<Submenu
data={data}
startSelect={currentTab}
<Tabs
items={data}
selectedItemId={currentTabId}
onSelect={(e) => onSelect(e)}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
stickyTop={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -32,7 +32,7 @@ import { inject, observer } from "mobx-react";
import { useNavigate } from "react-router-dom";
import { ProfileViewLoader } from "@docspace/shared/skeletons/profile";
import { Submenu } from "@docspace/shared/components/submenu";
import { Tabs } from "@docspace/shared/components/tabs";
import MainProfile from "./sub-components/main-profile";
import LoginContent from "./sub-components/LoginContent";
@ -54,7 +54,7 @@ const Wrapper = styled.div`
}
`;
const StyledSubMenu = styled(Submenu)`
const StyledTabs = styled(Tabs)`
> .sticky {
z-index: 201;
margin-inline-end: -17px;
@ -91,13 +91,13 @@ const SectionBodyContent = (props) => {
content: <FileManagement />,
});
const getCurrentTab = () => {
const getCurrentTabId = () => {
const path = location.pathname;
const currentTab = data.findIndex((item) => path.includes(item.id));
return currentTab !== -1 ? currentTab : 0;
const currentTab = data.find((item) => path.includes(item.id));
return currentTab !== -1 && data.length ? currentTab.id : data[0].id;
};
const currentTab = getCurrentTab();
const currentTabId = getCurrentTabId();
const onSelect = (e) => {
const arrayPaths = location.pathname.split("/");
@ -110,11 +110,11 @@ const SectionBodyContent = (props) => {
return (
<Wrapper>
<MainProfile />
<StyledSubMenu
data={data}
startSelect={currentTab}
<StyledTabs
items={data}
selectedItemId={currentTabId}
onSelect={onSelect}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
stickyTop={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
</Wrapper>
);

View File

@ -36,6 +36,7 @@ import {
RoomsProviderType,
ShareAccessRights,
Events,
FilterKeys,
} from "@docspace/shared/enums";
import { RoomsTypes } from "@docspace/shared/utils";
@ -50,6 +51,7 @@ import { getDaysRemaining } from "@docspace/shared/utils/common";
import {
MEDIA_VIEW_URL,
PDF_FORM_DIALOG_KEY,
ROOMS_PROVIDER_TYPE_NAME,
} from "@docspace/shared/constants";
import {
@ -1438,6 +1440,19 @@ class FilesStore {
filterData.pageCount = 100;
}
const defaultFilter = FilesFilter.getDefault();
const { filterType, searchInContent } = filterData;
if (typeof filterData.withSubfolders !== "boolean")
filterData.withSubfolders = defaultFilter.withSubfolders;
if (typeof searchInContent !== "boolean")
filterData.searchInContent = defaultFilter.searchInContent;
if (!Object.keys(FilterType).find((key) => FilterType[key] === filterType))
filterData.filterType = defaultFilter.filterType;
setSelectedNode([folderId + ""]);
return api.files
@ -1733,6 +1748,22 @@ class FilesStore {
if (folderId) setSelectedNode([folderId + ""]);
const defaultFilter = RoomsFilter.getDefault();
const { provider, quotaFilter, type } = filterData;
if (!ROOMS_PROVIDER_TYPE_NAME[provider])
filterData.provider = defaultFilter.provider;
if (
quotaFilter &&
quotaFilter !== FilterKeys.customQuota &&
quotaFilter !== FilterKeys.defaultQuota
)
filterData.quotaFilter = defaultFilter.quotaFilter;
if (type && !RoomsType[type]) filterData.type = defaultFilter.type;
const request = () =>
api.rooms
.getRooms(filterData, this.roomsController.signal)

View File

@ -25,12 +25,11 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import type { Metadata } from "next";
import dynamic from "next/dynamic";
import AppLoader from "@docspace/shared/components/app-loader";
import { BRAND_NAME } from "@docspace/shared/constants";
import { getData } from "@/utils/actions";
import { RootPageProps } from "@/types";
import Root from "@/components/Root";
const initialSearchParams: RootPageProps["searchParams"] = {
fileId: undefined,
@ -42,13 +41,8 @@ const initialSearchParams: RootPageProps["searchParams"] = {
editorType: undefined,
};
const Root = dynamic(() => import("@/components/Root"), {
ssr: false,
loading: () => <AppLoader />,
});
export const metadata: Metadata = {
title: "ONLYOFFICE DocEditor page",
title: `${BRAND_NAME} DocEditor page`,
description: "",
};

View File

@ -36,9 +36,10 @@ import IConfig from "@onlyoffice/document-editor-react/dist/esm/types/model/conf
import { FolderType, ThemeKeys } from "@docspace/shared/enums";
import { getEditorTheme } from "@docspace/shared/utils";
import { EDITOR_ID } from "@docspace/shared/constants";
import { getBackUrl } from "@/utils";
import { IS_DESKTOP_EDITOR, IS_ZOOM } from "@/utils/constants";
import { IS_DESKTOP_EDITOR, IS_ZOOM, SHOW_CLOSE } from "@/utils/constants";
import { EditorProps, TGoBack } from "@/types";
import {
onSDKRequestHistoryClose,
@ -47,11 +48,11 @@ import {
onSDKWarning,
onSDKError,
onSDKRequestRename,
onOutdatedVersion,
} from "@/utils/events";
import useInit from "@/hooks/useInit";
import useEditorEvents from "@/hooks/useEditorEvents";
import useFilesSettings from "@/hooks/useFilesSettings";
import { EDITOR_ID } from "@docspace/shared/constants";
type IConfigType = IConfig & {
events?: {
@ -193,14 +194,13 @@ const Editor = ({
>["customization"] = JSON.parse(customization || "{}");
const theme = sdkCustomization?.uiTheme || user?.theme;
const showClose = document.referrer !== "" && window.history.length > 1;
if (newConfig.editorConfig)
newConfig.editorConfig.customization = {
...newConfig.editorConfig.customization,
...sdkCustomization,
goback: { ...goBack },
close: { visible: showClose, text: t("Common:CloseButton") },
close: { visible: SHOW_CLOSE, text: t("Common:CloseButton") },
uiTheme: getEditorTheme(theme as ThemeKeys),
};
@ -232,6 +232,7 @@ const Editor = ({
onDocumentStateChange,
onMetaChange,
onMakeActionLink,
onOutdatedVersion,
};
if (successAuth) {
@ -288,7 +289,7 @@ const Editor = ({
if (
(typeof window !== "undefined" &&
window.ClientConfig?.editor?.requestClose) ||
showClose ||
SHOW_CLOSE ||
IS_ZOOM
) {
newConfig.events.onRequestClose = onSDKRequestClose;
@ -299,7 +300,11 @@ const Editor = ({
}
newConfig.events.onSubmit = () => {
router.push(`/completed-form?${searchParams.toString()}`);
const origin = window.location.origin;
window.location.replace(
`${origin}/doceditor/completed-form?${searchParams.toString()}`,
);
};
return (

View File

@ -30,7 +30,6 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ErrorContainer from "@docspace/shared/components/error-container/ErrorContainer";
import AppLoader from "@docspace/shared/components/app-loader";
import { TResponse } from "@/types";
import useError from "@/hooks/useError";
@ -83,7 +82,8 @@ const Root = ({
useEffect(() => {
console.log("editor timer: ", timer);
}, [timer]);
console.log("openEdit timer: ", config?.timer);
}, [config?.timer, timer]);
useRootInit({
documentType: config?.documentType,

View File

@ -380,6 +380,7 @@ export async function openEdit(
searchParams: string,
share?: string,
) {
const startDate = new Date();
const hdrs = headers();
const cookie = hdrs.get("cookie");
@ -397,10 +398,12 @@ export async function openEdit(
const config = await res.json();
if (res.ok) {
const timer = new Date().getTime() - startDate.getTime();
config.response.editorUrl = (
config.response as IInitialConfig
).editorUrl.replace(REPLACED_URL_PATH, "");
return config.response as IInitialConfig;
return { ...config.response, timer } as IInitialConfig;
}
const editorUrl =

View File

@ -38,3 +38,8 @@ export const IS_VIEW =
: false;
export const REPLACED_URL_PATH = "/web-apps/apps/api/documents/api.js";
export const SHOW_CLOSE =
typeof document !== "undefined" &&
document.referrer !== "" &&
window.history.length > 1;

View File

@ -119,3 +119,5 @@ export const onSDKRequestRename = async (
const title = (event as TRenameEvent).data;
await updateFile(id, title);
};
export const onOutdatedVersion = () => window.location.reload();

View File

@ -56,26 +56,26 @@ import { Link, LinkType } from "../link";
import { Loader, LoaderTypes } from "../loader";
import { Row } from "../row";
import { Scrollbar } from "../scrollbar";
import { TabsContainer } from "../tabs-container";
import { Tabs, TabsTypes } from "../tabs";
import { Text } from "../text";
import { Toast, toastr } from "../toast";
import { ToggleContent } from "../toggle-content";
import { Tooltip } from "../tooltip";
const ArrayItems = [
const arrayItems = [
{
key: "0",
title: "Tab 1",
id: "0",
name: "Tab 1",
content: <div>Tab 1 content</div>,
},
{
key: "1",
title: "Tab 2",
id: "1",
name: "Tab 2",
content: <div>Tab 2 content</div>,
},
{
key: "2",
title: "Tab 3",
id: "2",
name: "Tab 3",
content: <div>Tab 3 content</div>,
},
];
@ -570,11 +570,11 @@ const Template = () => (
</div>
<div>
<div style={{ padding: "8px 0" }}>
<TabsContainer
elements={ArrayItems}
isDisabled={false}
<Tabs
items={arrayItems}
type={TabsTypes.Secondary}
onSelect={() => {}}
selectedItem={0}
selectedItemId={arrayItems[0].id}
/>
</div>
<div style={{ padding: "16px 0" }}>

View File

@ -28,12 +28,13 @@ import styled, { css } from "styled-components";
import ArrowRightSvg from "PUBLIC_DIR/images/arrow.right.react.svg";
import { Tabs } from "../tabs";
import { Base } from "../../themes";
import { mobile } from "../../utils/device";
import { ComboBox } from "../combobox";
import { Text } from "../text";
import { Submenu } from "../submenu";
import { AccessRightSelect } from "../access-right-select";
const accessComboboxStyles = css`
@ -251,7 +252,7 @@ const StyledItem = styled.div<{
}
.label {
color: #a3a9ae;
color: ${props.theme.selector.item.disableTextColor};
}
.disabled-text {
@ -468,7 +469,7 @@ const StyledAccessSelector = styled(AccessRightSelect)`
${accessComboboxStyles}
`;
const StyledTabs = styled(Submenu)`
const StyledTabs = styled(Tabs)`
padding: 0 16px;
margin-bottom: 16px;

View File

@ -32,7 +32,7 @@ import { TFileSecurity, TFolderSecurity } from "../../api/files/types";
import { TRoomSecurity } from "../../api/rooms/types";
import { AvatarRole } from "../avatar";
import { TSubmenuItem } from "../submenu";
import { TTabItem } from "../tabs";
import { SelectorAccessRightsMode } from "./Selector.enums";
@ -106,7 +106,7 @@ export type TSelectorBreadCrumbs =
// tabs
export type TWithTabs =
| { withTabs: true; tabsData: TSubmenuItem[]; activeTabId: string }
| { withTabs: true; tabsData: TTabItem[]; activeTabId: string }
| { withTabs?: undefined; tabsData?: undefined; activeTabId?: undefined };
// select all

View File

@ -226,11 +226,7 @@ const Body = ({
) : null}
{withTabs && tabsData && (
<StyledTabs
startSelect={0}
data={tabsData}
forsedActiveItemId={activeTabId}
/>
<StyledTabs items={tabsData} selectedItemId={activeTabId} />
)}
{isSearchLoading || isBreadCrumbsLoading ? (

View File

@ -1,71 +0,0 @@
# Submenu
### Usage
```js
import { Submenu } from "@docspace/shared/components/submenu";
```
```jsx
<Submenu
data={[
{
id: "FileInput",
name: "File Input",
content: (
<FileInput
accept={[".doc", ".docx"]}
id="file-input-id"
name="demoFileInputName"
onInput={function noRefCheck() {}}
placeholder="Input file"
/>
),
},
{
id: "ToggleButton",
name: "Toggle Button",
content: (
<ToggleButton
className="toggle className"
id="toggle id"
label="label text"
onChange={() => {}}
/>
),
},
]}
startSelect={1}
/>
```
#### Data is an array of objects with following fields:
- id - unique id
- name - header in submenu
- content - HTML object that will be rendered under submenu
##### Example:
```jsx
{
id: "FileInput",
name: "File Input",
content: (
<FileInput
accept={[".doc", ".docx"]}
id="file-input-id"
name="demoFileInputName"
onInput={function noRefCheck() {}}
placeholder="Input file"
/>
),
},
```
### Properties
| Props | Type | Required | Values | Default | Description |
| ------------- | :-------------: | :------: | :----: | :-----: | ----------------------------------------------------------------------------- |
| `data` | `array` | ✅ | - | - | List of elements |
| `startSelect` | `obj`, `number` | - | - | 0 | Object from data that will be chosen first **OR** Its index in **data** array |

View File

@ -1,182 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
import { Base, TColorScheme } from "../../themes";
export const StyledSubmenu = styled.div<{ topProps?: string }>`
display: flex;
flex-direction: column;
.scrollbar {
width: 100%;
height: auto;
}
.text {
width: auto;
display: inline-block;
position: absolute;
}
.sticky {
position: sticky;
top: ${(props) => (props.topProps ? props.topProps : 0)};
background: ${(props) => props.theme.submenu.backgroundColor};
z-index: 1;
}
.sticky-indent {
height: 20px;
}
`;
StyledSubmenu.defaultProps = { theme: Base };
export const StyledSubmenuBottomLine = styled.div`
height: 1px;
width: 100%;
margin-top: -1px;
background: ${(props) => props.theme.submenu.lineColor};
`;
StyledSubmenuBottomLine.defaultProps = { theme: Base };
export const StyledSubmenuContentWrapper = styled.div`
width: 100%;
display: flex;
align-items: center;
`;
export const StyledSubmenuItems = styled.div`
overflow: scroll;
display: flex;
flex-direction: row;
gap: 4px;
width: max-content;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
`;
export const StyledSubmenuItem = styled.div.attrs((props) => ({
id: props.id,
}))`
scroll-behavior: smooth;
cursor: pointer;
display: flex;
gap: 4px;
flex-direction: column;
padding-top: 4px;
line-height: 20px;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: 17px;
`
: css`
&:not(:last-child) {
margin-right: 17px;
}
`}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
`;
export const StyledSubmenuItemText = styled.div<{ isActive?: boolean }>`
width: max-content;
display: flex;
.item-text {
color: ${(props) =>
props.isActive
? props.theme.submenu.activeTextColor
: props.theme.submenu.textColor};
font-weight: 600;
}
`;
StyledSubmenuItemText.defaultProps = { theme: Base };
export const StyledSubmenuItemLabel = styled.div<{ isActive?: boolean }>`
z-index: 1;
width: 100%;
height: 4px;
bottom: 0px;
border-radius: 4px 4px 0 0;
background-color: ${(props) =>
props.isActive ? props.theme.submenu.bottomLineColor : ""};
`;
StyledSubmenuItemLabel.defaultProps = { theme: Base };
export const SubmenuScroller = styled.div`
position: relative;
display: inline-block;
flex: 1 1 auto;
white-space: nowrap;
scrollbar-width: none; // Firefox
&::-webkit-scrollbar {
display: none; // Safari + Chrome
}
overflow-x: auto;
overflow-y: hidden;
`;
export const SubmenuRoot = styled.div`
overflow: hidden;
min-height: 32px;
// Add iOS momentum scrolling for iOS < 13.0
-webkit-overflow-scrolling: touch;
display: flex;
`;
export const SubmenuScrollbarSize = styled.div`
height: 32;
position: absolute;
top: -9999;
overflow-x: auto;
overflow-y: hidden;
// Hide dimensionless scrollbar on macOS
scrollbar-width: none; // Firefox
&::-webkit-scrollbar {
display: none; // Safari + Chrome
}
`;
export const StyledItemLabelTheme = styled(StyledSubmenuItemLabel)<{
$currentColorScheme?: TColorScheme;
}>`
background-color: ${(props) =>
props.isActive ? props.$currentColorScheme?.main?.accent : "none"};
&:hover {
background-color: ${(props) =>
props.isActive && props.$currentColorScheme?.main?.accent};
}
`;

View File

@ -1,203 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useRef, useState, useCallback } from "react";
import { useTheme } from "styled-components";
import { countAutoFocus, countAutoOffset } from "./Submenu.utils";
import {
StyledSubmenu,
StyledSubmenuBottomLine,
StyledSubmenuContentWrapper,
StyledSubmenuItems,
StyledSubmenuItemText,
SubmenuScroller,
SubmenuRoot,
SubmenuScrollbarSize,
StyledItemLabelTheme,
StyledSubmenuItem,
} from "./Submenu.styled";
import { ColorTheme, ThemeId } from "../color-theme";
import { SubmenuProps, TSubmenuItem } from "./Submenu.types";
const Submenu = (props: SubmenuProps) => {
const {
data,
startSelect = 0,
forsedActiveItemId,
onSelect,
topProps,
...rest
} = props;
const submenuItemsRef = useRef<HTMLDivElement | null>(null);
const [currentItem, setCurrentItem] = useState<null | TSubmenuItem>(null);
const theme = useTheme();
useEffect(() => {
if (typeof startSelect === "number")
return setCurrentItem(data[startSelect]);
setCurrentItem(startSelect);
}, [data, startSelect]);
// if (!data) return null;
const onCheckCurrentItem = useCallback(() => {
const isSelect = data.find((item: TSubmenuItem) =>
window.location.pathname.endsWith(item.id),
);
if (isSelect) setCurrentItem(isSelect);
}, [data, setCurrentItem]);
useEffect(() => {
window.addEventListener("popstate", onCheckCurrentItem);
onCheckCurrentItem();
return () => {
window.removeEventListener("popstate", onCheckCurrentItem);
};
}, [onCheckCurrentItem]);
const selectSubmenuItem = (e: React.MouseEvent<HTMLDivElement>) => {
if (!forsedActiveItemId) {
const item = data.find((el) => el.id === e.currentTarget.id);
if (!item) return;
setCurrentItem(item);
const offset = countAutoFocus(item.name, data, submenuItemsRef.current);
if (submenuItemsRef.current && offset)
submenuItemsRef.current.scrollLeft += offset;
onSelect?.(item);
}
};
useEffect(() => {
if (!submenuItemsRef.current) return;
let isDown = false;
let startX: number;
let scrollLeft: number;
const mouseDown = (e: MouseEvent) => {
e.preventDefault();
isDown = true;
if (submenuItemsRef.current) {
startX = e.pageX - submenuItemsRef.current.offsetLeft;
scrollLeft = submenuItemsRef.current.scrollLeft;
}
};
const mouseMove = (e: MouseEvent) => {
if (!isDown) return;
e.preventDefault();
if (submenuItemsRef.current) {
const x = e.pageX - submenuItemsRef.current.offsetLeft;
const walk = x - startX;
submenuItemsRef.current.scrollLeft = scrollLeft - walk;
}
};
const mouseUp = () => {
const offset = countAutoOffset(data, submenuItemsRef.current);
if (submenuItemsRef.current && offset)
submenuItemsRef.current.scrollLeft += offset;
isDown = false;
};
const mouseLeave = () => (isDown = false);
const ref = submenuItemsRef.current;
ref.addEventListener("mousedown", mouseDown);
ref.addEventListener("mousemove", mouseMove);
ref.addEventListener("mouseup", mouseUp);
ref.addEventListener("mouseleave", mouseLeave);
return () => {
ref?.removeEventListener("mousedown", mouseDown);
ref?.removeEventListener("mousemove", mouseMove);
ref?.removeEventListener("mouseup", mouseUp);
ref?.removeEventListener("mouseleave", mouseLeave);
};
}, [data, submenuItemsRef]);
return (
<StyledSubmenu {...rest} topProps={topProps}>
<div className="sticky">
<SubmenuRoot>
<SubmenuScrollbarSize />
<SubmenuScroller>
<StyledSubmenuItems ref={submenuItemsRef} role="list">
{data.map((d) => {
const isActive =
d.id === (forsedActiveItemId || currentItem?.id);
return (
<StyledSubmenuItem
key={d.id}
id={d.id}
onClick={(e) => {
d.onClick?.();
selectSubmenuItem(e);
}}
>
<StyledSubmenuItemText isActive={isActive}>
<ColorTheme
{...props}
as="div"
themeId={ThemeId.SubmenuText}
className="item-text"
fontSize="13px"
fontWeight="600"
truncate={false}
isActive={isActive}
>
{d.name}
</ColorTheme>
</StyledSubmenuItemText>
<StyledItemLabelTheme
isActive={isActive}
$currentColorScheme={theme.currentColorScheme}
/>
</StyledSubmenuItem>
);
})}
</StyledSubmenuItems>
</SubmenuScroller>
</SubmenuRoot>
<StyledSubmenuBottomLine className="bottom-line" />
</div>
<div className="sticky-indent" />
<StyledSubmenuContentWrapper>
{currentItem?.content}
</StyledSubmenuContentWrapper>
</StyledSubmenu>
);
};
export { Submenu };

View File

@ -1,193 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { DomHelpers, size } from "../../utils";
import { TSubmenuItem } from "./Submenu.types";
const paddingGap = 14;
const flexGap = 4;
const offset = 32;
const wrapperPadding = DomHelpers.getViewport().width <= size.desktop ? 16 : 20;
const countText = (text: string) => {
const inputText = text;
const font = "600 13px open sans";
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (context) context.font = font;
return { id: text, width: context?.measureText(inputText).width };
};
const countItemsAndGaps = (
texts: {
id: string;
width: number | undefined;
}[],
) => {
const result: {
type: string;
length?: number;
start: number;
end: number;
id?: string;
}[] = [];
texts.forEach(({ id, width }) => {
if (!result.length)
result.push(
{
type: "gap",
length: paddingGap,
start: 0,
end: paddingGap + wrapperPadding,
},
{
id,
type: "item",
length: width,
start: paddingGap,
end: width ? paddingGap + width : paddingGap,
},
);
else {
const lastItem = result[result.length - 1];
result.push(
{
type: "gap",
length: paddingGap * 2 + flexGap,
start: lastItem.end,
end: lastItem.end + paddingGap * 2 + flexGap,
},
{
id,
type: "item",
length: width,
start: lastItem.end + paddingGap * 2 + flexGap,
end: width
? lastItem.end + paddingGap * 2 + flexGap + width
: lastItem.end + paddingGap * 2 + flexGap,
},
);
}
});
result.push({
type: "gap",
length: paddingGap,
start: result[result.length - 1].end,
end: result[result.length - 1].end + paddingGap + wrapperPadding,
});
return result;
};
const countParams = (data: TSubmenuItem[], submenuItemsRef: HTMLDivElement) => {
const refCurrent = submenuItemsRef;
const texts = data.map((d) => countText(d.name));
const itemsAndGaps = countItemsAndGaps(texts);
const submenuWidth = refCurrent.offsetWidth;
const marker = refCurrent.scrollLeft + submenuWidth - wrapperPadding;
const [itemOnMarker] = itemsAndGaps.filter(
(obj) => obj.start < marker && marker < obj.end,
);
return [marker, itemsAndGaps, itemOnMarker];
};
export const countAutoOffset = (
data: TSubmenuItem[],
submenuItemsRef: HTMLDivElement | null,
) => {
if (submenuItemsRef) {
const [marker, itemsAndGaps, itemOnMarker] = countParams(
data,
submenuItemsRef,
);
if (itemOnMarker === undefined) return 0;
if (
!Array.isArray(itemOnMarker) &&
Array.isArray(itemsAndGaps) &&
typeof marker === "number" &&
typeof itemOnMarker !== "number"
) {
if (
itemOnMarker.type === "gap" &&
itemOnMarker !== itemsAndGaps[itemsAndGaps.length - 1]
)
return itemOnMarker.end - marker + offset - wrapperPadding;
if (itemOnMarker.type === "item" && marker - itemOnMarker.start < 32) {
return -(marker - itemOnMarker.start - offset) - wrapperPadding;
}
if (
itemOnMarker.type === "item" &&
itemOnMarker.end - marker < 7.5 &&
itemOnMarker !== itemsAndGaps[itemsAndGaps.length - 2]
) {
return itemOnMarker.end - marker + offset * 2 - wrapperPadding;
}
return 0;
}
}
};
export const countAutoFocus = (
itemId: string,
data: TSubmenuItem[],
submenuItemsRef: HTMLDivElement | null,
) => {
if (submenuItemsRef) {
const [marker, itemsAndGaps, itemOnMarker] = countParams(
data,
submenuItemsRef,
);
if (
!Array.isArray(itemOnMarker) &&
Array.isArray(itemsAndGaps) &&
typeof marker === "number" &&
typeof itemOnMarker !== "number"
) {
const [focusedItem] = itemsAndGaps.filter((obj) => obj.id === itemId);
const submenuWidth = submenuItemsRef.offsetWidth;
if (itemOnMarker?.id && focusedItem.id === itemOnMarker.id)
return focusedItem.end - marker;
if (
focusedItem.start < marker - submenuWidth ||
focusedItem.start - offset < marker - submenuWidth
)
return (
focusedItem.start - marker + submenuWidth - wrapperPadding - offset
);
return 0;
}
}
};

View File

@ -1,90 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { Canvas, Meta, Story } from "@storybook/blocks";
import Submenu from "./";
import * as stories from "./submenu.stories.js";
<Meta
title="Components/Submenu"
component={Submenu}
argTypes={{
data: { required: true },
}}
/>
# Submenu
<Canvas>
<Story story={stories.Default} name="Default" />
</Canvas>
### Usage
```js
import {Submenu } from "@docspace/shared/components";
```
```jsx
<Submenu
data={[
{
id: "FileInput",
name: "File Input",
content: (
<FileInput
accept={[".doc, .docx"]}
id="file-input-id"
name="demoFileInputName"
onInput={function noRefCheck() {}}
placeholder="Input file"
/>
),
},
{
id: "ToggleButton",
name: "Toggle Button",
content: (
<ToggleButton
className="toggle className"
id="toggle id"
label="label text"
onChange={() => {}}
/>
),
},
]}
startSelect={1}
/>
```
### Properties
| Props | Type | Required | Values | Default | Description |
| ------------- | :-------------: | :------: | :----: | :-----: | ----------------------------------------------------------------------------------- |
| `data` | `array` | ✅ | - | - | List of elements |
| `startSelect` | `obj`, `number` | - | - | 0 | Object from **`data`** **OR** Its index in **`data`** that will be a default select |

View File

@ -1,80 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
// // @ts-expect-error TS(7016): Could not find a declaration file for module 'enzy... Remove this comment to see the full error message
// import { mount, shallow } from "enzyme";
// import React from "react";
// import Submenu from "./";
// import { testData, testStartSelect } from "./data";
// const props = {
// data: testData,
// startSelect: testStartSelect,
// };
// const onlyData = {
// data: testData,
// };
describe("<Submenu />", () => {
it("renders without error", () => {
// const wrapper = mount(<Submenu {...props} />);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper).toExist(true);
});
// // @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
// it("gets data prop", () => {
// const wrapper = mount(<Submenu {...onlyData} />);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper.prop("data")).toEqual(testData);
// });
// // @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
// it("doesnt render without data prop", () => {
// const wrapper = mount(<Submenu {...props} />);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper).toExist(false);
// });
// // @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
// it("gets startSelect prop", () => {
// const wrapper = mount(<Submenu {...props} />);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper.prop("startSelect")).toEqual(testStartSelect);
// });
// // @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
// it("selects first data item as currentItem without startSelect prop", () => {
// const wrapper = shallow(<Submenu {...onlyData} />)
// .find("styled-submenu__StyledSubmenuContentWrapper")
// .childAt(0);
// const currentItemWrapper = shallow(testData[0].content);
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(wrapper.debug()).toEqual(currentItemWrapper.debug());
// });
});

View File

@ -478,10 +478,10 @@ const StyledTableCell = styled.div<{ hasAccess?: boolean; checked?: boolean }>`
display: flex;
align-items: center;
${({ theme }) =>
/* ${({ theme }) =>
theme.interfaceDirection === "rtl"
? `padding-left: 30px;`
: `padding-right: 30px;`}
: `padding-right: 30px;`} */
.react-svg-icon svg {
margin-top: 2px;

View File

@ -1,85 +0,0 @@
# TabContainer
Custom Tabs menu
### Usage
```js
import { TabContainer } from "@docspace/shared/components";
```
```js
const array_items = [
{
key: "0",
title: "Title1",
content: (
<div>
<div>
<button>BUTTON</button>
</div>
<div>
<button>BUTTON</button>
</div>
<div>
<button>BUTTON</button>
</div>
</div>
),
},
{
key: "1",
title: "Title2",
content: (
<div>
<div>
<label>LABEL</label>
</div>
<div>
<label>LABEL</label>
</div>
<div>
<label>LABEL</label>
</div>
</div>
),
},
{
key: "2",
title: "Title3",
content: (
<div>
<div>
<input></input>
</div>
<div>
<input></input>
</div>
<div>
<input></input>
</div>
</div>
),
},
];
```
```jsx
<TabContainer>{array_items}</TabContainer>
```
### TabContainer Properties
| Props | Type | Required | Values | Default | Description |
| -------------- | :-------: | :------: | :----: | :-----: | ---------------------------------- |
| `isDisabled` | `boolean` | - | - | - | Disable the TabContainer |
| `selectedItem` | `number` | - | - | `0` | Selected title of tabs container |
| `onSelect` | `func` | - | - | - | Triggered when a title is selected |
### Array Items Properties
| Props | Type | Required | Values | Default | Description |
| --------- | :------: | :------: | :----: | :-----: | --------------------- |
| `id` | `string` | ✅ | - | - | Index of object array |
| `title` | `string` | ✅ | - | - | Tabs title |
| `content` | `object` | ✅ | - | - | Content in Tab |

View File

@ -1,120 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { Story, ArgsTable, Canvas, Meta } from "@storybook/blocks";
import * as stories from "./TabsContainer.stories.tsx";
import { TabContainer } from "./TabsContainer.tsx";
<Meta
title="Components/TabContainer"
component={TabContainer}
parameters={{
source: {
code: stories.basic,
},
}}
argTypes={{
onSelect: { action: "onSelect. Selected items: " },
}}
/>
# TabContainer
Custom Tabs menu
<Canvas>
<Story story={stories.basic} name="Default" />
</Canvas>
<ArgsTable story="Default" />
### Array Items Properties
| Props | Type | Required | Values | Default | Description |
| --------- | :------: | :------: | :----: | :-----: | --------------------- |
| `id` | `string` | ✅ | - | - | Index of object array |
| `title` | `string` | ✅ | - | - | Tabs title |
| `content` | `object` | ✅ | - | - | Content in Tab |
```js
const array_items = [
{
key: "0",
title: "Title1",
content: (
<div>
<div>
<button>BUTTON</button>
</div>
<div>
<button>BUTTON</button>
</div>
<div>
<button>BUTTON</button>
</div>
</div>
),
},
{
key: "1",
title: "Title2",
content: (
<div>
<div>
<label>LABEL</label>
</div>
<div>
<label>LABEL</label>
</div>
<div>
<label>LABEL</label>
</div>
</div>
),
},
{
key: "2",
title: "Title3",
content: (
<div>
<div>
<input></input>
</div>
<div>
<input></input>
</div>
<div>
<input></input>
</div>
</div>
),
},
];
```
```jsx
<TabContainer elements={array_items} />
```

View File

@ -1,493 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import styled from "styled-components";
import { Meta, StoryObj } from "@storybook/react";
import { TabsContainer } from "./TabsContainer";
import { TElement, TabsContainerProps } from "./TabsContainer.types";
const meta = {
title: "Components/TabsContainer",
component: TabsContainer,
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/ZiW5KSwb4t7Tj6Nz5TducC/UI-Kit-DocSpace-1.0.0?type=design&node-id=638-4439&mode=design&t=TBNCKMQKQMxr44IZ-0",
},
},
} satisfies Meta<typeof TabsContainer>;
type Story = StoryObj<typeof TabsContainer>;
export default meta;
const arrayItems = [
{
key: "tab0",
title: "Title1",
content: (
<div>
<div>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
</div>
<div>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
</div>
<div>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
</div>
</div>
),
},
{
key: "tab1",
title: "Title2",
content: (
<div>
<div>
<label>LABEL</label> <label>LABEL</label> <label>LABEL</label>
</div>
<div>
<label>LABEL</label> <label>LABEL</label> <label>LABEL</label>
</div>
<div>
<label>LABEL</label> <label>LABEL</label> <label>LABEL</label>
</div>
</div>
),
},
{
key: "tab2",
title: "Title3",
content: (
<div>
<div>
<input /> <input /> <input />
</div>
<div>
<input /> <input /> <input />
</div>
<div>
<input /> <input /> <input />
</div>
</div>
),
},
{
key: "tab3",
title: "Title4",
content: (
<div>
<div>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
</div>
<div>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
</div>
<div>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
<button type="button">BUTTON</button>
</div>
</div>
),
},
{
key: "tab4",
title: "Title5",
content: (
<div>
<div>
<label>LABEL</label> <label>LABEL</label> <label>LABEL</label>
</div>
<div>
<label>LABEL</label> <label>LABEL</label> <label>LABEL</label>
</div>
<div>
<label>LABEL</label> <label>LABEL</label> <label>LABEL</label>
</div>
</div>
),
},
];
const scrollArrayItems = [
{
key: "tab0",
title: "First long tab container",
content: (
<>
<label>Tab_0 Tab_0 Tab_0</label>
<br />
<label>Tab_0 Tab_0 Tab_0</label>
<br />
<label>Tab_0 Tab_0 Tab_0</label>
</>
),
},
{
key: "tab1",
title: "Short",
content: (
<>
<label>Tab_1 Tab_1 Tab_1</label>
<br />
<label>Tab_1 Tab_1 Tab_1</label>
<br />
<label>Tab_1 Tab_1 Tab_1</label>
</>
),
},
{
key: "tab2",
title: "Second long tab container",
content: (
<>
<label>Tab_2 Tab_2 Tab_2</label>
<br />
<label>Tab_2 Tab_2 Tab_2</label>
<br />
<label>Tab_2 Tab_2 Tab_2</label>
</>
),
},
{
key: "tab3",
title: "Short2",
content: (
<>
<label>Tab_3 Tab_3 Tab_3</label>
<br />
<label>Tab_3 Tab_3 Tab_3</label>
<br />
<label>Tab_3 Tab_3 Tab_3</label>
</>
),
},
{
key: "tab4",
title: "Third long tab container header",
content: (
<>
<label>Tab_4 Tab_4 Tab_4</label>
<br />
<label>Tab_4 Tab_4 Tab_4</label>
<br />
<label>Tab_4 Tab_4 Tab_4</label>
</>
),
},
{
key: "tab5",
title: "Short3",
content: (
<>
<label>Tab_5 Tab_5 Tab_5</label>
<br />
<label>Tab_5 Tab_5 Tab_5</label>
<br />
<label>Tab_5 Tab_5 Tab_5</label>
</>
),
},
{
key: "tab6",
title: "tab container",
content: (
<>
<label>Tab_6 Tab_6 Tab_6</label>
<br />
<label>Tab_6 Tab_6 Tab_6</label>
<br />
<label>Tab_6 Tab_6 Tab_6</label>
</>
),
},
{
key: "tab7",
title: "Very long tabs-container field",
content: (
<>
<label>Tab_7 Tab_7 Tab_7</label>
<br />
<label>Tab_7 Tab_7 Tab_7</label>
<br />
<label>Tab_7 Tab_7 Tab_7</label>
</>
),
},
{
key: "tab8",
title: "tab container",
content: (
<>
<label>Tab_8 Tab_8 Tab_8</label>
<br />
<label>Tab_8 Tab_8 Tab_8</label>
<br />
<label>Tab_8 Tab_8 Tab_8</label>
</>
),
},
{
key: "tab9",
title: "Short_04",
content: (
<>
<label>Tab_9 Tab_9 Tab_9</label>
<br />
<label>Tab_9 Tab_9 Tab_9</label>
<br />
<label>Tab_9 Tab_9 Tab_9</label>
</>
),
},
{
key: "tab10",
title: "Short__05",
content: (
<>
<label>Tab_10 Tab_10 Tab_10</label>
<br />
<label>Tab_10 Tab_10 Tab_10</label>
<br />
<label>Tab_10 Tab_10 Tab_10</label>
</>
),
},
{
key: "tab11",
title: "TabsContainer",
content: (
<>
<label>Tab_11 Tab_11 Tab_11</label>
<br />
<label>Tab_11 Tab_11 Tab_11</label>
<br />
<label>Tab_11 Tab_11 Tab_11</label>
</>
),
},
];
const tabsItems = [
{
key: "tab0",
title: "Title00000000",
content: (
<>
<label>Tab_0 Tab_0 Tab_0</label>
<br />
<label>Tab_0 Tab_0 Tab_0</label>
<br />
<label>Tab_0 Tab_0 Tab_0</label>
</>
),
},
{
key: "tab1",
title: "Title00000001",
content: (
<>
<label>Tab_1 Tab_1 Tab_1</label>
<br />
<label>Tab_1 Tab_1 Tab_1</label>
<br />
<label>Tab_1 Tab_1 Tab_1</label>
</>
),
},
{
key: "tab2",
title: "Title00000002",
content: (
<>
<label>Tab_2 Tab_2 Tab_2</label>
<br />
<label>Tab_2 Tab_2 Tab_2</label>
<br />
<label>Tab_2 Tab_2 Tab_2</label>
</>
),
},
{
key: "tab3",
title: "Title00000003",
content: (
<>
<label>Tab_3 Tab_3 Tab_3</label>
<br />
<label>Tab_3 Tab_3 Tab_3</label>
<br />
<label>Tab_3 Tab_3 Tab_3</label>
</>
),
},
{
key: "tab4",
title: "Title00000004",
content: (
<>
<label>Tab_4 Tab_4 Tab_4</label>
<br />
<label>Tab_4 Tab_4 Tab_4</label>
<br />
<label>Tab_4 Tab_4 Tab_4</label>
</>
),
},
{
key: "tab5",
title: "Title00000005",
content: (
<>
<label>Tab_5 Tab_5 Tab_5</label>
<br />
<label>Tab_5 Tab_5 Tab_5</label>
<br />
<label>Tab_5 Tab_5 Tab_5</label>
</>
),
},
{
key: "tab6",
title: "Title00000006",
content: (
<>
<label>Tab_6 Tab_6 Tab_6</label>
<br />
<label>Tab_6 Tab_6 Tab_6</label>
<br />
<label>Tab_6 Tab_6 Tab_6</label>
</>
),
},
{
key: "tab7",
title: "Title00000007",
content: (
<>
<label>Tab_7 Tab_7 Tab_7</label>
<br />
<label>Tab_7 Tab_7 Tab_7</label>
<br />
<label>Tab_7 Tab_7 Tab_7</label>
</>
),
},
{
key: "tab8",
title: "Title00000008",
content: (
<>
<label>Tab_8 Tab_8 Tab_8</label>
<br />
<label>Tab_8 Tab_8 Tab_8</label>
<br />
<label>Tab_8 Tab_8 Tab_8</label>
</>
),
},
{
key: "tab9",
title: "Title00000009",
content: (
<>
<label>Tab_9 Tab_9 Tab_9</label>
<br />
<label>Tab_9 Tab_9 Tab_9</label>
<br />
<label>Tab_9 Tab_9 Tab_9</label>
</>
),
},
];
const StyledTitle = styled.h5.attrs({ dir: "auto" })`
text-align: ${({ theme }) =>
theme.interfaceDirection === "rtl" ? `right` : `left`};
`;
const Template = ({ onSelect, ...args }: TabsContainerProps) => {
return (
<div>
<StyledTitle style={{ marginBottom: 20 }}>
Base TabsContainer:
</StyledTitle>
<TabsContainer
{...args}
onSelect={(index: TElement) => onSelect(index)}
selectedItem={arrayItems.indexOf(arrayItems[0])}
elements={arrayItems}
/>
<div style={{ marginTop: 32, maxWidth: 430 }}>
<StyledTitle style={{ marginTop: 100, marginBottom: 20 }}>
Autoscrolling with different tab widths:
</StyledTitle>
<TabsContainer
{...args}
selectedItem={3}
elements={scrollArrayItems}
onSelect={(index: TElement) => onSelect(index)}
/>
</div>
<div style={{ marginTop: 32, maxWidth: 430 }}>
<StyledTitle style={{ marginTop: 100, marginBottom: 20 }}>
Autoscrolling with the same tabs width:
</StyledTitle>
<TabsContainer
{...args}
selectedItem={5}
elements={tabsItems}
onSelect={(index: TElement) => onSelect(index)}
/>
</div>
</div>
);
};
export const basic: Story = {
render: (args) => <Template {...args} />,
args: {
elements: tabsItems,
isDisabled: false,
selectedItem: 0,
onSelect: () => {},
},
};

View File

@ -1,130 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
import { NoUserSelect } from "../../utils";
import { Base, TColorScheme } from "../../themes";
import { Scrollbar } from "../scrollbar";
const StyledScrollbar = styled(Scrollbar)`
width: ${(props) => props.theme.tabsContainer.scrollbar.width} !important;
height: ${(props) => props.theme.tabsContainer.scrollbar.height} !important;
`;
StyledScrollbar.defaultProps = { theme: Base };
const NavItem = styled.div`
position: relative;
white-space: nowrap;
display: flex;
gap: 8px;
`;
NavItem.defaultProps = { theme: Base };
const Label = styled.div<{ isDisabled?: boolean; selected?: boolean }>`
height: ${(props) => props.theme.tabsContainer.label.height};
border-radius: ${(props) => props.theme.tabsContainer.label.borderRadius};
min-width: ${(props) => props.theme.tabsContainer.label.minWidth};
width: ${(props) => props.theme.tabsContainer.label.width};
display: flex;
align-items: center;
.title_style {
text-align: center;
padding: ${(props) => props.theme.tabsContainer.label.title.padding};
overflow: ${(props) =>
props.theme.interfaceDirection === "rtl" ? "visible" : "hidden"};
${NoUserSelect};
}
${(props) =>
props.isDisabled &&
css`
pointer-events: none;
`}
${(props) =>
props.selected
? css`
border: 1px solid transparent;
cursor: default;
background-color: ${props.theme.tabsContainer.label.backgroundColor};
.title_style {
color: ${props.theme.tabsContainer.label.title.color};
}
&:hover {
cursor: pointer;
opacity: 0.85;
}
&:active {
background: ${props.theme.tabsContainer.label
.activeSelectedBackgroundColor};
}
`
: css`
border: ${props.theme.tabsContainer.label.border};
&:hover {
cursor: pointer;
background-color: ${props.theme.tabsContainer.label
.hoverBackgroundColor};
.title_style {
color: ${props.theme.tabsContainer.label.title.hoverColor};
}
}
&:active {
background-color: ${props.theme.tabsContainer.label
.activeBackgroundColor};
}
`}
${(props) =>
props.isDisabled &&
css`
${props.selected && `opacity: 0.6;`}
.title_style {
color: ${props.theme.tabsContainer.label.title.disableColor};
}
`}
`;
Label.defaultProps = { theme: Base };
const StyledLabelTheme = styled(Label)<{ $currentColorScheme?: TColorScheme }>`
background-color: ${(props) =>
props.selected && props.$currentColorScheme?.main?.accent} !important;
.title_style {
color: ${(props) =>
props.selected && props.$currentColorScheme?.text?.accent};
}
`;
export { NavItem, Label, StyledScrollbar, StyledLabelTheme };

View File

@ -1,204 +0,0 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { Text } from "../text";
import {
NavItem,
StyledLabelTheme,
StyledScrollbar,
} from "./TabsContainer.styled";
import { TElement, TabsContainerProps } from "./TabsContainer.types";
const TabsContainer = ({
elements,
isDisabled,
onSelect,
selectedItem = 0,
}: TabsContainerProps) => {
const arrayRefs = React.useRef<HTMLDivElement[]>([]);
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const [state, setState] = React.useState({
activeTab: selectedItem,
onScrollHide: true,
});
React.useEffect(() => {
const countElements = elements.length;
let item = countElements;
while (item !== 0) {
arrayRefs.current.push();
item -= 1;
}
}, [elements]);
const getWidthElements = () => {
const arrayWidths = [];
const length = arrayRefs.current.length - 1;
let widthItem = 0;
while (length + 1 !== widthItem) {
arrayWidths.push(arrayRefs.current[widthItem].offsetWidth);
widthItem += 1;
}
return arrayWidths;
};
const setTabPosition = (index: number, currentRef: HTMLDivElement) => {
const scrollElement = scrollRef.current;
if (!scrollElement) return;
const arrayOfWidths = getWidthElements(); // get tabs widths
const scrollLeft = scrollElement.scrollLeft; // get scroll position relative to left side
const staticScroll = scrollElement.scrollWidth; // get static scroll width
const containerWidth = scrollElement.clientWidth; // get main container width
const currentTabWidth = currentRef.offsetWidth;
const marginRight = 8;
// get tabs of left side
let leftTabs = 0;
let leftFullWidth = 0;
while (leftTabs !== index) {
leftTabs += 1;
leftFullWidth += arrayOfWidths[leftTabs] + marginRight;
}
leftFullWidth += arrayOfWidths[0] + marginRight;
// get tabs of right side
let rightTabs = arrayRefs.current.length - 1;
let rightFullWidth = 0;
while (rightTabs !== index - 1) {
rightFullWidth += arrayOfWidths[rightTabs] + marginRight;
rightTabs -= 1;
}
// Out of range of left side
if (leftFullWidth > containerWidth + scrollLeft) {
let prevIndex = index - 1;
let widthBlocksInContainer = 0;
while (prevIndex !== -1) {
widthBlocksInContainer += arrayOfWidths[prevIndex] + marginRight;
prevIndex -= 1;
}
const difference = containerWidth - widthBlocksInContainer;
const currentContainerWidth = currentTabWidth;
scrollElement.scrollTo({
left: difference * -1 + currentContainerWidth + marginRight,
});
}
// Out of range of left side
else if (rightFullWidth > staticScroll - scrollLeft) {
scrollElement.scrollTo({ left: staticScroll - rightFullWidth });
}
};
const titleClick = (index: number, item: TElement, ref: HTMLDivElement) => {
if (state.activeTab !== index) {
setState((s) => ({ ...s, activeTab: index }));
const newItem = { ...item };
delete newItem.content;
onSelect?.(newItem);
setTabPosition(index, ref);
}
};
const onClick = (index: number, item: TElement) => {
titleClick(index, item, arrayRefs.current[index]);
};
const setPrimaryTabPosition = React.useCallback((index: number) => {
const scrollElement = scrollRef.current;
if (!scrollElement) return;
const arrayOfWidths = getWidthElements(); // get tabs widths
const marginRight = 8;
let rightTabs = arrayRefs.current.length - 1;
let rightFullWidth = 0;
while (rightTabs !== index - 1) {
rightFullWidth += arrayOfWidths[rightTabs] + marginRight;
rightTabs -= 1;
}
rightFullWidth -= marginRight;
scrollElement.scrollTo({
left: scrollElement.scrollWidth - rightFullWidth,
});
}, []);
React.useEffect(() => {
if (state.activeTab !== 0 && arrayRefs.current[state.activeTab] !== null) {
setPrimaryTabPosition(state.activeTab);
}
}, [setPrimaryTabPosition, state.activeTab]);
const onMouseEnter = () => {
setState((s) => ({ ...s, onScrollHide: false }));
};
const onMouseLeave = () => {
setState((s) => ({ ...s, onScrollHide: true }));
};
return (
<>
<StyledScrollbar
autoHide={state.onScrollHide}
className="scrollbar"
// @ts-expect-error error from custom scrollbar
ref={scrollRef}
>
<NavItem className="className_items">
{elements.map((item, index) => (
<StyledLabelTheme
id={item.id}
onMouseMove={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={(ref) => {
if (ref) arrayRefs.current.push(ref);
}}
onClick={() => onClick(index, item)}
key={item.key}
selected={state.activeTab === index}
isDisabled={isDisabled}
>
<Text fontWeight={600} className="title_style" fontSize="13px">
{item.title}
</Text>
</StyledLabelTheme>
))}
</NavItem>
</StyledScrollbar>
<div className="tabs_body">{elements[state.activeTab].content}</div>
</>
);
};
export { TabsContainer };

View File

@ -0,0 +1,88 @@
# Tabs
### Usage
```js
import { Tabs } from "@docspace/shared/components/tabs";
```
```js
const array_items = [
{
id: "0",
name: "Title1",
content: (
<div>
<div>
<button>BUTTON</button>
</div>
<div>
<button>BUTTON</button>
</div>
<div>
<button>BUTTON</button>
</div>
</div>
),
},
{
id: "1",
name: "Title2",
content: (
<div>
<div>
<label>LABEL</label>
</div>
<div>
<label>LABEL</label>
</div>
<div>
<label>LABEL</label>
</div>
</div>
),
},
{
id: "2",
name: "Title3",
isDisabled: true;
content: (
<div>
<div>
<input></input>
</div>
<div>
<input></input>
</div>
<div>
<input></input>
</div>
</div>
),
},
];
```
```jsx
<Tabs items={array_items} />
```
### Tabs Properties
| Props | Type | Required | Values | Default | Description |
| ---------------- | :--------------------: | :------: | :----: | :-------: | ------------------------------------------------------------------- |
| `items` | `array` | ✅ | - | - | Child elements |
| `selectedItemId` | `number`, `string` | - | - | - | Selected item id of tabs |
| `theme` | `primary`, `secondary` | - | - | `primary` | Theme for displaying tabs |
| `stickyTop` | `string` | - | - | - | Tab indentation for sticky positioning |
| `onSelect` | `func` | - | - | - | Sets a callback function that is triggered when the tab is selected |
### Array Items Properties
| Props | Type | Required | Values | Default | Description |
| ------------ | :-------: | :------: | :----: | :-----: | ------------------------------------------------------------------------ |
| `id` | `string` | ✅ | - | - | Index of object array |
| `name` | `string` | ✅ | - | - | Tab text |
| `content` | `node` | ✅ | - | - | Content in Tab |
| `isDisabled` | `boolean` | - | - | - | State of tab inclusion. State only works for tabs with a secondary theme |
| `onClick` | `func` | - | - | - | Triggered when a title is selected |

View File

@ -24,6 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
export { TabsContainer } from "./TabsContainer";
export const INDEX_NOT_FOUND = -1;
export type { TElement } from "./TabsContainer.types";
export const OFFSET_RIGHT = 48;
export const OFFSET_LEFT = 48;

View File

@ -24,9 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { Submenu } from "./Submenu";
import { TSubmenuItem } from "./Submenu.types";
export type { TSubmenuItem };
export { Submenu };
export enum TabsTypes {
Primary = "primary",
Secondary = "secondary",
}

View File

@ -25,15 +25,16 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { Meta, StoryObj } from "@storybook/react";
import { Submenu } from "./Submenu";
import { Tabs } from "./Tabs";
import { data, startSelect } from "./data";
import { SubmenuProps } from "./Submenu.types";
import { data } from "./data";
import { TabsProps } from "./Tabs.types";
import { TabsTypes } from "./Tabs.enums";
const meta = {
title: "Components/Submenu",
component: Submenu,
} satisfies Meta<typeof Submenu>;
title: "Components/Tabs",
component: Tabs,
} satisfies Meta<typeof Tabs>;
type Story = StoryObj<typeof meta>;
export default meta;
@ -48,16 +49,27 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
</div>
);
const Template = (args: SubmenuProps) => (
const Template = (args: TabsProps) => (
<Wrapper>
<Submenu {...args} />
<Tabs {...args} />
</Wrapper>
);
export const Default: Story = {
render: (args) => <Template {...args} />,
args: {
data,
startSelect,
items: data,
selectedItemId: data[0].id,
onSelect: () => {},
},
};
export const Secondary: Story = {
render: (args) => <Template {...args} />,
args: {
items: data,
type: TabsTypes.Secondary,
selectedItemId: data[0].id,
onSelect: () => {},
},
};

View File

@ -0,0 +1,250 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
import { Scrollbar } from "../scrollbar";
import { Base } from "../../themes";
import { TabsTypes } from "./Tabs.enums";
export const StyledTabs = styled.div<{
stickyTop?: string;
}>`
display: flex;
flex-direction: column;
.sticky {
height: 33px;
position: sticky;
position: -webkit-sticky;
top: ${(props) => (props.stickyTop ? props.stickyTop : 0)};
background: ${(props) => props.theme.tabs.backgroundColorPrimary};
z-index: 1;
display: flex;
flex-direction: row;
}
.sticky-indent {
height: 20px;
}
.blur-ahead {
position: absolute;
height: 32px;
width: 60px;
pointer-events: none;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 20.48%,
${(props) => props.theme.tabs.gradientColor} 100%
);
transform: matrix(-1, 0, 0, 1, 0, 0);
z-index: 1;
}
.blur-back {
position: absolute;
height: 32px;
width: 60px;
right: 0;
pointer-events: none;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 20.48%,
${(props) => props.theme.tabs.gradientColor} 100%
);
z-index: 1;
}
.tabs-body {
width: 100%;
display: flex;
align-items: center;
user-select: none;
}
`;
StyledTabs.defaultProps = { theme: Base };
export const ScrollbarTabs = styled(Scrollbar)<{
$type?: TabsTypes;
}>`
.scroller {
scroll-behavior: smooth;
}
.scroll-body {
position: absolute;
padding-inline-end: 0 !important;
}
.track {
z-index: 0;
padding: 0;
}
.track > .thumb-horizontal {
height: 1px !important;
}
.thumb {
display: ${(props) =>
props.$type === TabsTypes.Primary ? "block" : "none"};
background-color: rgba(100, 104, 112, 0.2) !important;
}
.thumb:active,
.thumb.dragging {
background-color: rgba(6, 22, 38, 0.3) !important;
}
`;
export const TabList = styled.div<{
$type?: TabsTypes;
}>`
display: flex;
align-items: center;
justify-content: inherit;
width: 100%;
height: 32px;
gap: ${(props) => (props.$type === TabsTypes.Primary ? "20px" : "8px")};
border-bottom: ${(props) =>
props.$type === TabsTypes.Primary &&
css`1px solid ${props.theme.tabs.lineColor}`};
`;
TabList.defaultProps = { theme: Base };
export const Tab = styled.div<{
isActive: boolean;
isDisabled?: boolean;
$type?: TabsTypes;
}>`
display: flex;
white-space: nowrap;
flex-direction: column;
gap: 4px;
width: max-content;
height: inhert;
font-weight: 600;
line-height: 20px;
cursor: pointer;
opacity: ${(props) => (props.isDisabled && props.$type === TabsTypes.Secondary ? 0.6 : 1)};
pointer-events: ${(props) => props.isDisabled && props.$type === TabsTypes.Secondary && "none"};
user-select: none;
padding: ${(props) =>
props.$type === TabsTypes.Primary ? "4px 0 0 0" : "4px 16px"};
color: ${(props) =>
props.$type === TabsTypes.Primary
? css`
${props.isActive
? props.theme.tabs.activeTextColorPrimary ||
props.theme.currentColorScheme?.main?.accent
: props.theme.tabs.textColorPrimary}
`
: css`
${props.isActive
? props.theme.tabs.activeTextColorSecondary
: props.theme.tabs.textColorSecondary}
`};
background-color: ${(props) =>
props.$type === TabsTypes.Secondary &&
css`
${props.isActive
? props.theme.tabs.activeBackgroundColorSecondary
: props.theme.tabs.backgroundColorSecondary}
`};
border: ${(props) =>
props.$type === TabsTypes.Secondary &&
css`1px solid ${props.theme.tabs.lineColor}`};}
border-radius: ${(props) => props.$type === TabsTypes.Secondary && "16px"};
&:hover {
color: ${(props) =>
props.$type === TabsTypes.Primary &&
!props.isActive &&
props.theme.tabs.hoverTextColorPrimary};
opacity: ${(props) =>
props.$type === TabsTypes.Primary && props.isActive && 0.85};
background-color: ${(props) =>
props.$type === TabsTypes.Secondary &&
!props.isActive &&
props.theme.tabs.hoverBackgroundColorSecondary};
};
&:active {
color: ${(props) =>
props.$type === TabsTypes.Primary &&
!props.isActive &&
props.theme.tabs.pressedTextColorPrimary};
opacity: ${(props) =>
props.$type === TabsTypes.Primary && props.isActive && 1};
background-color: ${(props) =>
props.$type === TabsTypes.Secondary &&
!props.isActive &&
props.theme.tabs.pressedBackgroundColorSecondary};
};
`;
Tab.defaultProps = { theme: Base };
export const TabSubLine = styled.div<{
isActive?: boolean;
$type?: TabsTypes;
}>`
z-index: 1;
width: 100%;
height: 4px;
bottom: 0px;
border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
transition: transform 0.3s ease;
display: ${(props) => props.$type === TabsTypes.Secondary && "none"};
background-color: ${(props) =>
props.$type === TabsTypes.Primary && props.isActive
? props.theme.currentColorScheme?.main?.accent
: "transparent"};
`;

View File

@ -24,17 +24,16 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
// import React from "react";
/* import { screen, render } from "@testing-library/react";
import "@testing-library/jest-dom";
// import { screen, render } from "@testing-library/react";
// import "@testing-library/jest-dom";
import { Tabs } from "./Tabs";
import { TTabItem } from "./Tabs.types"; */
// import { TabsContainer } from "./TabsContainer";
// const ArrayItems = [
// const arrayItems: TTabItem[] = [
// {
// key: "tab0",
// title: "Title1",
// id: "tab0",
// name: "Title1",
// content: (
// <div>
// <button>BUTTON</button>
@ -44,8 +43,8 @@
// ),
// },
// {
// key: "tab1",
// title: "Title2",
// id: "tab1",
// name: "Title2",
// content: (
// <div>
// <label>LABEL</label>
@ -55,19 +54,20 @@
// ),
// },
// {
// key: "tab2",
// title: "Title3",
// id: "tab2",
// name: "Title3",
// content: (
// <div>
// <input></input>
// <input></input>
// <input></input>
// <input />
// <input />
// <input />
// </div>
// ),
// },
// {
// key: "tab3",
// title: "Title4",
// id: "tab3",
// name: "Title4",
// isDisabled: true,
// content: (
// <div>
// <button>BUTTON</button>
@ -77,8 +77,8 @@
// ),
// },
// {
// key: "tab4",
// title: "Title5",
// id: "tab4",
// name: "Title5",
// content: (
// <div>
// <label>LABEL</label>
@ -89,49 +89,28 @@
// },
// ];
describe("<TabContainer />", () => {
describe("<Tabs />", () => {
it("renders without error", () => {
// render(
// <TabContainer
// elements={[
// {
// key: "0",
// title: "Title1",
// content: (
// <div>
// <button>BUTTON</button>
// <button>BUTTON</button>
// <button>BUTTON</button>
// </div>
// ),
// },
// ]}
// />,
// );
// render(<Tabs items={arrayItems} />);
// expect(wrapper).toExist();
});
// // @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
// it("TabsContainer not re-render test", () => {
// // @ts-expect-error TS(2322): Type '{ elements: { key: string; title: string; co... Remove this comment to see the full error message
// const wrapper = mount(<TabContainer elements={array_items} />).instance();
// const shouldUpdate = wrapper.shouldComponentUpdate(
// wrapper.props,
// wrapper.state,
// );
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(shouldUpdate).toBe(false);
// });
// it("Tabs not re-render test", () => {
// const wrapper = mount(<Tabs items={arrayItems} />).instance();
// const shouldUpdate = wrapper.shouldComponentUpdate(
// wrapper.props,
// wrapper.state,
// );
// // @ts-expect-error TS(2582): Cannot find name 'it'. Do you need to install type... Remove this comment to see the full error message
// it("TabsContainer not re-render test", () => {
// // @ts-expect-error TS(2322): Type '{ elements: { key: string; title: string; co... Remove this comment to see the full error message
// const wrapper = mount(<TabContainer elements={array_items} />).instance();
// const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props, {
// ...wrapper.state,
// activeTab: 3,
// expect(shouldUpdate).toBe(false);
// });
// it("Tabs not re-render test", () => {
// const wrapper = mount(<Tabs items={arrayItems} />).instance();
// const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props, {
// ...wrapper.state,
// activeTab: 3,
// });
// expect(shouldUpdate).toBe(true);
// });
// // @ts-expect-error TS(2304): Cannot find name 'expect'.
// expect(shouldUpdate).toBe(true);
// });
});

View File

@ -0,0 +1,139 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useRef, useEffect } from "react";
import { Scrollbar as ScrollbarType } from "../scrollbar/custom-scrollbar";
import { useViewTab } from "./hooks/useViewTab";
import {
ScrollbarTabs,
StyledTabs,
Tab,
TabList,
TabSubLine,
} from "./Tabs.styled";
import { TabsProps, TTabItem } from "./Tabs.types";
import { TabsTypes } from "./Tabs.enums";
import { OFFSET_RIGHT, OFFSET_LEFT, INDEX_NOT_FOUND } from "./Tabs.constants";
const Tabs = (props: TabsProps) => {
const {
items,
selectedItemId,
type = TabsTypes.Primary,
stickyTop,
onSelect,
...rest
} = props;
let selectedItemIndex = items.findIndex((item) => item.id === selectedItemId);
if (selectedItemIndex === INDEX_NOT_FOUND) {
selectedItemIndex = 0;
}
const [currentItem, setCurrentItem] = useState<TTabItem>(
items[selectedItemIndex],
);
const tabsRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<ScrollbarType>(null);
const isViewFirstTab = useViewTab(scrollRef, tabsRef, 0);
const isViewLastTab = useViewTab(scrollRef, tabsRef, items.length - 1);
useEffect(() => {
setCurrentItem(items[selectedItemIndex]);
}, [selectedItemIndex, items]);
const scrollToTab = (index: number): void => {
if (!scrollRef.current || !tabsRef.current) return;
const containerElement = scrollRef.current.scrollerElement;
const tabElement = tabsRef.current.children[index] as HTMLDivElement;
if (!containerElement || !tabElement) return;
const containerWidth = containerElement.offsetWidth;
const tabWidth = tabElement?.offsetWidth;
const tabOffsetLeft = tabElement.offsetLeft;
if (tabOffsetLeft - OFFSET_LEFT < containerElement.scrollLeft) {
scrollRef.current.scrollTo(tabOffsetLeft - OFFSET_LEFT);
} else if (
tabOffsetLeft + tabWidth >
containerElement.scrollLeft + containerWidth
) {
scrollRef.current.scrollTo(
tabOffsetLeft - containerWidth + tabWidth + OFFSET_RIGHT,
);
}
};
const setSelectedItem = (selectedTabItem: TTabItem, index: number): void => {
setCurrentItem(selectedTabItem);
scrollToTab(index);
onSelect?.(selectedTabItem);
};
return (
<StyledTabs {...rest} stickyTop={stickyTop}>
<div className="sticky">
{!isViewFirstTab && <div className="blur-ahead" />}
<ScrollbarTabs ref={scrollRef} autoHide={false} noScrollY $type={type}>
<TabList ref={tabsRef} $type={type}>
{items.map((item, index) => {
const isActive = item.id === currentItem.id;
return (
<Tab
key={item.id}
isActive={isActive}
isDisabled={item?.isDisabled}
$type={type}
onClick={() => {
item.onClick?.();
setSelectedItem(item, index);
}}
>
{item.name}
<TabSubLine isActive={isActive} $type={type} />
</Tab>
);
})}
</TabList>
</ScrollbarTabs>
{!isViewLastTab && <div className="blur-back" />}
</div>
<div className="sticky-indent" />
<div className="tabs-body">{currentItem?.content}</div>
</StyledTabs>
);
};
export { Tabs };

View File

@ -24,21 +24,30 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
export type TSubmenuItem = {
import { TabsTypes } from "./Tabs.enums";
export type TTabItem = {
/** Element id. */
id: string;
/** Tab text. */
name: string;
/** Content that is shown when you click on the tab. */
content: React.ReactNode;
/** State of tab inclusion. State only works for tabs with a secondary theme. */
isDisabled?: boolean;
/** Sets a callback function that is triggered when the tab is selected */
onClick?: () => void;
};
export interface SubmenuProps {
/** List of the elements */
data: TSubmenuItem[];
/** Specifies the first item or the item's index to be displayed in the submenu. */
startSelect: number | TSubmenuItem;
/** Property that allows explicitly selecting content passed through an external operation */
forsedActiveItemId?: number | string;
/** Sets a callback function that is triggered when the submenu item is selected */
onSelect?: (item: TSubmenuItem) => void;
topProps?: string;
export interface TabsProps {
/** Child elements. */
items: TTabItem[];
/** Selected item of tabs. */
selectedItemId?: number | string;
/** Theme for displaying tabs. */
type?: TabsTypes;
/** Tab indentation for sticky positioning. */
stickyTop?: string;
/** Sets a callback function that is triggered when the tab is selected. */
onSelect?: (element: TTabItem) => void;
}

View File

@ -24,14 +24,13 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { TTabItem } from "./Tabs.types";
import { FileInput } from "../file-input";
import { Row } from "../row";
import { Text } from "../text";
import { InputSize } from "../text-input";
export const data = [
export const data: TTabItem[] = [
{
id: "Overview",
name: "Overview",
@ -86,13 +85,14 @@ export const data = [
),
},
{
id: "Time tracking",
name: "Time tracking",
id: "Time",
name: "Time",
content: <p>Time tracking</p>,
},
{
id: "Contacts",
name: "Contacts",
isDisabled: true,
content: <p>Contacts</p>,
},
{
@ -101,20 +101,3 @@ export const data = [
content: <p>Team</p>,
},
];
export const startSelect = data[2];
export const testData = [
{
id: "Tab1",
name: "Tab1",
content: <p>1</p>,
},
{
id: "Tab2",
name: "Tab2",
content: <p>2</p>,
},
];
export const testStartSelect = testData[1];

View File

@ -0,0 +1,64 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.s
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useEffect, useState, useRef, RefObject } from "react";
import { Scrollbar as ScrollbarType } from "../../scrollbar/custom-scrollbar";
export const useViewTab = (
containerRef: RefObject<ScrollbarType>,
tabRef: RefObject<HTMLDivElement>,
index: number,
) => {
const [isViewTab, setIsViewTab] = useState<boolean>(true);
const observerRef = useRef<IntersectionObserver>();
useEffect(() => {
const container = containerRef.current?.scrollerElement;
const trackedElement = tabRef.current?.children[index];
if (!container || !trackedElement) return;
const observerCallback: IntersectionObserverCallback = ([entry]) => {
setIsViewTab(entry.isIntersecting);
};
observerRef.current = new IntersectionObserver(observerCallback, {
root: container,
rootMargin: "4px",
threshold: 1,
});
observerRef.current.observe(trackedElement);
return () => {
if (observerRef.current) {
observerRef.current.unobserve(trackedElement);
}
};
}, [containerRef, index, tabRef]);
return isViewTab;
};

View File

@ -24,20 +24,10 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
export type TElement = {
id?: string;
key: string;
title: string;
content: React.ReactNode;
};
import { Tabs } from "./Tabs";
import { TabsTypes } from "./Tabs.enums";
import { type TTabItem, type TabsProps } from "./Tabs.types";
export interface TabsContainerProps {
/** Child elements */
elements: TElement[];
/** Disables the TabContainer */
isDisabled: boolean;
/** Sets a callback function that is triggered when the title is selected */
onSelect: (element: TElement) => void;
/** Selected title of tabs container */
selectedItem: number;
}
export { Tabs };
export { TabsTypes };
export { TTabItem, TabsProps };

View File

@ -976,36 +976,6 @@ export const getBaseTheme = () => {
},
},
tabsContainer: {
scrollbar: {
width: "100%",
height: "44px",
},
label: {
height: "30px",
border: `1px solid ${grayLightMid}`,
borderRadius: "16px",
minWidth: "fit-content",
marginRight: "8px",
width: "fit-content",
backgroundColor: blueLightMid,
hoverBackgroundColor: lightGrayHover,
disableBackgroundColor: grayLightMid,
activeBackgroundColor: grayLightMid,
activeSelectedBackgroundColor: `linear-gradient(0deg, ${blueLightMid}, ${blueLightMid}), linear-gradient(0deg, ${onWhiteColor}, ${onWhiteColor})`,
title: {
padding: "4px 16px",
overflow: "hidden",
color: white,
hoverColor: black,
disableColor: grayStrong,
},
},
},
fieldContainer: {
horizontal: {
margin: "0 0 16px 0",
@ -2148,8 +2118,10 @@ export const getBaseTheme = () => {
hoverBackground: grayLight,
selectedBackground: lightGrayHover,
inputButtonBorder: grayStrong,
inputButtonBorderHover: lightGrayDark,
inputButtonBorder: "#D0D5DA",
inputButtonBorderHover: grayMain,
disableTextColor: "#A3A9AE",
},
emptyScreen: {
@ -2964,12 +2936,23 @@ export const getBaseTheme = () => {
background: white,
},
submenu: {
lineColor: grayLightMid,
backgroundColor: white,
textColor: lightGrayDark,
activeTextColor: link,
bottomLineColor: link,
tabs: {
gradientColor: white,
lineColor: "#eceef1",
textColorPrimary: "#657077",
activeTextColorPrimary: "",
hoverTextColorPrimary: "#A3A9AE",
pressedTextColorPrimary: "#555F65",
backgroundColorPrimary: white,
textColorSecondary: "#333333",
activeTextColorSecondary: white,
backgroundColorSecondary: white,
hoverBackgroundColorSecondary: "#F3F4F4",
pressedBackgroundColorSecondary: "#ECEEF1",
activeBackgroundColorSecondary: "#265A8F",
},
hotkeys: {

View File

@ -960,36 +960,6 @@ const Dark: TTheme = {
},
},
tabsContainer: {
scrollbar: {
width: "100%",
height: "44px",
},
label: {
height: " 30px",
border: `1px solid ${grayDarkStrong}`,
borderRadius: "16px",
minWidth: "fit-content",
marginRight: "8px",
width: "fit-content",
backgroundColor: white,
hoverBackgroundColor: grayDarkStrong,
disableBackgroundColor: darkGrayLight,
activeBackgroundColor: darkGrayLight,
activeSelectedBackgroundColor: `linear-gradient(0deg, ${white}, ${white}), linear-gradient(0deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.18))`,
title: {
padding: "4px 16px",
overflow: "hidden",
color: black,
hoverColor: white,
disableColor: grayDarkStrong,
},
},
},
fieldContainer: {
horizontal: {
margin: "0 0 16px 0",
@ -2118,8 +2088,8 @@ const Dark: TTheme = {
border: `1px solid ${grayDarkStrong}`,
breadCrumbs: {
prevItemColor: darkGrayDark,
arrowRightColor: darkGrayDark,
prevItemColor: "#CCCCCC",
arrowRightColor: "#ADADAD",
},
info: {
@ -2133,8 +2103,10 @@ const Dark: TTheme = {
hoverBackground: lightDarkGrayHover,
selectedBackground: lightDarkGrayHover,
inputButtonBorder: grayDarkStrong,
inputButtonBorderHover: white,
inputButtonBorder: "#474747",
inputButtonBorderHover: grayMaxLight,
disableTextColor: "#858585",
},
emptyScreen: {
@ -2946,12 +2918,23 @@ const Dark: TTheme = {
background: black,
},
submenu: {
lineColor: grayDarkStrong,
backgroundColor: black,
activeTextColor: white,
textColor: darkGrayDark,
bottomLineColor: darkLink,
tabs: {
gradientColor: black,
lineColor: "#474747",
textColorPrimary: "#657077",
activeTextColorPrimary: white,
hoverTextColorPrimary: white,
pressedTextColorPrimary: "#CCCCCC",
backgroundColorPrimary: "#333",
textColorSecondary: "#FFFFFF",
activeTextColorSecondary: "#333333",
backgroundColorSecondary: "#333",
hoverBackgroundColorSecondary: "#474747",
pressedBackgroundColorSecondary: "#282828",
activeBackgroundColorSecondary: "#FFFFFF",
},
hotkeys: {

View File

@ -1054,25 +1054,41 @@ export const mapCulturesToArray = (
isBetaBadge: boolean = true,
i18nArg?: I18n,
) => {
let t = null;
if (i18nArg) {
const t = i18nArg.getFixedT(null, "Common");
return culturesArg.map((culture, index) => {
return {
key: culture,
label: t(`Culture_${culture}`),
icon: flagsIcons?.get(`${culture}.react.svg`),
...(isBetaBadge && { isBeta: isBetaLanguage(culture) }),
index,
};
});
t = i18nArg.getFixedT(null, "Common");
}
return culturesArg.map((culture, index) => {
return {
key: culture,
icon: flagsIcons?.get(`${culture}.react.svg`),
index,
};
let iconName = culture;
switch (culture) {
case "sr-Cyrl-RS":
case "sr-Latn-RS":
iconName = "sr";
break;
default:
break;
}
const icon = flagsIcons?.get(`${iconName}.react.svg`);
const cultureObj = t
? {
key: culture,
label: t(`Culture_${culture}`),
icon,
...(isBetaBadge && { isBeta: isBetaLanguage(culture) }),
index,
}
: {
key: culture,
icon,
index,
};
return cultureObj;
});
};

View File

@ -53,7 +53,7 @@ import RuReactSvgUrl from "PUBLIC_DIR/images/flags/ru.react.svg?url";
import SkReactSvgUrl from "PUBLIC_DIR/images/flags/sk.react.svg?url";
import SlReactSvgUrl from "PUBLIC_DIR/images/flags/sl.react.svg?url";
import SiReactSvgUrl from "PUBLIC_DIR/images/flags/si.react.svg?url";
import SrLatnRSReactSvgUrl from "PUBLIC_DIR/images/flags/sr-Latn-RS.react.svg?url";
import SrReactSvgUrl from "PUBLIC_DIR/images/flags/sr.react.svg?url";
import TrReactSvgUrl from "PUBLIC_DIR/images/flags/tr.react.svg?url";
import UkUAReactSvgUrl from "PUBLIC_DIR/images/flags/uk-UA.react.svg?url";
import ViReactSvgUrl from "PUBLIC_DIR/images/flags/vi.react.svg?url";
@ -89,7 +89,7 @@ export const flagsIcons = new Map([
["sk.react.svg", SkReactSvgUrl],
["sl.react.svg", SlReactSvgUrl],
["si.react.svg", SiReactSvgUrl],
["sr-Latn-RS.react.svg", SrLatnRSReactSvgUrl],
["sr.react.svg", SrReactSvgUrl],
["tr.react.svg", TrReactSvgUrl],
["uk-UA.react.svg", UkUAReactSvgUrl],
["vi.react.svg", ViReactSvgUrl],

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB