diff --git a/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.graph.ts b/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.graph.ts index 665e59a70b..b67d3ce89e 100644 --- a/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.graph.ts +++ b/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.graph.ts @@ -5,8 +5,15 @@ * ------------------------------------------------------------------------------------------- */ -import { IGraph, prepScopes } from '@microsoft/mgt-element'; +import { IGraph, BetaGraph, CacheItem, CacheService, CacheStore } from '@microsoft/mgt-element'; import { Team } from '@microsoft/microsoft-graph-types'; +import { + getPhotoForResource, + CachePhoto, + getPhotoInvalidationTime, + getIsPhotosCacheEnabled +} from '../../graph/graph.photos'; +import { schemas } from '../../graph/cacheStores'; /** * async promise, returns all Teams associated with the user logged in @@ -15,11 +22,46 @@ import { Team } from '@microsoft/microsoft-graph-types'; * @memberof Graph */ export async function getAllMyTeams(graph: IGraph): Promise { - const scopes = 'team.readbasic.all'; - const teams = await graph - .api('/me/joinedTeams') - .select(['displayName', 'id', 'isArchived']) - .middlewareOptions(prepScopes(scopes)) - .get(); + const teams = await graph.api('/me/joinedTeams').select(['displayName', 'id', 'isArchived']).get(); return teams ? teams.value : null; } + +/** An object collection of cached photos. */ +type CachePhotos = { + [key: string]: CachePhoto; +}; + +export async function getTeamsPhotosforPhotoIds(graph: BetaGraph, teamIds: string[]): Promise { + let cache: CacheStore; + let photos: CachePhotos = {}; + + if (getIsPhotosCacheEnabled()) { + cache = CacheService.getCache(schemas.photos, schemas.photos.stores.teams); + for (const id of teamIds) { + try { + const photoDetail = await cache.getValue(id); + if (photoDetail && getPhotoInvalidationTime() > Date.now() - photoDetail.timeCached) { + photos[id] = photoDetail; + } + } catch (_) {} + } + if (Object.keys(photos).length) { + return photos; + } + } + + let scopes = ['team.readbasic.all']; + photos = {}; + + for (const id of teamIds) { + try { + const photoDetail = await getPhotoForResource(graph, `/teams/${id}`, scopes); + if (getIsPhotosCacheEnabled() && photoDetail) { + cache.putValue(id, photoDetail); + } + photos[id] = photoDetail; + } catch (_) {} + } + + return photos; +} diff --git a/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.scss b/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.scss index b99a6a9115..5e1f839521 100644 --- a/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.scss +++ b/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.scss @@ -9,335 +9,115 @@ @import '../../styles/shared-styles.scss'; @import './mgt-teams-channel-picker.theme.scss'; -:host { +:host, +:host mgt-teams-channel-picker { font-family: $font-family; -} - -.root { - position: relative; - .input-wrapper { - font-family: $font-family; + .container { display: flex; - flex-wrap: wrap; - align-items: center; - padding: 4px 10px; - position: relative; - background-color: $input__background-color; - font-size: 14px; - color: $color; - height: 29px; - line-height: 19px; - cursor: text; - @include input__border($theme-default); - &:hover { - border-color: $input__border-color--hover; - } - - &.focused { - border-color: $input__border-color--focus; - } + flex-direction: column; + } - .selected-team { - padding-right: 10px; - border: none; - display: flex; - position: relative; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - align-items: center; - height: 24px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - .arrow { - margin-left: 8px; - margin-right: 8px; - } - .selected-team-name { - font-weight: bold; - } - .search-wrapper { - margin-left: 12px; - height: 18px; - display: flex; - flex-wrap: nowrap; - align-items: center; - margin-right: 30px; - } - } + .dropdown { + z-index: 1; + display: none; + margin-top: 4px; - .search-wrapper { - overflow: hidden; - height: 18px; + &.visible { display: flex; - flex-wrap: nowrap; - align-items: center; - margin-right: 30px; - } - .hide-icon { - display: none; } - .team-chosen-input { - display: inline-block; - border: none; - line-height: normal; - outline: none; - font-family: $font-family; - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 19px; - background-color: transparent; - color: $color; - padding-left: 5px; - &::placeholder { - color: $placeholder__color; + .team { + &-photo { + width: 24px; + position: inherit; + border-radius: 50%; + margin: 0px 6px; } - } - .team-chosen-input[contenteditable]:empty::after { - content: attr(data-placeholder); - color: $placeholder__color; - } - - .search-icon { - margin-top: 1px; - align-self: flex-start; - font-family: 'FabricMDL2Icons'; - color: var(--accent-color, $commblue_primary); - line-height: normal; - } - - .input-search { - height: 19px; - } - .input-search-start { - white-space: nowrap; - line-height: normal; - margin-inline-start: 0px; - margin-inline-end: 0px; - } - - &::after { - content: ''; - position: absolute; - width: 100%; - height: 90%; - top: 2px; - right: 2px; - @include selected-team__overflow($theme-default); - } - } + &-start-slot { + width: max-content; + } - .close-icon { - display: inline-block; - position: absolute; - right: 10px; - top: 12px; - line-height: normal; - font-family: 'FabricMDL2Icons'; - cursor: pointer; - color: $placeholder__color; - background-color: $input__background-color; - &:hover { - color: $color; + &-parent-name { + width: auto; + } } } - .dropdown { - padding-top: 5px; - padding-bottom: 11px; - position: absolute; - box-shadow: 0px 1.3289px 2.65781px rgba(180, 180, 180, 0.182), 0px 1.3289px 2.65781px rgba(68, 68, 68, 0.3); - border-radius: 8px; - background-color: $dropdown__background-color; - z-index: 1; /* Sit on top */ - width: 100%; - text-align: left; - list-style-type: none; + .search-error-text, + .loading-text { font-family: $font-family; font-style: normal; - font-weight: normal; - color: $color; - display: none; - max-height: 346px; - overflow-y: auto; - - &.visible { - display: block; - } - - .item { - .arrow svg { - fill: set-var(arrow, $theme-default, $channel-picker); - } - - &.list-team { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - -o-user-select: none; - display: flex; - flex-direction: row; - align-items: center; - padding: 5px 12px 6px 14px; - margin: 6px 4px 4px 4px; - border-radius: 4px; - font-family: $font-family; - font-style: normal; - font-weight: 600; - font-size: 16px; - color: $color; - - flex-flow: nowrap; - position: relative; - overflow: hidden; - white-space: nowrap; - background-color: transparent; - &:after { - content: ''; - position: absolute; - width: 36px; - height: 100%; - top: 0; - right: 0; - @include dropdown-item__overflow($theme-default); - } - - &:hover { - background-color: $dropdown-item__background-color--hover; - } - .arrow { - margin-right: 5px; - margin-bottom: 1px; - } - } + font-weight: 400; + font-size: 14px; + line-height: 20px; + } - &.selected { - background-color: set-var(dropdown-item__background-color--selected, $theme-default, $channel-picker); - } + .message-parent { + display: flex; + flex-direction: row; + gap: 5px; + padding: 5px; - &.focused { - background-color: $dropdown-item__background-color--hover; - } + .loading-text { + color: var(--color, #0078d4); + margin: auto; } + } +} - .channel-display { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - -o-user-select: none; - cursor: pointer; - padding-left: 21px; - padding-right: 10px; +:host fluent-card { + --fill-color: var(--dropdown-background-color, --fill-color); + --card-height: auto; + --width: var(--card-width); +} - .showing { - padding-left: 10px; - padding-top: 4px; - padding-bottom: 4px; - .channel-name-text { - font-size: 14px; - font-weight: normal; - color: $color; - margin: 0; - padding: 0; +:host fluent-text-field { + width: 100%; + --fill-color: var(--color, $color); + --neutral-fill-input-rest: var(--input-background-color, #ffffff); + --input-placeholder-rest: var(--placeholder-color, --input-placeholder-rest); + --input-placeholder-hover: var(--placeholder-color--focus, --input-placeholder-hover); +} +:host fluent-tree-view { + --tree-item-nested-width: 2em; +} - &.highlight-search-text { - font-weight: bold; - } - } - } +:host fluent-tree-item { + --tree-item-nested-width: 2em; + --neutral-foreground-rest: var(--color, $color); + --neutral-fill-stealth-hover: var(--dropdown-item-hover-background, --neutral-fill-stealth-hover); + --neutral-fill-secondary-rest: var(--dropdown-item-selected-background, --neutral-fill-secondary-rest); + --neutral-fill-stealth-rest: var(--dropdown-background-color, --neutral-fill-stealth-rest); +} - &:hover { - background-color: $dropdown-item__background-color--hover; - } - } +:host fluent-tree-view { + min-width: 100%; +} - .search-error-text, - .loading-text { - font-family: $font-family; - font-style: normal; - font-weight: 600; - font-size: 14px; - color: $color; - line-height: 19px; - text-align: center; - } +:host fluent-breadcrumb-item { + .team-parent-name { + font-weight: bold; + } + .team-photo { + width: 19px; + position: inherit; + border-radius: 50%; + } + .arrow { + margin-left: 8px; + margin-right: 8px; - .message-parent { - padding: 2px; - margin-top: 30px; - margin-bottom: 30px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - vertical-align: middle; - .loading-text { - margin-top: 3px; - margin-left: 9px; - color: #0078d4; - } + svg { + stroke: var(--arrow-fill, $color); } } } [dir='rtl'] { - .input-wrapper::after { - content: ''; - position: absolute; - width: 99%; - height: 90%; - top: 2px; - left: 0px; - background-image: linear-gradient( - to left, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) 80%, - $input__background-color 100% - ); - background-image: -moz-linear-gradient( - right, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) 80%, - $input__background-color 100% - ); - background-image: -o-linear-gradient( - right, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) 80%, - $input__background-color 100% - ); - background-image: -ms-linear-gradient( - right, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) 80%, - $input__background-color 100% - ); - background-image: -webkit-linear-gradient( - right, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) 80%, - $input__background-color 100% - ); - } - .search-wrapper { - margin-right: 0px !important; - } - .search-icon { - margin-left: 8px !important; - } - .channel-display { - padding: 0px 34px 0px 10px !important; - } - .close-icon { - right: auto; - left: 10px; + :host { + --direction: rtl; } .dropdown { text-align: right; @@ -355,24 +135,28 @@ .selected-team { padding-left: 10px; } + .message-parent { + .loading-text { + right: auto; + left: 10px; + padding-right: 8px; + text-align: right; + } + } } @media (forced-colors: active) and (prefers-color-scheme: dark) { - .root { + :host fluent-text-field { svg { - fill: rgb(255, 255, 255) !important; - fill-rule: nonzero !important; - clip-rule: nonzero !important; + stroke: rgb(255, 255, 255) !important; } } } @media (forced-colors: active) and (prefers-color-scheme: light) { - .root { + :host fluent-text-field { svg { - fill: rgb(0, 0, 0) !important; - fill-rule: nonzero !important; - clip-rule: nonzero !important; + stroke: rgb(0, 0, 0) !important; } } } diff --git a/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.ts b/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.ts index 2079146046..76a54cc870 100644 --- a/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.ts +++ b/packages/mgt-components/src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.ts @@ -7,16 +7,42 @@ import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import { html, TemplateResult } from 'lit'; -import { property } from 'lit/decorators.js'; +import { state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; -import { Providers, ProviderState, MgtTemplatedComponent, customElement, mgtHtml } from '@microsoft/mgt-element'; +import { + Providers, + ProviderState, + MgtTemplatedComponent, + BetaGraph, + customElement, + mgtHtml +} from '@microsoft/mgt-element'; import '../../styles/style-helper'; import '../sub-components/mgt-spinner/mgt-spinner'; import { getSvg, SvgIcon } from '../../utils/SvgHelper'; import { debounce } from '../../utils/Utils'; import { styles } from './mgt-teams-channel-picker-css'; -import { getAllMyTeams } from './mgt-teams-channel-picker.graph'; +import { getAllMyTeams, getTeamsPhotosforPhotoIds } from './mgt-teams-channel-picker.graph'; import { strings } from './strings'; +import { repeat } from 'lit/directives/repeat.js'; +import { registerFluentComponents } from '../../utils/FluentComponents'; +import { + fluentBreadcrumb, + fluentBreadcrumbItem, + fluentTreeView, + fluentTreeItem, + fluentCard, + fluentTextField +} from '@fluentui/web-components'; + +registerFluentComponents( + fluentBreadcrumb, + fluentBreadcrumbItem, + fluentCard, + fluentTreeView, + fluentTreeItem, + fluentTextField +); /** * Team with displayName @@ -162,7 +188,6 @@ export interface MgtTeamsChannelPickerConfig { * */ @customElement('teams-channel-picker') -// @customElement('mgt-teams-channel-picker') export class MgtTeamsChannelPicker extends MgtTemplatedComponent { /** * Array of styles to apply to the element. The styles should be defined @@ -191,6 +216,7 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { private static _config = { useTeamsBasedScopes: false }; + private teamsPhotos = {}; /** * Gets Selected item to be used @@ -235,31 +261,32 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { } // User input in search - private get _input(): HTMLElement { - return this.renderRoot.querySelector('.team-chosen-input'); + private get _input(): HTMLInputElement { + const wrapper = this.renderRoot.querySelector('fluent-text-field'); + const input = wrapper.shadowRoot.querySelector('input'); + return input; } private _inputValue: string = ''; - private _isFocused = false; - - private _selectedItemState: ChannelPickerItemState; + @state() private _selectedItemState: ChannelPickerItemState; private _items: DropdownItem[]; private _treeViewState: ChannelPickerItemState[] = []; + private _focusList: ChannelPickerItemState[] = []; // focus state - private _focusList: ChannelPickerItemState[] = []; - private _focusedIndex: number = -1; private debouncedSearch; // determines loading state - @property({ attribute: false }) private _isDropdownVisible; + @state() private _isDropdownVisible: boolean; + @state() private _isFocused: boolean; constructor() { super(); this.handleWindowClick = this.handleWindowClick.bind(this); - this.addEventListener('keydown', e => this.onUserKeyDown(e)); this.addEventListener('focus', _ => this.loadTeamsIfNotLoaded()); this.addEventListener('mouseover', _ => this.loadTeamsIfNotLoaded()); + this.addEventListener('blur', _ => this.lostFocus()); + this.clearState(); } /** @@ -270,6 +297,11 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { public connectedCallback() { super.connectedCallback(); window.addEventListener('click', this.handleWindowClick); + + const ownerDocument = this.renderRoot.ownerDocument; + if (ownerDocument) { + ownerDocument.documentElement.setAttribute('dir', this.direction); + } } /** @@ -300,7 +332,9 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { for (const item of this._treeViewState) { for (const channel of item.channels) { if (channel.item.id === channelId) { + item.isExpanded = true; this.selectChannel(channel); + this.markSelectedChannelInDropdown(channelId); return true; } } @@ -309,6 +343,21 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { return false; } + /** + * Marks a channel selected by ID as selected in the dropdown menu. + * It ensures the parent team is set to as expanded to show the channel. + * @param channelId ID string of the selected channel + */ + private markSelectedChannelInDropdown(channelId: string) { + const treeItem = this.renderRoot.querySelector(`[id='${channelId}']`) as HTMLElement; + if (treeItem) { + treeItem.setAttribute('selected', 'true'); + if (treeItem.parentElement) { + treeItem.parentElement.setAttribute('expanded', 'true'); + } + } + } + /** * Invoked on each update to perform rendering tasks. This method must return a lit-html TemplateResult. * Setting properties inside this method will not trigger the element to update. @@ -316,38 +365,30 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { * @memberof MgtTeamsChannelPicker */ public render() { - const inputClasses = { - focused: this._isFocused, - 'input-wrapper': true - }; - - const iconClasses = { - focused: this._isFocused && !!this._selectedItemState, - 'search-icon': true - }; - const dropdownClasses = { dropdown: true, visible: this._isDropdownVisible }; - const searchClasses = { - 'hide-icon': !!this._selectedItemState, - 'search-wrapper': true - }; - return ( this.renderTemplate('default', { teams: this.items }) || html` -
-
- ${this.renderSelected()} -
${this.renderSearchIcon()} ${this.renderInput()}
-
- ${this.renderCloseButton()} -
${this.renderDropdown()}
-
- ` +
+ this.handleInputChanged(e)}> +
${this.renderSelected()}
+
${this.renderChevrons()}${this.renderCloseButton()}
+
+ + ${this.renderDropdown()} + +
` ); } @@ -360,17 +401,28 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { */ protected renderSelected() { if (!this._selectedItemState) { - return html``; + return this.renderSearchIcon(); } + let icon: TemplateResult; + if (this._selectedItemState.parent.channels) { + icon = html`${this._selectedItemState.parent.item.displayName}`; + } + + const parentName = this._selectedItemState?.parent?.item?.displayName.trim(); + const channelName = this._selectedItemState?.item?.displayName.trim(); return html` -
  • -
    ${this._selectedItemState.parent.item.displayName}
    -
    ${getSvg(SvgIcon.TeamSeparator, '#B3B0AD')}
    - ${this._selectedItemState.item.displayName} -
    ${this.renderSearchIcon()} ${this.renderInput()}
    -
  • - `; + + + ${icon} + ${parentName} + ${getSvg(SvgIcon.TeamSeparator, '#000000')} + + ${channelName} + `; } /** @@ -384,6 +436,7 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { this._inputValue = ''; this._treeViewState = []; this._focusList = []; + this._isDropdownVisible = false; } /** @@ -402,56 +455,86 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { } /** - * Renders input field + * Renders close button * * @protected * @returns * @memberof MgtTeamsChannelPicker */ - protected renderInput() { - const rootClasses = { - 'input-search': !!this._selectedItemState, - 'input-search-start': !this._selectedItemState - }; - + protected renderCloseButton() { return html` -
    - this.handleInputChanged(e)} - contenteditable - > + `; } /** - * Renders close button + * Displays the close button after selecting a channel. + */ + protected showCloseIcon() { + const downChevron = this.renderRoot.querySelector('.down-chevron') as HTMLElement; + const upChevron = this.renderRoot.querySelector('.up-chevron') as HTMLElement; + const closeIcon = this.renderRoot.querySelector('.close-icon') as HTMLElement; + if (downChevron) { + downChevron.style.display = 'none'; + } + if (upChevron) { + upChevron.style.display = 'none'; + } + + if (closeIcon) { + closeIcon.style.display = null; + } + } + + /** + * Renders down chevron icon * * @protected * @returns * @memberof MgtTeamsChannelPicker */ - protected renderCloseButton() { + protected renderDownChevron() { return html` -
     -
    - `; +
    + + + +
    `; + } + + /** + * Renders up chevron icon + * + * @protected + * @returns + * @memberof MgtTeamsChannelPicker + */ + protected renderUpChevron() { + return html` + `; + } + + /** + * Renders both chevrons + */ + private renderChevrons() { + return html`${this.renderUpChevron()}${this.renderDownChevron()}`; } /** * Renders dropdown content * - * @param {ChannelPickerItemState[]} items - * @param {number} [level=0] * @returns * @memberof MgtTeamsChannelPicker */ @@ -476,23 +559,44 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { * * @protected * @param {ChannelPickerItemState[]} items - * @param {number} [level=0] * @returns * @memberof MgtTeamsChannelPicker */ - protected renderDropdownList(items: ChannelPickerItemState[], level: number = 0) { - if (items && items.length) { - return items.map((treeItem, index) => { - const isLeaf = !treeItem.channels; - const renderChannels = !isLeaf && treeItem.isExpanded; - - return html` - ${this.renderItem(treeItem)} - ${renderChannels ? this.renderDropdownList(treeItem.channels, level + 1) : html``} - `; - }); + protected renderDropdownList(items: ChannelPickerItemState[]) { + if (items && items.length > 0) { + let icon: TemplateResult = null; + + return html` + + ${repeat( + items, + (itemObj: ChannelPickerItemState) => itemObj?.item, + (obj: ChannelPickerItemState) => { + if (obj.channels) { + icon = html`${obj.item.displayName}`; + } + return html` + this.handleTeamTreeItemClick(e)}> + ${icon}${obj.item.displayName} + ${repeat( + obj?.channels, + (channels: ChannelPickerItemState) => channels.item, + (channel: ChannelPickerItemState) => { + return this.renderItem(channel); + } + )} + `; + } + )} + `; } - return null; } @@ -504,90 +608,13 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { * @memberof MgtTeamsChannelPicker */ protected renderItem(itemState: ChannelPickerItemState) { - let icon: TemplateResult = null; - - if (itemState.channels) { - // must be team with channels - icon = itemState.isExpanded ? getSvg(SvgIcon.ArrowDown, '#252424') : getSvg(SvgIcon.ArrowRight, '#252424'); - } - - let isSelected = false; - if (this.selectedItem) { - if (this.selectedItem.channel === itemState.item) { - isSelected = true; - } - } - - const classes = { - focused: this._focusList[this._focusedIndex] === itemState, - item: true, - 'list-team': itemState.channels ? true : false, - selected: isSelected - }; - - const dropDown = this.renderRoot.querySelector('.dropdown'); - - if (dropDown.children[this._focusedIndex]) { - dropDown.children[this._focusedIndex].scrollIntoView(false); - } - return html` -
    this.handleItemClick(itemState)} class="${classMap(classes)}"> -
    - ${icon} -
    - ${itemState.channels ? itemState.item.displayName : this.renderHighlightedText(itemState.item)} -
    - `; - } - - /** - * Renders the channel with the query text higlighted - * - * @protected - * @param {*} channel - * @returns - * @memberof MgtTeamsChannelPicker - */ - protected renderHighlightedText(channel: any) { - // tslint:disable-next-line: prefer-const - let channels: any = {}; - - const highlightLocation = channel.displayName.toLowerCase().indexOf(this._inputValue.toLowerCase()); - if (highlightLocation !== -1) { - // no location - if (highlightLocation === 0) { - // highlight is at the beginning of sentence - channels.first = ''; - channels.highlight = channel.displayName.slice(0, this._inputValue.length); - channels.last = channel.displayName.slice(this._inputValue.length, channel.displayName.length); - } else if (highlightLocation === channel.displayName.length) { - // highlight is at end of the sentence - channels.first = channel.displayName.slice(0, highlightLocation); - channels.highlight = channel.displayName.slice(highlightLocation, channel.displayName.length); - channels.last = ''; - } else { - // highlight is in middle of sentence - channels.first = channel.displayName.slice(0, highlightLocation); - channels.highlight = channel.displayName.slice(highlightLocation, highlightLocation + this._inputValue.length); - channels.last = channel.displayName.slice( - highlightLocation + this._inputValue.length, - channel.displayName.length - ); - } - } else { - channels.last = channel.displayName; - } - - return html` -
    -
    - ${channels.first}${channels.highlight}${channels.last} -
    -
    - `; + this.onUserKeyDown(e, itemState)} + @click=${() => this.handleItemClick(itemState)}> + ${itemState?.item.displayName} + `; } /** @@ -604,7 +631,10 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { template || html`
    -
    +
    ${this.strings.noResultsFound}
    @@ -655,6 +685,12 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { teams = await getAllMyTeams(graph); teams = teams.filter(t => !t.isArchived); + let teamsIds = teams.map(t => t.id); + + const beta = BetaGraph.fromGraph(graph); + + this.teamsPhotos = await getTeamsPhotosforPhotoIds(beta, teamsIds); + const batch = graph.createBatch(); const scopes = ['team.readbasic.all']; @@ -668,7 +704,7 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { const response = responses.get(team.id); if (response && response.content && response.content.value) { - team.channels = response.content.value.map(c => { + team.channels = response.content.value.map((c: MicrosoftGraph.Team) => { return { item: c }; @@ -687,20 +723,55 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { this.resetFocusState(); } + /** + * Handles operations that are performed on the DOM when you remove a + * channel. For example on clicking the X button. + * @param item a selected channel item + */ + private removeSelectedChannel(item: ChannelPickerItemState) { + this.selectChannel(item); + const treeItems = this.renderRoot.querySelectorAll('fluent-tree-item') as NodeListOf; + if (treeItems) { + treeItems.forEach((treeItem: HTMLElement) => { + treeItem.removeAttribute('expanded'); + treeItem.removeAttribute('selected'); + }); + } + } + private handleItemClick(item: ChannelPickerItemState) { if (item.channels) { item.isExpanded = !item.isExpanded; } else { this.selectChannel(item); + this.lostFocus(); } + } - this._focusedIndex = -1; - this.resetFocusState(); + private handleTeamTreeItemClick(event: Event) { + event.preventDefault(); + event.stopImmediatePropagation(); + const element = event.target as HTMLElement; + if (element) { + const expanded = element.getAttribute('expanded'); + + if (!!expanded) { + element.removeAttribute('expanded'); + } else { + element.setAttribute('expanded', 'true'); + } + element.removeAttribute('selected'); + const hasId = element.getAttribute('id'); + if (hasId) { + element.setAttribute('selected', 'true'); + } + } } - private handleInputChanged(e) { - if (this._inputValue !== e.target.textContent) { - this._inputValue = e.target.textContent; + private handleInputChanged(e: KeyboardEvent) { + const target = e.target as HTMLInputElement; + if (this._inputValue !== target?.value) { + this._inputValue = target?.value; } else { return; } @@ -717,10 +788,28 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { this.debouncedSearch(); } + private onUserKeyDown(e: KeyboardEvent, item?: ChannelPickerItemState) { + const key = e.code; + switch (key) { + case 'Enter': + this.selectChannel(item); + this.resetFocusState(); + this.lostFocus(); + e.preventDefault(); + break; + case 'Backspace': + if (this._inputValue.length === 0 && this._selectedItemState) { + this.selectChannel(null); + this.resetFocusState(); + e.preventDefault(); + } + break; + } + } + private filterList() { if (this.items) { this._treeViewState = this.generateTreeViewState(this.items, this._inputValue); - this._focusedIndex = -1; this.resetFocusState(); } } @@ -797,81 +886,6 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { } } - private onUserKeyDown(event: KeyboardEvent) { - if (event.keyCode === 13) { - // No new line - event.preventDefault(); - } - - if (this._treeViewState.length === 0) { - return; - } - - const currentFocusedItem = this._focusList[this._focusedIndex]; - - switch (event.keyCode) { - case 40: // down - this._focusedIndex = (this._focusedIndex + 1) % this._focusList.length; - this.requestUpdate(); - event.preventDefault(); - break; - case 38: // up - if (this._focusedIndex === -1) { - this._focusedIndex = this._focusList.length; - } - this._focusedIndex = (this._focusedIndex - 1 + this._focusList.length) % this._focusList.length; - this.requestUpdate(); - event.preventDefault(); - break; - case 39: // right - if (currentFocusedItem && currentFocusedItem.channels && !currentFocusedItem.isExpanded) { - currentFocusedItem.isExpanded = true; - this.resetFocusState(); - event.preventDefault(); - } - break; - case 37: // left - if (currentFocusedItem && currentFocusedItem.channels && currentFocusedItem.isExpanded) { - currentFocusedItem.isExpanded = false; - this.resetFocusState(); - event.preventDefault(); - } - break; - case 9: // tab - if (!currentFocusedItem) { - this.lostFocus(); - break; - } - case 13: // return/enter - if (currentFocusedItem && currentFocusedItem.channels) { - // focus item is a Team - currentFocusedItem.isExpanded = !currentFocusedItem.isExpanded; - this.resetFocusState(); - event.preventDefault(); - } else if (currentFocusedItem && !currentFocusedItem.channels) { - this.selectChannel(currentFocusedItem); - - // refocus to new textbox on initial selection - this.resetFocusState(); - this._focusedIndex = -1; - event.preventDefault(); - } - break; - case 8: // backspace - if (this._inputValue.length === 0 && this._selectedItemState) { - this.selectChannel(null); - event.preventDefault(); - } - break; - case 27: // esc - this.selectChannel(this._selectedItemState); - this._focusedIndex = -1; - this.resetFocusState(); - event.preventDefault(); - break; - } - } - private gainedFocus() { this._isFocused = true; const input = this._input; @@ -880,30 +894,77 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent { } this._isDropdownVisible = true; + this.toggleChevron(); + this.resetFocusState(); } private lostFocus() { - this._isFocused = false; const input = this._input; if (input) { - input.textContent = this._inputValue = ''; + input.value = this._inputValue = ''; + input.textContent = ''; } + this._isFocused = false; this._isDropdownVisible = false; this.filterList(); + this.toggleChevron(); + this.requestUpdate(); + + if (this._selectedItemState !== undefined) { + this.showCloseIcon(); + } } private selectChannel(item: ChannelPickerItemState) { - if (this._selectedItemState !== item) { - this._selectedItemState = item; - this.fireCustomEvent('selectionChanged', this.selectedItem); + if (item && this._selectedItemState !== item) { + this._input.setAttribute('disabled', 'true'); + } else { + this._input.removeAttribute('disabled'); } + this._selectedItemState = item; + this.lostFocus(); + this.fireCustomEvent('selectionChanged', this.selectedItem); + } - const input = this._input; - if (input) { - input.textContent = this._inputValue = ''; + /** + * Hides the close icon. + */ + private hideCloseIcon() { + const closeIcon = this.renderRoot.querySelector('.close-icon') as HTMLElement; + if (closeIcon) { + closeIcon.style.display = 'none'; } - this.requestUpdate(); + } + + /** + * Toggles the up and down chevron depending on the dropdown + * visibility. + */ + private toggleChevron() { + const downChevron = this.renderRoot.querySelector('.down-chevron') as HTMLElement; + const upChevron = this.renderRoot.querySelector('.up-chevron') as HTMLElement; + if (this._isDropdownVisible) { + if (downChevron) { + downChevron.style.display = 'none'; + } + if (upChevron) { + upChevron.style.display = null; + } + } else { + if (downChevron) { + downChevron.style.display = null; + this.hideCloseIcon(); + } + if (upChevron) { + upChevron.style.display = 'none'; + } + } + this.hideCloseIcon(); + } + + private handleUpChevronClick(e: Event) { + e.stopPropagation(); this.lostFocus(); } } diff --git a/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.scss b/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.scss index c9298068c7..a708ff576c 100644 --- a/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.scss +++ b/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.scss @@ -6,23 +6,3 @@ */ @import '../../../styles/shared-styles.scss'; - -:host { - .spinner { - border: 2px solid #c7e0f4; /* Light grey */ - border-top: 2px solid #0078d4; /* Blue */ - border-radius: 50%; - width: 20px; - height: 20px; - animation: spin 2s linear infinite; - } - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } -} diff --git a/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.tests.ts b/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.tests.ts index 0324a391e7..7044b2b094 100644 --- a/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.tests.ts +++ b/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.tests.ts @@ -18,6 +18,6 @@ describe('mgt-spinner tests', () => { it('should render', async () => { const spinnerHtml = await screen.findByTitle('spinner'); expect(spinnerHtml).not.toBeNull(); - expect(spinner.shadowRoot.innerHTML).toContain('
    '); + expect(spinner.shadowRoot.innerHTML).toContain(''); }); }); diff --git a/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.ts b/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.ts index e1902a4e4a..6b9c8320c6 100644 --- a/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.ts +++ b/packages/mgt-components/src/components/sub-components/mgt-spinner/mgt-spinner.ts @@ -34,8 +34,6 @@ export class MgtSpinner extends MgtBaseComponent { * @memberof MgtSpinner */ public render() { - return html` -
    - `; + return html``; } } diff --git a/packages/mgt-components/src/graph/cacheStores.ts b/packages/mgt-components/src/graph/cacheStores.ts index 6775ebf3c9..b9454cba3d 100644 --- a/packages/mgt-components/src/graph/cacheStores.ts +++ b/packages/mgt-components/src/graph/cacheStores.ts @@ -30,7 +30,8 @@ export const schemas = { stores: { contacts: 'contacts', users: 'users', - groups: 'groups' + groups: 'groups', + teams: 'teams' }, version: 1 }, diff --git a/packages/mgt-components/src/utils/SvgHelper.ts b/packages/mgt-components/src/utils/SvgHelper.ts index d24f76f72c..fb4f3f5cb3 100644 --- a/packages/mgt-components/src/utils/SvgHelper.ts +++ b/packages/mgt-components/src/utils/SvgHelper.ts @@ -260,9 +260,7 @@ export function getSvg(svgIcon: SvgIcon, color?: string) { return html` - - - `; + `; case SvgIcon.SkypeArrow: return html` diff --git a/packages/mgt-element/src/utils/LocalizationHelper.ts b/packages/mgt-element/src/utils/LocalizationHelper.ts index 2677de403a..0dd2634baf 100644 --- a/packages/mgt-element/src/utils/LocalizationHelper.ts +++ b/packages/mgt-element/src/utils/LocalizationHelper.ts @@ -46,7 +46,9 @@ export class LocalizationHelper { * @memberof LocalizationHelper */ public static getDocumentDirection() { - return document.body?.getAttribute('dir') || document.documentElement?.getAttribute('dir'); + // Re-set the dir to ltr if the dir attribute is already loaded and the first two options + // are returning null values. + return document.body?.getAttribute('dir') || document.documentElement?.getAttribute('dir') || 'ltr'; } /** diff --git a/stories/components/teamsChannelPicker/teamsChannelPicker.a.js b/stories/components/teamsChannelPicker/teamsChannelPicker.a.js index 446c512e93..08a4eecceb 100644 --- a/stories/components/teamsChannelPicker/teamsChannelPicker.a.js +++ b/stories/components/teamsChannelPicker/teamsChannelPicker.a.js @@ -50,9 +50,9 @@ export const selectionChangedEvent = () => html` picker.addEventListener('selectionChanged', e => { const output = document.querySelector('.output'); - if (e.detail.length) { - output.innerHTML = 'channel: ' + e.detail[0].channel.displayName; - output.innerHTML += '
    team: ' + e.detail[0].team.displayName; + if (e.detail) { + output.innerHTML = 'channel: ' + e.detail.channel.displayName; + output.innerHTML += '
    team: ' + e.detail.team.displayName; } else { output.innerText = 'no channel selected'; }