Merge pull request #534 from ONLYOFFICE/feature/inputWithChips
Feature/input with chips
This commit is contained in:
commit
026fdbbec5
70
packages/asc-web-components/email-chips/README.md
Normal file
70
packages/asc-web-components/email-chips/README.md
Normal file
@ -0,0 +1,70 @@
|
||||
# EmailChips
|
||||
|
||||
Custom email-chips
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import EmailChips from "@appserver/components/email-chips";
|
||||
```
|
||||
|
||||
```jsx
|
||||
<EmailChips
|
||||
options={[]}
|
||||
onChange={(selected) => console.log(selected)}
|
||||
placeholder="Invite people by name or email"
|
||||
clearButtonLabel="Clear list"
|
||||
existEmailText="This email address has already been entered"
|
||||
invalidEmailText="Invalid email address"
|
||||
exceededLimitText="The limit on the number of emails has reached the maximum"
|
||||
exceededLimitInputText="The limit on the number of characters has reached the maximum value"
|
||||
chipOverLimitText="The limit on the number of characters has reached the maximum value"
|
||||
exceededLimit=500,
|
||||
/>
|
||||
```
|
||||
|
||||
#### Options - an array of objects that contains the following fields:
|
||||
|
||||
```js
|
||||
const options = [
|
||||
{
|
||||
name: "Ivan Petrov",
|
||||
email: "myname@gmul.com",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Options have options:
|
||||
|
||||
- name - Display text
|
||||
- email - Email address
|
||||
- isValid - Displays whether the email is valid
|
||||
|
||||
#### Actions that can be performed on chips and input:
|
||||
|
||||
- Enter a chip into the input (chips are checked for a valid email, and the same chips).
|
||||
- Add chips by pressing Enter or NumpadEnter.
|
||||
- By double-clicking on the mouse button or pressing enter on a specific selected chip, you can switch to the chip editing mode.
|
||||
- You can exit the editing mode by pressing Escape, Enter, NumpadEnter or by clicking ouside.
|
||||
- Remove the chips by clicking on the button in the form of a cross.
|
||||
- Click on the chip once, thereby highlighting it.
|
||||
- Hold down the shift button by moving the arrows to the left, right or clicking the mouse on the chips, thereby highlighting several chips.
|
||||
- The highlighted chip(s) can be removed by clicking on the button Backspace or Delete.
|
||||
- The selected chip(s) can be copied to the clipboard by pressing "ctrl + c".
|
||||
- You can remove all chips by clicking on the button "Clear list".
|
||||
|
||||
### Properties
|
||||
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ------------------------ | :------------: | :------: | :----: | :-----------------------------------------------------------------------------: | -------------------------------------------------------------------------------- |
|
||||
| `options` | `obj`, `array` | - | - | - | Array of objects with chips |
|
||||
| `onChange` | `func` | ✅ | - | - | displays valid email addresses. Called when changing chips |
|
||||
| `placeholder` | `string` | - | - | Invite people by name or email | Placeholder text for the input |
|
||||
| `clearButtonLabel` | `string` | - | - | Clear list | The text of the button for cleaning all chips |
|
||||
| `existEmailText` | `string` | - | - | This email address has already been entered | Warning text when entering an existing email |
|
||||
| `invalidEmailText` | `string` | - | - | Invalid email address | Warning text when entering an invalid email |
|
||||
| `exceededLimit` | `number` | - | - | 500 | Limit of chips (number) |
|
||||
| `exceededLimitText` | `string` | - | - | The limit on the number of emails has reached the maximum | Warning text when exceeding the limit of the number of chips |
|
||||
| `exceededLimitInputText` | `string` | - | - | The limit on the number of characters has reached the maximum value | Warning text when entering the number of characters in input exceeding the limit |
|
||||
| `chipOverLimitText` | `string` | - | - | The limit on the number of characters in an email has reached its maximum value | Warning text when entering the number of email characters exceeding the limit |
|
@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import EmailChips from ".";
|
||||
|
||||
const Options = [
|
||||
{ name: "Ivan Petrov", email: "myname@gmul.com", isValid: true },
|
||||
{ name: "Donna Cross", email: "myname45@gmul.com", isValid: true },
|
||||
{ name: "myname@gmul.co45", email: "myname@gmul.co45", isValid: false },
|
||||
{ name: "Lisa Cooper", email: "myn348ame@gmul.com", isValid: true },
|
||||
{ name: "myname19@gmail.com", email: "myname19@gmail.com", isValid: true },
|
||||
{ name: "myname@gmail.com", email: "myname@gmail.com", isValid: true },
|
||||
{
|
||||
name: "mynameiskonstantine1353434@gmail.com",
|
||||
email: "mynameiskonstantine1353434@gmail.com",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "mynameiskonstantine56454864846455488875454654846454@gmail.com",
|
||||
email: "mynameiskonstantine56454864846455488875454654846454@gmail.com",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "mynameiskonstantine3246@gmail.com",
|
||||
email: "mynameiskonstantine3246@gmail.com",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = (props) => (
|
||||
<div
|
||||
style={{
|
||||
height: "220px",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Template = (args) => (
|
||||
<Wrapper>
|
||||
<EmailChips {...args} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
options: Options,
|
||||
onChange: (selected) => console.log(selected),
|
||||
placeholder: "Invite people by name or email",
|
||||
clearButtonLabel: "Clear list",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email address",
|
||||
exceededLimitText:
|
||||
"The limit on the number of emails has reached the maximum",
|
||||
exceededLimitInputText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
chipOverLimitText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
exceededLimit: 500,
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
options: [],
|
||||
placeholder: "Type your chips...",
|
||||
clearButtonLabel: "Clear list",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email address",
|
||||
exceededLimitText:
|
||||
"The limit on the number of emails has reached the maximum",
|
||||
exceededLimitInputText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
chipOverLimitText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
exceededLimit: 500,
|
||||
};
|
@ -0,0 +1,72 @@
|
||||
import { Meta, Story, ArgsTable, Canvas } from "@storybook/addon-docs/blocks";
|
||||
import EmailChips from "./";
|
||||
import * as stories from "./email-chips.stories.js";
|
||||
|
||||
<Meta
|
||||
title="Components/EmailChips"
|
||||
component={EmailChips}
|
||||
argTypes={{
|
||||
onChange: { required: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
# EmailChips
|
||||
|
||||
Custom email-chips
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import EmailChips from "@appserver/components/email-chips";
|
||||
```
|
||||
|
||||
### EmailChips - Default
|
||||
|
||||
<Canvas>
|
||||
<Story story={stories.Default} name="Default" />
|
||||
</Canvas>
|
||||
|
||||
#### Properties
|
||||
|
||||
<ArgsTable story="Default" />
|
||||
|
||||
#### Options - an array of objects that contains the following fields:
|
||||
|
||||
```js
|
||||
const options = [
|
||||
{
|
||||
name: "Ivan Petrov",
|
||||
email: "myname@gmul.com",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Options have options:
|
||||
|
||||
- name - Display text
|
||||
- email - Email address
|
||||
- isValid - Displays whether the email is valid
|
||||
|
||||
### EmailChips - Empty
|
||||
|
||||
<Canvas>
|
||||
<Story story={stories.Empty} name="Empty" />
|
||||
</Canvas>
|
||||
|
||||
#### Properties
|
||||
|
||||
<ArgsTable story="Empty" />
|
||||
|
||||
#### Actions that can be performed on chips and input:
|
||||
|
||||
- Enter a chip into the input (chips are checked for a valid email, and the same chips).
|
||||
- Add chips by pressing Enter or NumpadEnter.
|
||||
- By double-clicking on the mouse button or pressing enter on a specific selected chip, you can switch to the chip editing mode.
|
||||
- You can exit the editing mode by pressing Escape, Enter, NumpadEnter or by clicking ouside.
|
||||
- Remove the chips by clicking on the button in the form of a cross.
|
||||
- Click on the chip once, thereby highlighting it.
|
||||
- Hold down the shift button by moving the arrows to the left, right or clicking the mouse on the chips, thereby highlighting several chips.
|
||||
- The highlighted chip(s) can be removed by clicking on the button Backspace or Delete.
|
||||
- The selected chip(s) can be copied to the clipboard by pressing "ctrl + c".
|
||||
- You can remove all chips by clicking on the button "Clear list".
|
32
packages/asc-web-components/email-chips/email-chips.test.js
Normal file
32
packages/asc-web-components/email-chips/email-chips.test.js
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import EmailChips from ".";
|
||||
|
||||
const baseProps = {
|
||||
placeholder: "Placeholder",
|
||||
clearButtonLabel: "Clear list ",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email",
|
||||
};
|
||||
|
||||
describe("<InputWithChips />", () => {
|
||||
it("accepts id", () => {
|
||||
const wrapper = mount(<EmailChips {...baseProps} id="testId" />);
|
||||
|
||||
expect(wrapper.prop("id")).toEqual("testId");
|
||||
});
|
||||
|
||||
it("accepts className", () => {
|
||||
const wrapper = mount(<EmailChips {...baseProps} className="test" />);
|
||||
|
||||
expect(wrapper.prop("className")).toEqual("test");
|
||||
});
|
||||
|
||||
it("accepts style", () => {
|
||||
const wrapper = mount(
|
||||
<EmailChips {...baseProps} style={{ color: "red" }} />
|
||||
);
|
||||
|
||||
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
|
||||
});
|
||||
});
|
368
packages/asc-web-components/email-chips/index.js
Normal file
368
packages/asc-web-components/email-chips/index.js
Normal file
@ -0,0 +1,368 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Scrollbar from "../scrollbar";
|
||||
import { useClickOutside } from "../utils/useClickOutside.js";
|
||||
|
||||
import {
|
||||
StyledContent,
|
||||
StyledChipGroup,
|
||||
StyledChipWithInput,
|
||||
} from "./styled-emailchips";
|
||||
import {
|
||||
MAX_EMAIL_LENGTH_WITH_DOTS,
|
||||
sliceEmail,
|
||||
} from "./sub-components/helpers";
|
||||
import InputGroup from "./sub-components/input-group";
|
||||
import ChipsRender from "./sub-components/chips-render";
|
||||
import { EmailSettings, parseAddresses } from "../utils/email";
|
||||
|
||||
const calcMaxLengthInput = (exceededLimit) =>
|
||||
exceededLimit * MAX_EMAIL_LENGTH_WITH_DOTS;
|
||||
|
||||
const EmailChips = ({
|
||||
options,
|
||||
placeholder,
|
||||
onChange,
|
||||
clearButtonLabel,
|
||||
existEmailText,
|
||||
invalidEmailText,
|
||||
exceededLimit,
|
||||
exceededLimitText,
|
||||
exceededLimitInputText,
|
||||
chipOverLimitText,
|
||||
...props
|
||||
}) => {
|
||||
const [chips, setChips] = useState(options || []);
|
||||
const [currentChip, setCurrentChip] = useState(null);
|
||||
const [selectedChips, setSelectedChips] = useState([]);
|
||||
|
||||
const [isExistedOn, setIsExistedOn] = useState(false);
|
||||
const [isExceededLimitChips, setIsExceededLimitChips] = useState(false);
|
||||
const [isExceededLimitInput, setIsExceededLimitInput] = useState(false);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const blockRef = useRef(null);
|
||||
const scrollbarRef = useRef(null);
|
||||
const chipsCount = useRef(options?.length);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(
|
||||
chips.map((it) => {
|
||||
if (it?.name === it?.email || it?.name === "") {
|
||||
return {
|
||||
email: it?.email,
|
||||
isValid: it?.isValid,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: it?.name,
|
||||
email: it?.email,
|
||||
isValid: it?.isValid,
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [chips]);
|
||||
|
||||
useEffect(() => {
|
||||
const isChipAdd = chips.length > chipsCount.current;
|
||||
if (scrollbarRef.current && isChipAdd) {
|
||||
scrollbarRef.current.scrollToBottom();
|
||||
}
|
||||
chipsCount.current = chips.length;
|
||||
}, [chips.length]);
|
||||
|
||||
useClickOutside(
|
||||
blockRef,
|
||||
() => {
|
||||
if (selectedChips.length > 0) {
|
||||
setSelectedChips([]);
|
||||
}
|
||||
},
|
||||
selectedChips
|
||||
);
|
||||
|
||||
useClickOutside(inputRef, () => {
|
||||
onHideAllTooltips();
|
||||
});
|
||||
|
||||
const onClick = (value, isShiftKey) => {
|
||||
if (isShiftKey) {
|
||||
const isExisted = !!selectedChips?.find((it) => it.email === value.email);
|
||||
return isExisted
|
||||
? setSelectedChips(
|
||||
selectedChips.filter((it) => it.email != value.email)
|
||||
)
|
||||
: setSelectedChips([value, ...selectedChips]);
|
||||
} else {
|
||||
setSelectedChips([value]);
|
||||
}
|
||||
};
|
||||
|
||||
const onDoubleClick = (value) => {
|
||||
setCurrentChip(value);
|
||||
};
|
||||
|
||||
const onDelete = useCallback(
|
||||
(value) => {
|
||||
setChips(chips.filter((it) => it.email !== value.email));
|
||||
},
|
||||
[chips]
|
||||
);
|
||||
|
||||
const checkSelected = (value) => {
|
||||
return !!selectedChips?.find((item) => item?.email === value?.email);
|
||||
};
|
||||
|
||||
const onSaveNewChip = (value, newValue) => {
|
||||
const settings = new EmailSettings();
|
||||
settings.allowName = true;
|
||||
let parsed = parseAddresses(newValue, settings);
|
||||
parsed[0].isValid = parsed[0].isValid();
|
||||
if (newValue && newValue !== `"${value?.name}" <${value?.email}>`) {
|
||||
const newChips = chips.map((it) => {
|
||||
return it.email === value.email ? sliceEmail(parsed[0]) : it;
|
||||
});
|
||||
setChips(newChips);
|
||||
setSelectedChips([sliceEmail(parsed[0])]);
|
||||
}
|
||||
|
||||
containerRef.current.setAttribute("tabindex", "-1");
|
||||
containerRef.current.focus();
|
||||
|
||||
setCurrentChip(null);
|
||||
};
|
||||
|
||||
const copyToClipbord = () => {
|
||||
if (currentChip === null) {
|
||||
navigator.clipboard.writeText(
|
||||
selectedChips
|
||||
.map((it) => {
|
||||
if (it.name !== it.email) {
|
||||
let copyItem = `"${it.name}" <${it.email}>`;
|
||||
return copyItem;
|
||||
} else {
|
||||
return it.email;
|
||||
}
|
||||
})
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
const whiteList = [
|
||||
"Enter",
|
||||
"Escape",
|
||||
"Backspace",
|
||||
"Delete",
|
||||
"ArrowRigth",
|
||||
"ArrowLeft",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"KeyC",
|
||||
];
|
||||
|
||||
const code = e.code;
|
||||
|
||||
const isShiftDown = e.shiftKey;
|
||||
const isCtrlDown = e.ctrlKey;
|
||||
|
||||
if (!whiteList.includes(code) && !isCtrlDown && !isShiftDown) {
|
||||
return;
|
||||
}
|
||||
if (code === "Enter" && selectedChips.length == 1 && !currentChip) {
|
||||
e.stopPropagation();
|
||||
setCurrentChip(selectedChips[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === "Escape") {
|
||||
setSelectedChips(currentChip ? [currentChip] : []);
|
||||
containerRef.current.setAttribute("tabindex", "0");
|
||||
containerRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedChips.length > 0 &&
|
||||
(code === "Backspace" || code === "Delete") &&
|
||||
!currentChip
|
||||
) {
|
||||
const filteredChips = chips.filter((e) => !~selectedChips.indexOf(e));
|
||||
setChips(filteredChips);
|
||||
setSelectedChips([]);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChips.length > 0 && !currentChip) {
|
||||
let chip = null;
|
||||
|
||||
if (isShiftDown && code === "ArrowRigth") {
|
||||
chip = selectedChips[selectedChips.length - 1];
|
||||
} else {
|
||||
chip = selectedChips[0];
|
||||
}
|
||||
|
||||
const index = chips.findIndex((it) => it.email === chip?.email);
|
||||
|
||||
switch (code) {
|
||||
case "ArrowLeft": {
|
||||
if (isShiftDown) {
|
||||
selectedChips.includes(chips[index - 1])
|
||||
? setSelectedChips(
|
||||
selectedChips.filter((it) => it !== chips[index])
|
||||
)
|
||||
: chips[index - 1] &&
|
||||
setSelectedChips([chips[index - 1], ...selectedChips]);
|
||||
} else if (index != 0) {
|
||||
setSelectedChips([chips[index - 1]]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowRight": {
|
||||
if (isShiftDown) {
|
||||
selectedChips.includes(chips[index + 1])
|
||||
? setSelectedChips(
|
||||
selectedChips.filter((it) => it !== chips[index])
|
||||
)
|
||||
: chips[index + 1] &&
|
||||
setSelectedChips([chips[index + 1], ...selectedChips]);
|
||||
} else {
|
||||
if (index != chips.length - 1) {
|
||||
setSelectedChips([chips[index + 1]]);
|
||||
} else {
|
||||
setSelectedChips([]);
|
||||
if (inputRef) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "KeyC": {
|
||||
if (isCtrlDown) {
|
||||
copyToClipbord();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const goFromInputToChips = () => {
|
||||
setSelectedChips([chips[chips?.length - 1]]);
|
||||
};
|
||||
|
||||
const onClearClick = () => {
|
||||
setChips([]);
|
||||
};
|
||||
|
||||
const onHideAllTooltips = () => {
|
||||
setIsExceededLimitChips(false);
|
||||
setIsExistedOn(false);
|
||||
setIsExceededLimitInput(false);
|
||||
};
|
||||
|
||||
const showTooltipOfLimit = () => {
|
||||
setIsExceededLimitInput(true);
|
||||
};
|
||||
|
||||
const onAddChip = (chipsToAdd) => {
|
||||
setIsExceededLimitChips(chips.length >= exceededLimit);
|
||||
if (chips.length >= exceededLimit) return;
|
||||
const filterLimit = exceededLimit - chips.length;
|
||||
|
||||
const filteredChips = chipsToAdd.map(sliceEmail).filter((it, index) => {
|
||||
const isExisted = !!chips.find(
|
||||
(chip) => chip.email === it || chip.email === it?.email
|
||||
);
|
||||
if (chipsToAdd.length === 1) {
|
||||
setIsExistedOn(isExisted);
|
||||
if (isExisted) return false;
|
||||
}
|
||||
return !isExisted && index < filterLimit;
|
||||
});
|
||||
setChips([...chips, ...filteredChips]);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContent {...props}>
|
||||
<StyledChipGroup onKeyDown={onKeyDown} ref={containerRef} tabindex="-1">
|
||||
<StyledChipWithInput length={chips.length}>
|
||||
<Scrollbar scrollclass={"scroll"} stype="thumbV" ref={scrollbarRef}>
|
||||
<ChipsRender
|
||||
chips={chips}
|
||||
checkSelected={checkSelected}
|
||||
currentChip={currentChip}
|
||||
blockRef={blockRef}
|
||||
onClick={onClick}
|
||||
invalidEmailText={invalidEmailText}
|
||||
chipOverLimitText={chipOverLimitText}
|
||||
onDelete={onDelete}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onSaveNewChip={onSaveNewChip}
|
||||
/>
|
||||
</Scrollbar>
|
||||
|
||||
<InputGroup
|
||||
placeholder={placeholder}
|
||||
exceededLimitText={exceededLimitText}
|
||||
existEmailText={existEmailText}
|
||||
exceededLimitInputText={exceededLimitInputText}
|
||||
clearButtonLabel={clearButtonLabel}
|
||||
inputRef={inputRef}
|
||||
containerRef={containerRef}
|
||||
maxLength={calcMaxLengthInput(exceededLimit)}
|
||||
goFromInputToChips={goFromInputToChips}
|
||||
onClearClick={onClearClick}
|
||||
isExistedOn={isExistedOn}
|
||||
isExceededLimitChips={isExceededLimitChips}
|
||||
isExceededLimitInput={isExceededLimitInput}
|
||||
onHideAllTooltips={onHideAllTooltips}
|
||||
showTooltipOfLimit={showTooltipOfLimit}
|
||||
onAddChip={onAddChip}
|
||||
/>
|
||||
</StyledChipWithInput>
|
||||
</StyledChipGroup>
|
||||
</StyledContent>
|
||||
);
|
||||
};
|
||||
|
||||
EmailChips.propTypes = {
|
||||
/** Array of objects with chips */
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
/** Placeholder text for the input */
|
||||
placeholder: PropTypes.string,
|
||||
/** The text that is displayed in the button for cleaning all chips */
|
||||
clearButtonLabel: PropTypes.string,
|
||||
/** Warning text when entering an existing email */
|
||||
existEmailText: PropTypes.string,
|
||||
/** Warning text when entering an invalid email */
|
||||
invalidEmailText: PropTypes.string,
|
||||
/** Limit of chips */
|
||||
exceededLimit: PropTypes.number,
|
||||
/** Warning text when entering the number of chips exceeding the limit */
|
||||
exceededLimitText: PropTypes.string,
|
||||
/** Warning text when entering the number of characters in input exceeding the limit */
|
||||
exceededLimitInputText: PropTypes.string,
|
||||
/** Warning text when entering the number of email characters exceeding the limit */
|
||||
chipOverLimitText: PropTypes.string,
|
||||
/** Will be called when the selected items are changed */
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
EmailChips.defaultProps = {
|
||||
placeholder: "Invite people by name or email",
|
||||
clearButtonLabel: "Clear list",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email address",
|
||||
exceededLimitText:
|
||||
"The limit on the number of emails has reached the maximum",
|
||||
exceededLimitInputText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
exceededLimit: 50,
|
||||
};
|
||||
|
||||
export default EmailChips;
|
164
packages/asc-web-components/email-chips/styled-emailchips.js
Normal file
164
packages/asc-web-components/email-chips/styled-emailchips.js
Normal file
@ -0,0 +1,164 @@
|
||||
import styled from "styled-components";
|
||||
import commonInputStyle from "../text-input/common-input-styles";
|
||||
import Base from "../themes/base";
|
||||
import TextInput from "../text-input";
|
||||
|
||||
const StyledChipWithInput = styled.div`
|
||||
min-height: 32px;
|
||||
max-height: 220px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: fit-content;
|
||||
width: ${(props) => props.length === 0 && "100%"};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
position: relative;
|
||||
width: 469px;
|
||||
height: 220px;
|
||||
`;
|
||||
|
||||
const StyledChipGroup = styled.div`
|
||||
:focus-visible {
|
||||
outline: 0px solid #2da7db !important;
|
||||
}
|
||||
height: fit-content;
|
||||
${commonInputStyle} :focus-within {
|
||||
border-color: ${(props) => props.theme.inputBlock.borderColor};
|
||||
}
|
||||
|
||||
.scroll {
|
||||
height: fit-content;
|
||||
position: inherit !important;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
:focus-visible {
|
||||
outline: 0px solid #2da7db !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
`;
|
||||
StyledChipGroup.defaultProps = { theme: Base };
|
||||
|
||||
const StyledAllChips = styled.div`
|
||||
width: 448px;
|
||||
max-height: 180px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
`;
|
||||
|
||||
const StyledChip = styled.div`
|
||||
width: fit-content;
|
||||
max-width: calc(100% - 18px);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
background: #eceef1;
|
||||
|
||||
height: 32px;
|
||||
margin: 2px 4px;
|
||||
padding: ${(props) => (props.isSelected ? "5px 7px" : "6px 8px")};
|
||||
|
||||
border-radius: 3px 0 0 3px;
|
||||
border: ${(props) => props.isSelected && "1px dashed #000"};
|
||||
background: ${(props) => (props.isValid ? "#ECEEF1" : "#F7CDBE")};
|
||||
|
||||
user-select: none;
|
||||
|
||||
.warning_icon_wrap {
|
||||
cursor: pointer;
|
||||
.warning_icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledChipValue = styled.div`
|
||||
margin-right: 4px;
|
||||
min-width: 0px;
|
||||
max-width: 395px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
|
||||
color: #333333;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledChipInput = styled(TextInput)`
|
||||
flex: ${(props) => `${props.flexvalue}!important`};
|
||||
`;
|
||||
|
||||
const StyledInputWithLink = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: auto 15%;
|
||||
align-content: space-between;
|
||||
width: calc(100% - 8px);
|
||||
|
||||
.textInput {
|
||||
width: calc(100% - 8px);
|
||||
padding: 0px;
|
||||
margin: 8px 0px 10px 8px;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-align: end;
|
||||
margin: 10px 0px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled.div`
|
||||
position: absolute;
|
||||
top: -49px;
|
||||
left: 0;
|
||||
|
||||
max-width: 435px;
|
||||
padding: 16px;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
background: #f8f7bf;
|
||||
border-radius: 6px;
|
||||
opacity: 0.9;
|
||||
`;
|
||||
|
||||
export {
|
||||
StyledChipWithInput,
|
||||
StyledContent,
|
||||
StyledChipGroup,
|
||||
StyledAllChips,
|
||||
StyledChip,
|
||||
StyledChipValue,
|
||||
StyledContainer,
|
||||
StyledChipInput,
|
||||
StyledInputWithLink,
|
||||
StyledTooltip,
|
||||
};
|
190
packages/asc-web-components/email-chips/sub-components/chip.js
Normal file
190
packages/asc-web-components/email-chips/sub-components/chip.js
Normal file
@ -0,0 +1,190 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import IconButton from "../../icon-button";
|
||||
import Tooltip from "../../tooltip";
|
||||
import { useClickOutside } from "../../utils/useClickOutside.js";
|
||||
|
||||
import { DeleteIcon, WarningIcon } from "../svg";
|
||||
import {
|
||||
MAX_EMAIL_LENGTH,
|
||||
MAX_EMAIL_LENGTH_WITH_DOTS,
|
||||
sliceEmail,
|
||||
} from "./helpers";
|
||||
|
||||
import {
|
||||
StyledChip,
|
||||
StyledChipInput,
|
||||
StyledChipValue,
|
||||
StyledContainer,
|
||||
} from "../styled-emailchips.js";
|
||||
|
||||
const Chip = (props) => {
|
||||
const {
|
||||
value,
|
||||
currentChip,
|
||||
isSelected,
|
||||
isValid,
|
||||
invalidEmailText,
|
||||
chipOverLimitText,
|
||||
onDelete,
|
||||
onDoubleClick,
|
||||
onSaveNewChip,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
function initNewValue() {
|
||||
return value?.email === value?.name || value?.name === ""
|
||||
? value?.email
|
||||
: `"${value?.name}" <${value?.email}>`;
|
||||
}
|
||||
|
||||
const [newValue, setNewValue] = useState(initNewValue());
|
||||
const [chipWidth, setChipWidth] = useState(0);
|
||||
const [isChipOverLimit, setIsChipOverLimit] = useState(false);
|
||||
|
||||
const tooltipRef = useRef(null);
|
||||
const warningRef = useRef(null);
|
||||
const chipRef = useRef(null);
|
||||
const chipInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setChipWidth(chipRef.current?.clientWidth);
|
||||
}, [chipRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected) {
|
||||
chipRef.current?.scrollIntoView({ block: "end" });
|
||||
}
|
||||
}, [isSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newValue.length > MAX_EMAIL_LENGTH) {
|
||||
setIsChipOverLimit(true);
|
||||
} else {
|
||||
setIsChipOverLimit(false);
|
||||
}
|
||||
}, [newValue]);
|
||||
|
||||
useClickOutside(warningRef, () => tooltipRef.current.hideTooltip());
|
||||
useClickOutside(
|
||||
chipInputRef,
|
||||
() => {
|
||||
onSaveNewChip(value, newValue);
|
||||
},
|
||||
newValue
|
||||
);
|
||||
|
||||
const onChange = (e) => {
|
||||
if (
|
||||
e.target.value.length <= MAX_EMAIL_LENGTH_WITH_DOTS ||
|
||||
e.target.value.length < newValue.length
|
||||
) {
|
||||
setNewValue(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickHandler = (e) => {
|
||||
if (e.shiftKey) {
|
||||
document.getSelection().removeAllRanges();
|
||||
}
|
||||
onClick(value, e.shiftKey);
|
||||
};
|
||||
|
||||
const onDoubleClickHandler = () => {
|
||||
onDoubleClick(value);
|
||||
};
|
||||
|
||||
const onIconClick = () => {
|
||||
onDelete(value);
|
||||
};
|
||||
|
||||
const onInputKeyDown = useCallback(
|
||||
(e) => {
|
||||
const code = e.code;
|
||||
|
||||
switch (code) {
|
||||
case "Enter":
|
||||
case "NumpadEnter": {
|
||||
onSaveNewChip(value, newValue);
|
||||
setNewValue(sliceEmail(newValue).email);
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
setNewValue(initNewValue());
|
||||
onDoubleClick(null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
[newValue]
|
||||
);
|
||||
if (value?.email === currentChip?.email) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{isChipOverLimit && (
|
||||
<Tooltip getContent={() => {}} id="input" effect="float" />
|
||||
)}
|
||||
<StyledChipInput
|
||||
data-for="input"
|
||||
data-tip={chipOverLimitText}
|
||||
value={newValue}
|
||||
forwardedRef={chipInputRef}
|
||||
onChange={onChange}
|
||||
onKeyDown={onInputKeyDown}
|
||||
isAutoFocussed
|
||||
withBorder={false}
|
||||
maxLength={MAX_EMAIL_LENGTH_WITH_DOTS}
|
||||
flexvalue={
|
||||
value?.name !== value?.email ? "0 1 auto" : `0 0 ${chipWidth}px`
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledChip
|
||||
isSelected={isSelected}
|
||||
onDoubleClick={onDoubleClickHandler}
|
||||
onClick={onClickHandler}
|
||||
isValid={isValid}
|
||||
ref={chipRef}
|
||||
>
|
||||
{!isValid && (
|
||||
<div className="warning_icon_wrap" ref={warningRef}>
|
||||
<IconButton
|
||||
iconName={WarningIcon}
|
||||
size={12}
|
||||
className="warning_icon_wrap warning_icon "
|
||||
data-for="group"
|
||||
data-tip={invalidEmailText}
|
||||
/>
|
||||
<Tooltip
|
||||
getContent={() => {}}
|
||||
id="group"
|
||||
reference={tooltipRef}
|
||||
place={"top"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<StyledChipValue>{value?.name || value?.email}</StyledChipValue>
|
||||
<IconButton iconName={DeleteIcon} size={12} onClick={onIconClick} />
|
||||
</StyledChip>
|
||||
);
|
||||
};
|
||||
|
||||
Chip.propTypes = {
|
||||
value: PropTypes.object,
|
||||
currentChip: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
isValid: PropTypes.bool,
|
||||
invalidEmailText: PropTypes.string,
|
||||
chipOverLimitText: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onSaveNewChip: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Chip;
|
@ -0,0 +1,76 @@
|
||||
import React, { memo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { EmailSettings, parseAddress } from "../../utils/email";
|
||||
import Chip from "./chip";
|
||||
|
||||
import { StyledAllChips } from "../styled-emailchips";
|
||||
|
||||
const ChipsRender = memo(
|
||||
({
|
||||
chips,
|
||||
currentChip,
|
||||
blockRef,
|
||||
checkSelected,
|
||||
invalidEmailText,
|
||||
chipOverLimitText,
|
||||
onDelete,
|
||||
onDoubleClick,
|
||||
onSaveNewChip,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const emailSettings = new EmailSettings();
|
||||
|
||||
const checkEmail = (email) => {
|
||||
const emailObj = parseAddress(email, emailSettings);
|
||||
return emailObj.isValid();
|
||||
};
|
||||
|
||||
const checkIsSelected = (value) => {
|
||||
return checkSelected(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAllChips ref={blockRef}>
|
||||
{chips?.map((it) => {
|
||||
return (
|
||||
<Chip
|
||||
key={it?.email}
|
||||
value={it}
|
||||
currentChip={currentChip}
|
||||
isSelected={checkIsSelected(it)}
|
||||
isValid={checkEmail(it?.email)}
|
||||
invalidEmailText={invalidEmailText}
|
||||
chipOverLimitText={chipOverLimitText}
|
||||
onDelete={onDelete}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onSaveNewChip={onSaveNewChip}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledAllChips>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChipsRender.propTypes = {
|
||||
chips: PropTypes.arrayOf(PropTypes.object),
|
||||
currentChip: PropTypes.object,
|
||||
|
||||
invalidEmailText: PropTypes.string,
|
||||
chipOverLimitText: PropTypes.string,
|
||||
|
||||
blockRef: PropTypes.shape({ current: PropTypes.any }),
|
||||
|
||||
checkSelected: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onSaveNewChip: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
ChipsRender.displayName = "ChipsRender";
|
||||
|
||||
export default ChipsRender;
|
@ -0,0 +1,24 @@
|
||||
// Maximum allowed email length
|
||||
// https://www.lifewire.com/is-email-address-length-limited-1171110
|
||||
export const MAX_EMAIL_LENGTH = 320;
|
||||
export const MAX_EMAIL_LENGTH_WITH_DOTS = 323;
|
||||
const MAX_DISPLAY_NAME_LENGTH = 64;
|
||||
const MAX_VALUE_LENGTH = 256;
|
||||
|
||||
export const truncate = (str, length) =>
|
||||
str?.length > length ? str?.slice(0, length) + "..." : str;
|
||||
|
||||
export const sliceEmail = (it) => {
|
||||
if (typeof it === "string") {
|
||||
const res = truncate(it, MAX_EMAIL_LENGTH);
|
||||
return {
|
||||
name: res,
|
||||
email: res,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...it,
|
||||
name: truncate(it?.name, MAX_DISPLAY_NAME_LENGTH),
|
||||
email: truncate(it?.email, MAX_VALUE_LENGTH),
|
||||
};
|
||||
};
|
@ -0,0 +1,137 @@
|
||||
import React, { memo, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Link from "../../link";
|
||||
import TextInput from "../../text-input";
|
||||
|
||||
import { StyledInputWithLink, StyledTooltip } from "../styled-emailchips";
|
||||
import { EmailSettings, parseAddresses } from "../../utils/email";
|
||||
|
||||
const InputGroup = memo(
|
||||
({
|
||||
placeholder,
|
||||
exceededLimitText,
|
||||
existEmailText,
|
||||
exceededLimitInputText,
|
||||
clearButtonLabel,
|
||||
|
||||
inputRef,
|
||||
containerRef,
|
||||
|
||||
maxLength,
|
||||
|
||||
isExistedOn,
|
||||
isExceededLimitChips,
|
||||
isExceededLimitInput,
|
||||
|
||||
goFromInputToChips,
|
||||
onClearClick,
|
||||
onHideAllTooltips,
|
||||
showTooltipOfLimit,
|
||||
onAddChip,
|
||||
}) => {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const onInputChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
onHideAllTooltips();
|
||||
if (e.target.value.length >= maxLength) showTooltipOfLimit();
|
||||
};
|
||||
|
||||
const onInputKeyDown = (e) => {
|
||||
const code = e.code;
|
||||
|
||||
switch (code) {
|
||||
case "Enter":
|
||||
case "NumpadEnter": {
|
||||
onEnterPress();
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
const isCursorStart = inputRef.current.selectionStart === 0;
|
||||
if (!isCursorStart) return;
|
||||
goFromInputToChips();
|
||||
if (inputRef) {
|
||||
onHideAllTooltips();
|
||||
inputRef.current.blur();
|
||||
containerRef.current.setAttribute("tabindex", "0");
|
||||
containerRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEnterPress = () => {
|
||||
if (isExceededLimitChips) return;
|
||||
if (isExistedOn) return;
|
||||
if (value.trim().length == 0) return;
|
||||
const settings = new EmailSettings();
|
||||
settings.allowName = true;
|
||||
const chipsFromString = parseAddresses(value, settings).map((it) => ({
|
||||
name: it.name === "" ? it.email : it.name,
|
||||
email: it.email,
|
||||
isValid: it.isValid(),
|
||||
parseErrors: it.parseErrors,
|
||||
}));
|
||||
onAddChip(chipsFromString);
|
||||
setValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledInputWithLink>
|
||||
{isExistedOn && <StyledTooltip>{existEmailText}</StyledTooltip>}
|
||||
{isExceededLimitChips && (
|
||||
<StyledTooltip>{exceededLimitText}</StyledTooltip>
|
||||
)}
|
||||
{isExceededLimitInput && (
|
||||
<StyledTooltip>{exceededLimitInputText}</StyledTooltip>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
forwardedRef={inputRef}
|
||||
onKeyDown={onInputKeyDown}
|
||||
placeholder={placeholder}
|
||||
withBorder={false}
|
||||
className="textInput"
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
<Link
|
||||
type="action"
|
||||
isHovered={true}
|
||||
className="link"
|
||||
onClick={onClearClick}
|
||||
>
|
||||
{clearButtonLabel}
|
||||
</Link>
|
||||
</StyledInputWithLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputGroup.propTypes = {
|
||||
inputRef: PropTypes.shape({ current: PropTypes.any }),
|
||||
containerRef: PropTypes.shape({ current: PropTypes.any }),
|
||||
|
||||
placeholder: PropTypes.string,
|
||||
exceededLimitText: PropTypes.string,
|
||||
existEmailText: PropTypes.string,
|
||||
exceededLimitInputText: PropTypes.string,
|
||||
clearButtonLabel: PropTypes.string,
|
||||
|
||||
maxLength: PropTypes.number,
|
||||
|
||||
goFromInputToChips: PropTypes.func,
|
||||
onClearClick: PropTypes.func,
|
||||
isExistedOn: PropTypes.bool,
|
||||
isExceededLimitChips: PropTypes.bool,
|
||||
isExceededLimitInput: PropTypes.bool,
|
||||
onHideAllTooltips: PropTypes.func,
|
||||
showTooltipOfLimit: PropTypes.func,
|
||||
onAddChip: PropTypes.func,
|
||||
};
|
||||
|
||||
InputGroup.displayName = "InputGroup";
|
||||
|
||||
export default InputGroup;
|
10
packages/asc-web-components/email-chips/svg/Delete.svg
Normal file
10
packages/asc-web-components/email-chips/svg/Delete.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_19101_130319)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.41442 6.00033L10.707 9.29295L9.29284 10.7072L6.00033 7.41465L2.70919 10.7063L1.29486 9.29222L4.58611 6.00044L1.29284 2.70716L2.70705 1.29295L6.00021 4.58611L9.29278 1.29301L10.7071 2.70711L7.41442 6.00033Z" fill="#657077"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_19101_130319">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 531 B |
3
packages/asc-web-components/email-chips/svg/Warning.svg
Normal file
3
packages/asc-web-components/email-chips/svg/Warning.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 -5.24537e-07C2.68629 -8.1423e-07 8.1423e-07 2.68629 5.24537e-07 6C2.34843e-07 9.31371 2.68629 12 6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 -2.34843e-07 6 -5.24537e-07ZM7 6C7 6.55228 6.55229 7 6 7C5.44772 7 5 6.55228 5 6L5 3C5 2.44771 5.44772 2 6 2C6.55229 2 7 2.44771 7 3L7 6ZM6 10C6.55228 10 7 9.55228 7 9C7 8.44771 6.55229 8 6 8C5.44772 8 5 8.44771 5 9C5 9.55228 5.44772 10 6 10Z" fill="#F21C0E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 564 B |
2
packages/asc-web-components/email-chips/svg/index.js
Normal file
2
packages/asc-web-components/email-chips/svg/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as WarningIcon } from "./Warning.svg";
|
||||
export { default as DeleteIcon } from "./Delete.svg";
|
@ -16,6 +16,7 @@ const StyledTooltip = styled.div`
|
||||
pointer-events: ${(props) => props.theme.tooltip.pointerEvents};
|
||||
max-width: ${(props) =>
|
||||
props.maxWidth ? props.maxWidth : props.theme.tooltip.maxWidth};
|
||||
color: ${(props) => props.theme.tooltip.textColor} !important;
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.tooltip.textColor} !important;
|
||||
|
14
packages/asc-web-components/utils/useClickOutside.js
Normal file
14
packages/asc-web-components/utils/useClickOutside.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
export const useClickOutside = (ref, handler, ...deps) => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
e.stopPropagation();
|
||||
if (ref.current && !ref.current.contains(e.target)) handler();
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [ref, ...deps]);
|
||||
};
|
Loading…
Reference in New Issue
Block a user