Skip to content

Commit c78e58d

Browse files
musalenmetulev
andauthored
People-picker any email support (#1069)
* WIP: add any email * WIP: fire a warning if an any mail submit btn is pressed but allow-any-mail=false * Conditional allowAnyEmail is handled within the block * Change the email validation from validatorjs Initial usage of validatorjs required adding types and in some builds it threw an error that isMail function is not exported. Using a custom email validator function should fix this and remove the need to use validatorjs. * Remove the anyEmail string logged in console.warn * Additional review changes * Add the story for people-picker allow-any-email attr * Update stories/components/peoplePicker.stories.js * Remove setting the id value in handleAnyEmail * Use .displayName property when id is missing Co-authored-by: Nikola Metulev <[email protected]>
1 parent 9a98dfd commit c78e58d

File tree

7 files changed

+112
-23
lines changed

7 files changed

+112
-23
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"build:compile": "npm run prettier:check && npm run clean && lerna run build:compile --scope @microsoft/*",
1717
"build:mgt": "cd ./packages/mgt && npm run build",
1818
"build:mgt-element": "cd ./packages/mgt-element && npm run build",
19+
"build:mgt-components": "cd ./packages/mgt-components && npm run build",
1920
"build:mgt-react": "lerna run build --scope @microsoft/mgt-react",
2021
"bundle": "cd ./packages/mgt && npm run bundle",
2122
"bootstrap": "lerna bootstrap --useWorkspaces",

packages/mgt-components/src/components/mgt-people-picker/mgt-people-picker.ts

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IDynamicPerson, ViewType } from '../../graph/types';
1616
import { Providers, ProviderState, MgtTemplatedComponent } from '@microsoft/mgt-element';
1717
import '../../styles/style-helper';
1818
import '../sub-components/mgt-spinner/mgt-spinner';
19-
import { debounce } from '../../utils/Utils';
19+
import { debounce, isValidEmail } from '../../utils/Utils';
2020
import { MgtPerson, PersonViewType } from '../mgt-person/mgt-person';
2121
import { PersonCardInteraction } from '../PersonCardInteraction';
2222
import { MgtFlyout } from '../sub-components/mgt-flyout/mgt-flyout';
@@ -299,6 +299,18 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
299299
})
300300
public disabled: boolean;
301301

302+
/**
303+
* Determines if a user can enter an email without selecting a person
304+
*
305+
* @type {boolean}
306+
* @memberof MgtPeoplePicker
307+
*/
308+
@property({
309+
attribute: 'allow-any-email',
310+
type: Boolean
311+
})
312+
public allowAnyEmail: boolean;
313+
302314
/**
303315
* Determines whether component allows multiple or single selection of people
304316
*
@@ -361,6 +373,7 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
361373
this.showMax = 6;
362374

363375
this.disabled = false;
376+
this.allowAnyEmail = false;
364377
}
365378

366379
/**
@@ -520,15 +533,17 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
520533
if (!this.selectedPeople || !this.selectedPeople.length) {
521534
return null;
522535
}
523-
524536
return html`
525537
${selectedPeople.slice(0, selectedPeople.length).map(
526538
person =>
527539
html`
528540
<div class="selected-list__person-wrapper">
529541
${
530-
this.renderTemplate('selected-person', { person }, `selected-${person.id}`) ||
531-
this.renderSelectedPerson(person)
542+
this.renderTemplate(
543+
'selected-person',
544+
{ person },
545+
`selected-${person.id ? person.id : person.displayName}`
546+
) || this.renderSelectedPerson(person)
532547
}
533548
534549
<div class="selected-list__person-wrapper__overflow">
@@ -849,6 +864,9 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
849864
protected removePerson(person: IDynamicPerson, e: MouseEvent): void {
850865
e.stopPropagation();
851866
const filteredPersonArr = this.selectedPeople.filter(p => {
867+
if (!person.id && p.displayName) {
868+
return p.displayName !== person.displayName;
869+
}
852870
return p.id !== person.id;
853871
});
854872
this.selectedPeople = filteredPersonArr;
@@ -865,7 +883,7 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
865883
if (person) {
866884
this.clearInput();
867885
const duplicatePeople = this.selectedPeople.filter(p => {
868-
if (!person.id) {
886+
if (!person.id && p.displayName) {
869887
return p.displayName === person.displayName;
870888
}
871889
return p.id === person.id;
@@ -964,10 +982,14 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
964982
// keyCodes capture: down arrow (40), right arrow (39), up arrow (38) and left arrow (37)
965983
return;
966984
}
967-
if (event.keyCode === 9 && !this.flyout.isOpen) {
985+
986+
if (event.code === 'Tab' && !this.flyout.isOpen) {
968987
// keyCodes capture: tab (9)
969-
this.gainedFocus();
988+
if (this.allowAnyEmail) {
989+
this.gainedFocus();
990+
}
970991
}
992+
971993
if (event.shiftKey) {
972994
this.gainedFocus();
973995
}
@@ -984,9 +1006,35 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
9841006
this.hideFlyout();
9851007
// fire selected people changed event
9861008
this.fireCustomEvent('selectionChanged', this.selectedPeople);
1009+
} else if (event.code === 'Comma' || event.code === 'Semicolon') {
1010+
if (this.allowAnyEmail) {
1011+
event.preventDefault();
1012+
event.stopPropagation();
1013+
}
1014+
return;
9871015
} else {
9881016
this.userInput = input.value;
989-
this.handleUserSearch();
1017+
const validEmail = isValidEmail(this.userInput);
1018+
if (!validEmail) {
1019+
this.handleUserSearch();
1020+
}
1021+
}
1022+
}
1023+
1024+
private handleAnyEmail() {
1025+
this._showLoading = false;
1026+
this._arrowSelectionCount = 0;
1027+
if (isValidEmail(this.userInput)) {
1028+
const anyMailUser = {
1029+
mail: this.userInput,
1030+
displayName: this.userInput
1031+
};
1032+
this.addPerson(anyMailUser);
1033+
}
1034+
this.hideFlyout();
1035+
if (this.input) {
1036+
this.input.focus();
1037+
this._isFocused = true;
9901038
}
9911039
}
9921040

@@ -1044,17 +1092,33 @@ export class MgtPeoplePicker extends MgtTemplatedComponent {
10441092
event.preventDefault();
10451093
}
10461094
}
1047-
if (event.keyCode === 9 || event.keyCode === 13) {
1095+
1096+
const input = event.target as HTMLInputElement;
1097+
if (event.code === 'Tab' || event.code === 'Enter') {
10481098
if (!event.shiftKey && this._foundPeople) {
10491099
// keyCodes capture: tab (9) and enter (13)
1100+
event.preventDefault();
1101+
event.stopPropagation();
10501102
if (this._foundPeople.length) {
10511103
this.fireCustomEvent('blur');
1052-
event.preventDefault();
10531104
}
1054-
this.addPerson(this._foundPeople[this._arrowSelectionCount]);
1105+
1106+
const foundPerson = this._foundPeople[this._arrowSelectionCount];
1107+
if (foundPerson) {
1108+
this.addPerson(foundPerson);
1109+
} else if (this.allowAnyEmail) {
1110+
this.handleAnyEmail();
1111+
}
10551112
}
10561113
this.hideFlyout();
10571114
(event.target as HTMLInputElement).value = '';
1115+
} else if (event.code === 'Comma' || event.code === 'Semicolon') {
1116+
if (this.allowAnyEmail) {
1117+
event.preventDefault();
1118+
event.stopPropagation();
1119+
this.userInput = input.value;
1120+
this.handleAnyEmail();
1121+
}
10581122
}
10591123
}
10601124

packages/mgt-components/src/components/mgt-people/mgt-people.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export class MgtPeople extends MgtTemplatedComponent {
309309
<ul class="people-list">
310310
${repeat(
311311
maxPeople,
312-
p => p.id,
312+
p => (p.id ? p.id : p.displayName),
313313
p => html`
314314
<li class="people-person">
315315
${this.renderPerson(p)}

packages/mgt-components/src/utils/Utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,14 @@ export function extractEmailAddress(emailString: string): string {
234234
return emailString.match(/([a-zA-Z0-9+._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi).toString();
235235
} else return emailString;
236236
}
237+
238+
/**
239+
* checks if the email string is a valid email
240+
*
241+
* @param {string} emailString
242+
* @returns {boolean}
243+
*/
244+
export function isValidEmail(emailString: string): boolean {
245+
const emailRx: RegExp = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
246+
return emailRx.test(emailString);
247+
}

packages/mgt-react/src/generated/react.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export type PeoplePickerProps = {
103103
selectionMode?: string;
104104
showMax?: number;
105105
disabled?: boolean;
106+
allowAnyEmail?: boolean;
106107
templateContext?: TemplateContext;
107108
mediaQuery?: ComponentMediaQuery;
108109
selectionChanged?: (e: Event) => void;

samples/react-app/src/App.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,26 @@ const MyEvent = (props: MgtTemplateProps) => {
5959
const MyTemplate = (props: MgtTemplateProps) => {
6060
const me = props.dataContext as MicrosoftGraph.User;
6161

62-
return <div>hello {me.displayName}</div>
63-
}
62+
return <div>hello {me.displayName}</div>;
63+
};
6464

6565
const MyMessage = (props: MgtTemplateProps) => {
6666
const message = props.dataContext as MicrosoftGraph.Message;
6767

68-
return <div>
69-
<b>Subject:</b>{message.subject}
68+
return (
7069
<div>
71-
<b>From:</b>
72-
<Person
73-
personQuery={message.from?.emailAddress?.address || ""}
74-
fallbackDetails={{mail: message.from?.emailAddress?.address, displayName: message.from?.emailAddress?.name}}
75-
view={PersonViewType.oneline}>
76-
</Person>
70+
<b>Subject:</b>
71+
{message.subject}
72+
<div>
73+
<b>From:</b>
74+
<Person
75+
personQuery={message.from?.emailAddress?.address || ''}
76+
fallbackDetails={{ mail: message.from?.emailAddress?.address, displayName: message.from?.emailAddress?.name }}
77+
view={PersonViewType.oneline}
78+
></Person>
79+
</div>
7780
</div>
78-
</div>;
81+
);
7982
};
8083

8184
export default App;

stories/components/peoplePicker.stories.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,12 @@ export const darkTheme = () => html`
168168
}
169169
</style>
170170
`;
171+
172+
export const pickerAllowAnyEmail = () => html`
173+
<mgt-people-picker allow-any-email></mgt-people-picker>
174+
<!-- Type any email address and press comma(,), semicolon(;), tab or enter to add it -->
175+
<script type="module">
176+
const peoplePicker = document.querySelector('mgt-people-picker');
177+
peoplePicker.selectedPeople = [{mail: "[email protected]", displayName: "[email protected]"}]
178+
</script>
179+
`;

0 commit comments

Comments
 (0)