Merge branch 'develop' into feature/login-redesign

This commit is contained in:
Alexey Safronov 2022-03-02 12:08:11 +03:00 committed by GitHub
commit 511592ddb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 606 additions and 1 deletions

View File

@ -14,7 +14,7 @@ using ASC.Web.Studio.Core.Notify;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

View File

@ -0,0 +1,71 @@
# Submenu
### Usage
```js
import Submenu from "@appserver/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

@ -0,0 +1,126 @@
import { tablet } from "../utils/device";
import DomHelpers from "../utils/domHelpers";
const paddingGap = 14;
const flexGap = 4;
const offset = 32;
const wrapperPadding = DomHelpers.getViewport() <= tablet ? 16 : 20;
export const countAutoOffset = (data, submenuItemsRef) => {
const [marker, itemsAndGaps, itemOnMarker] = countParams(
data,
submenuItemsRef
);
if (itemOnMarker === undefined) return 0;
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, data, submenuItemsRef) => {
const [marker, itemsAndGaps, itemOnMarker] = countParams(
data,
submenuItemsRef
);
const [focusedItem] = itemsAndGaps.filter((obj) => obj.id === itemId);
const submenuWidth = submenuItemsRef.current.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;
};
const countParams = (data, submenuItemsRef) => {
const refCurrent = submenuItemsRef.current;
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];
};
const countText = (text) => {
const inputText = text;
const font = "600 13px open sans";
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = font;
return { id: text, width: context.measureText(inputText).width };
};
const countItemsAndGaps = (texts) => {
const result = [];
texts.forEach(({ id, width }) => {
if (!result.length)
result.push(
{
type: "gap",
length: paddingGap,
start: 0,
end: paddingGap + wrapperPadding,
},
{
id: id,
type: "item",
length: width,
start: paddingGap,
end: paddingGap + width,
}
);
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: id,
type: "item",
length: width,
start: lastItem.end + paddingGap * 2 + flexGap,
end: lastItem.end + paddingGap * 2 + flexGap + width,
}
);
}
});
result.push({
type: "gap",
length: paddingGap,
start: result[result.length - 1].end,
end: result[result.length - 1].end + paddingGap + wrapperPadding,
});
return result;
};

View File

@ -0,0 +1,89 @@
import React from "react";
import FileInput from "@appserver/components/file-input";
import Row from "@appserver/components/row";
import Textarea from "@appserver/components/textarea";
import Text from "../text";
export const data = [
{
id: "Overview",
name: "Overview",
content: (
<FileInput
accept=".doc, .docx"
id="file-input-id"
name="demoFileInputName"
onInput={() => {}}
placeholder="Input file"
/>
),
},
{
id: "Documents",
name: "Documents",
content: (
<Textarea
onChange={() => {}}
value="Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae"
/>
),
},
{
id: "Milestones",
name: "Milestones",
content: (
<Row
key="1"
checked
contextOptions={[
{
key: "key1",
label: "Edit",
onClick: () => {},
},
{
key: "key2",
label: "Delete",
onClick: function noRefCheck() {},
},
]}
onRowClick={function noRefCheck() {}}
onSelect={function noRefCheck() {}}
>
<Text truncate>Sample text</Text>
</Row>
),
},
{
id: "Time tracking",
name: "Time tracking",
content: <p>Time tracking</p>,
},
{
id: "Contacts",
name: "Contacts",
content: <p>Contacts</p>,
},
{
id: "Team",
name: "Team",
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,110 @@
import PropTypes from "prop-types";
import React, { useEffect, useRef, useState } from "react";
import Text from "../text";
import { countAutoFocus, countAutoOffset } from "./autoOffset";
import {
StyledSubmenu,
StyledSubmenuBottomLine,
StyledSubmenuContentWrapper,
StyledSubmenuItem,
StyledSubmenuItemLabel,
StyledSubmenuItems,
StyledSubmenuItemText,
} from "./styled-submenu";
const Submenu = ({ data, startSelect = 0, ...rest }) => {
if (!data) return null;
const [currentItem, setCurrentItem] = useState(
data[startSelect] || startSelect || null
);
const submenuItemsRef = useRef();
const selectSubmenuItem = (e) => {
const item = data.find((el) => el.id === e.currentTarget.id);
if (item) setCurrentItem(item);
const offset = countAutoFocus(item.name, data, submenuItemsRef);
submenuItemsRef.current.scrollLeft += offset;
};
useEffect(() => {
if (!submenuItemsRef.current) return;
let isDown = false;
let startX;
let scrollLeft;
const mouseDown = (e) => {
e.preventDefault();
isDown = true;
startX = e.pageX - submenuItemsRef.current.offsetLeft;
scrollLeft = submenuItemsRef.current.scrollLeft;
};
const mouseMove = (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - submenuItemsRef.current.offsetLeft;
const walk = x - startX;
submenuItemsRef.current.scrollLeft = scrollLeft - walk;
};
const mouseUp = () => {
const offset = countAutoOffset(data, submenuItemsRef);
submenuItemsRef.current.scrollLeft += offset;
isDown = false;
};
const mouseLeave = () => (isDown = false);
submenuItemsRef.current.addEventListener("mousedown", mouseDown);
submenuItemsRef.current.addEventListener("mousemove", mouseMove);
submenuItemsRef.current.addEventListener("mouseup", mouseUp);
submenuItemsRef.current.addEventListener("mouseleave", mouseLeave);
return () => {
submenuItemsRef.current?.removeEventListener("mousedown", mouseDown);
submenuItemsRef.current?.removeEventListener("mousemove", mouseMove);
submenuItemsRef.current?.removeEventListener("mouseup", mouseUp);
submenuItemsRef.current?.removeEventListener("mouseleave", mouseLeave);
};
}, [submenuItemsRef]);
return (
<StyledSubmenu {...rest}>
<StyledSubmenuItems ref={submenuItemsRef} role="list">
{data.map((d) => {
const isActive = d === currentItem;
return (
<StyledSubmenuItem key={d.id} id={d.id} onClick={selectSubmenuItem}>
<StyledSubmenuItemText>
<Text
color={isActive ? "#316DAA" : "#657077"}
fontSize="13px"
fontWeight="600"
truncate="false"
>
{d.name}
</Text>
</StyledSubmenuItemText>
<StyledSubmenuItemLabel color={isActive ? "#316DAA" : "none"} />
</StyledSubmenuItem>
);
})}
</StyledSubmenuItems>
<StyledSubmenuBottomLine />
<StyledSubmenuContentWrapper>
{currentItem.content}
</StyledSubmenuContentWrapper>
</StyledSubmenu>
);
};
Submenu.propTypes = {
data: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired,
startSelect: PropTypes.oneOfType([PropTypes.object, PropTypes.number]),
};
export default Submenu;

View File

@ -0,0 +1,76 @@
import styled from "styled-components";
import { tablet } from "../utils/device";
export const StyledSubmenu = styled.div`
display: flex;
flex-direction: column;
.scrollbar {
width: 100%;
height: auto;
}
.text {
width: auto;
display: inline-block;
position: absolute;
}
`;
export const StyledSubmenuBottomLine = styled.div`
height: 1px;
width: 100%;
margin: -1px 0 15px 0;
background: #eceef1;
`;
export const StyledSubmenuContentWrapper = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
export const StyledSubmenuItems = styled.div`
overflow: scroll;
display: flex;
flex-direction: row;
gap: 4px;
padding: 0 20px;
@media ${tablet} {
padding: 0 16px;
}
&::-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: 4px 14px 0;
line-height: 20px;
`;
export const StyledSubmenuItemText = styled.div`
width: 100%;
display: flex;
`;
export const StyledSubmenuItemLabel = styled.div`
z-index: 1;
width: calc(100% + 28px);
margin-left: -14px;
height: 4px;
bottom: 0px;
border-radius: 4px 4px 0 0;
background-color: ${(props) => props.color};
`;

View File

@ -0,0 +1,25 @@
import Submenu from ".";
import React from "react";
import { data, startSelect } from "./data";
const Wrapper = (props) => (
<div
style={{
height: "170px",
}}
>
{props.children}
</div>
);
const Template = (args) => (
<Wrapper>
<Submenu {...args} />
</Wrapper>
);
export const Default = Template.bind({});
Default.args = {
data: data,
startSelect: startSelect,
};

View File

@ -0,0 +1,64 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/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 "@appserver/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}
/>
```
### 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

@ -0,0 +1,44 @@
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} />);
expect(wrapper).toExist(true);
});
it("gets data prop", () => {
const wrapper = mount(<Submenu {...onlyData} />);
expect(wrapper.prop("data")).toEqual(testData);
});
it("doesnt render without data prop", () => {
const wrapper = mount(<Submenu />);
expect(wrapper).toExist(false);
});
it("gets startSelect prop", () => {
const wrapper = mount(<Submenu {...props} />);
expect(wrapper.prop("startSelect")).toEqual(testStartSelect);
});
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);
expect(wrapper.debug()).toEqual(currentItemWrapper.debug());
});
});