Merge pull request #534 from ONLYOFFICE/feature/inputWithChips

Feature/input with chips
This commit is contained in:
Alexey Safronov 2022-04-01 19:22:12 +03:00 committed by GitHub
commit 026fdbbec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1238 additions and 0 deletions

View 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 |

View File

@ -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,
};

View File

@ -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".

View 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");
});
});

View 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;

View 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,
};

View 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;

View File

@ -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;

View File

@ -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),
};
};

View File

@ -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;

View 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

View 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

View File

@ -0,0 +1,2 @@
export { default as WarningIcon } from "./Warning.svg";
export { default as DeleteIcon } from "./Delete.svg";

View File

@ -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;

View 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]);
};