Skip to content

Commit 80e1af1

Browse files
committed
Merge branch 'modern-taxonomy-picker-2' of https://github.com/SherpasGroup/sp-dev-fx-controls-react into SherpasGroup-modern-taxonomy-picker-2
2 parents 666e0af + 76e6011 commit 80e1af1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1794
-69
lines changed
1.91 KB
Loading
7.45 KB
Loading
3.61 KB
Loading
16.2 KB
Loading
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Modern Taxonomy Picker
2+
3+
This control allows you to select one or more Terms from a TermSet via its TermSet ID. You can also configure the control to select the child terms from a specific term in the TermSet by setting the anchorTermId. This is the modern version of the taxonomy picker that uses the REST API and makes use of some load on demand features which makes it well suited for large term sets.
4+
5+
!!! note "Disclaimer"
6+
Since this control is meant to look as and work in the same way as the out-of-the-box control it lacks some of the features from the legacy ```TaxonomyPicker``` control. If you need some of those features please continue using the legacy version.
7+
8+
**Empty term picker**
9+
10+
![Empty term picker](../assets/modernTaxonomyPicker-empty.png)
11+
12+
**Selecting terms**
13+
14+
![Selecting terms](../assets/modernTaxonomyPicker-tree-selection.png)
15+
16+
**Selected terms in picker**
17+
18+
![Selected terms in the input](../assets/modernTaxonomyPicker-selected-terms.png)
19+
20+
**Term picker: Auto Complete**
21+
22+
![Selected terms in the input](../assets/modernTaxonomyPicker-input-autocomplete.png)
23+
24+
25+
## How to use this control in your solutions
26+
27+
- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency.
28+
- Import the following modules to your component:
29+
30+
```TypeScript
31+
import { ModernTaxonomyPicker } from "@pnp/spfx-controls-react/lib/ModernTaxonomyPicker";
32+
```
33+
34+
- Use the `ModernTaxonomyPicker` control in your code as follows:
35+
36+
```TypeScript
37+
<ModernTaxonomyPicker allowMultipleSelections={true}
38+
termSetId="f233d4b7-68fb-41ef-8b58-2af0bafc0d38"
39+
panelTitle="Select Term"
40+
label="Taxonomy Picker"
41+
context={this.props.context}
42+
onChange={this.onTaxPickerChange} />
43+
```
44+
45+
- With the `onChange` property you can capture the event of when the terms in the picker has changed:
46+
47+
```typescript
48+
private onTaxPickerChange(terms : ITermInfo[]) {
49+
console.log("Terms", terms);
50+
}
51+
```
52+
53+
## Implementation
54+
55+
The ModernTaxonomyPicker control can be configured with the following properties:
56+
57+
| Property | Type | Required | Description |
58+
| ---- | ---- | ---- | ---- |
59+
| panelTitle | string | yes | TermSet Picker Panel title. |
60+
| label | string | yes | Text displayed above the Taxonomy Picker. |
61+
| disabled | boolean | no | Specify if the control should be disabled. Default value is false. |
62+
| context | BaseComponentContext | yes | Context of the current web part or extension. |
63+
| initialValues | ITermInfo[] | no | Defines the terms selected by default. ITermInfo comes from PnP/PnPjs and can be imported with <br/>```import { ITermInfo } from '@pnp/sp/taxonomy';``` |
64+
| allowMultipleSelections | boolean | no | Defines if the user can select only one or multiple terms. Default value is false. |
65+
| termSetId | string | yes | The Id of the TermSet that you would like the Taxonomy Picker to select terms from. |
66+
| onChange | function | no | Captures the event of when the terms in the picker has changed. |
67+
| anchorTermId | string | no | Set the id of a child term in the TermSet to be able to select terms from that level and below. |
68+
| placeHolder | string | no | Short text hint to display in picker. |
69+
| required | boolean | no | Specifies if to display an asterisk near the label. Default value is false. |
70+
| customPanelWidth | number | no | Custom panel width in pixels. |
71+
| termPickerProps | IModernTermPickerProps | no | Custom properties for the term picker (More info: [IBasePickerProps interface](https://developer.microsoft.com/en-us/fluentui#/controls/web/pickers#IBasePickerProps)). |
72+
| themeVariant | IReadonlyTheme | no | The current loaded SharePoint theme/section background (More info: [Supporting section backgrounds](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/supporting-section-backgrounds)). |
73+
74+
![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/TaxonomyPicker)

docs/documentation/docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ The following controls are currently available:
8484
- [LivePersona](./controls/LivePersona) (Live Persona control)
8585
- [LocationPicker](./controls/LocationPicker) (Location Picker control)
8686
- [Map](./controls/Map) (renders a map in a web part)
87+
- [ModernTaxonomyPicker](./controls/ModernTaxonomyPicker) (Modern Taxonomy Picker)
8788
- [MyTeams](./controls/MyTeams) (My Teams)
8889
- [PeoplePicker](./controls/PeoplePicker) (People Picker)
8990
- [Placeholder](./controls/Placeholder) (shows an initial placeholder if the web part has to be configured)

docs/documentation/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nav:
4646
- LivePersona: 'controls/LivePersona.md'
4747
- LocationPicker: 'controls/LocationPicker.md'
4848
- Map: 'controls/Map.md'
49+
- ModernTaxonomyPicker: 'controls/ModernTaxonomyPicker.md'
4950
- MyTeams: 'controls/MyTeams.md'
5051
- Pagination: 'controls/Pagination.md'
5152
- PeoplePicker: 'controls/PeoplePicker.md'

src/ModernTaxonomyPicker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './controls/modernTaxonomyPicker';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.modernTaxonomyPicker {
2+
3+
.termField {
4+
align-items: center;
5+
border-spacing: 0;
6+
display: flex;
7+
width: 100%;
8+
9+
.termFieldInput {
10+
flex-grow: 1;
11+
}
12+
13+
.termFieldButton {
14+
text-align: center;
15+
width: 42px;
16+
}
17+
18+
input[type="text"] {
19+
cursor: pointer;
20+
opacity: 0.8;
21+
width: 100%;
22+
}
23+
}
24+
25+
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import * as React from 'react';
2+
import { BaseComponentContext } from '@microsoft/sp-component-base';
3+
import { Guid } from '@microsoft/sp-core-library';
4+
import { IIconProps } from 'office-ui-fabric-react/lib/components/Icon';
5+
import { PrimaryButton,
6+
DefaultButton,
7+
IconButton,
8+
IButtonStyles
9+
} from 'office-ui-fabric-react/lib/Button';
10+
import { Label } from 'office-ui-fabric-react/lib/Label';
11+
import { Panel,
12+
PanelType
13+
} from 'office-ui-fabric-react/lib/Panel';
14+
import { IBasePickerStyleProps,
15+
IBasePickerStyles,
16+
ISuggestionItemProps
17+
} from 'office-ui-fabric-react/lib/Pickers';
18+
import { IStackTokens,
19+
Stack
20+
} from 'office-ui-fabric-react/lib/Stack';
21+
import { IStyleFunctionOrObject } from 'office-ui-fabric-react/lib/Utilities';
22+
import { sp } from '@pnp/sp';
23+
import { SPTaxonomyService } from '../../services/SPTaxonomyService';
24+
import { TaxonomyPanelContents } from './taxonomyPanelContents';
25+
import styles from './ModernTaxonomyPicker.module.scss';
26+
import * as strings from 'ControlStrings';
27+
import { TooltipHost } from '@microsoft/office-ui-fabric-react-bundle';
28+
import { useId } from '@uifabric/react-hooks';
29+
import { ITooltipHostStyles } from 'office-ui-fabric-react';
30+
import { ITermInfo,
31+
ITermSetInfo,
32+
ITermStoreInfo
33+
} from '@pnp/sp/taxonomy';
34+
import { TermItemSuggestion } from './termItem/TermItemSuggestion';
35+
import { ModernTermPicker } from './modernTermPicker/ModernTermPicker';
36+
import { IModernTermPickerProps, ITermItemProps } from './modernTermPicker/ModernTermPicker.types';
37+
import { TermItem } from './termItem/TermItem';
38+
import { IReadonlyTheme } from "@microsoft/sp-component-base";
39+
import { isUndefined } from 'lodash';
40+
41+
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
42+
43+
export interface IModernTaxonomyPickerProps {
44+
allowMultipleSelections?: boolean;
45+
termSetId: string;
46+
anchorTermId?: string;
47+
panelTitle: string;
48+
label: string;
49+
context: BaseComponentContext;
50+
initialValues?: ITermInfo[];
51+
disabled?: boolean;
52+
required?: boolean;
53+
onChange?: (newValue?: ITermInfo[]) => void;
54+
onRenderItem?: (itemProps: ITermItemProps) => JSX.Element;
55+
onRenderSuggestionsItem?: (term: ITermInfo, itemProps: ISuggestionItemProps<ITermInfo>) => JSX.Element;
56+
placeHolder?: string;
57+
customPanelWidth?: number;
58+
themeVariant?: IReadonlyTheme;
59+
termPickerProps?: Optional<IModernTermPickerProps, 'onResolveSuggestions'>;
60+
}
61+
62+
export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
63+
const [taxonomyService] = React.useState(() => new SPTaxonomyService(props.context));
64+
const [panelIsOpen, setPanelIsOpen] = React.useState(false);
65+
const [selectedOptions, setSelectedOptions] = React.useState<ITermInfo[]>([]);
66+
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITermInfo[]>([]);
67+
const [currentTermStoreInfo, setCurrentTermStoreInfo] = React.useState<ITermStoreInfo>();
68+
const [currentTermSetInfo, setCurrentTermSetInfo] = React.useState<ITermSetInfo>();
69+
const [currentAnchorTermInfo, setCurrentAnchorTermInfo] = React.useState<ITermInfo>();
70+
const [currentLanguageTag, setCurrentLanguageTag] = React.useState<string>("");
71+
72+
React.useEffect(() => {
73+
sp.setup(props.context);
74+
taxonomyService.getTermStoreInfo()
75+
.then((termStoreInfo) => {
76+
setCurrentTermStoreInfo(termStoreInfo);
77+
setCurrentLanguageTag(props.context.pageContext.cultureInfo.currentUICultureName !== '' ?
78+
props.context.pageContext.cultureInfo.currentUICultureName :
79+
currentTermStoreInfo.defaultLanguageTag);
80+
setSelectedOptions(Object.prototype.toString.call(props.initialValues) === '[object Array]' ?
81+
props.initialValues.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfo;}) :
82+
[]);
83+
});
84+
taxonomyService.getTermSetInfo(Guid.parse(props.termSetId))
85+
.then((termSetInfo) => {
86+
setCurrentTermSetInfo(termSetInfo);
87+
});
88+
if (props.anchorTermId && props.anchorTermId !== Guid.empty.toString()) {
89+
taxonomyService.getTermById(Guid.parse(props.termSetId), props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty)
90+
.then((anchorTermInfo) => {
91+
setCurrentAnchorTermInfo(anchorTermInfo);
92+
});
93+
}
94+
}, []);
95+
96+
React.useEffect(() => {
97+
if (props.onChange) {
98+
props.onChange(selectedOptions);
99+
}
100+
}, [selectedOptions]);
101+
102+
function onOpenPanel(): void {
103+
if (props.disabled === true) {
104+
return;
105+
}
106+
setSelectedPanelOptions(selectedOptions);
107+
setPanelIsOpen(true);
108+
}
109+
110+
function onClosePanel(): void {
111+
setSelectedPanelOptions([]);
112+
setPanelIsOpen(false);
113+
}
114+
115+
function onApply(): void {
116+
setSelectedOptions([...selectedPanelOptions]);
117+
onClosePanel();
118+
}
119+
120+
async function onResolveSuggestions(filter: string, selectedItems?: ITermInfo[]): Promise<ITermInfo[]> {
121+
if (filter === '') {
122+
return [];
123+
}
124+
const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, currentLanguageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty);
125+
const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => {
126+
if (!selectedItems || selectedItems.length === 0) {
127+
return true;
128+
}
129+
return selectedItems.every((item) => item.id !== term.id);
130+
});
131+
const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable);
132+
return filteredTermsAndAvailable;
133+
}
134+
135+
async function onLoadParentLabel(termId: Guid): Promise<string> {
136+
const termInfo = await taxonomyService.getTermById(Guid.parse(props.termSetId), termId);
137+
if (termInfo.parent) {
138+
let labelsWithMatchingLanguageTag = termInfo.parent.labels.filter((termLabel) => (termLabel.languageTag === currentLanguageTag));
139+
if (labelsWithMatchingLanguageTag.length === 0) {
140+
labelsWithMatchingLanguageTag = termInfo.parent.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag));
141+
}
142+
return labelsWithMatchingLanguageTag[0]?.name;
143+
}
144+
else {
145+
let termSetNames = currentTermSetInfo.localizedNames.filter((name) => name.languageTag === currentLanguageTag);
146+
if (termSetNames.length === 0) {
147+
termSetNames = currentTermSetInfo.localizedNames.filter((name) => name.languageTag === currentTermStoreInfo.defaultLanguageTag);
148+
}
149+
return termSetNames[0].name;
150+
}
151+
}
152+
153+
function onRenderSuggestionsItem(term: ITermInfo, itemProps: ISuggestionItemProps<ITermInfo>): JSX.Element {
154+
return (
155+
<TermItemSuggestion
156+
onLoadParentLabel={onLoadParentLabel}
157+
term={term}
158+
termStoreInfo={currentTermStoreInfo}
159+
languageTag={currentLanguageTag}
160+
{...itemProps}
161+
/>
162+
);
163+
}
164+
165+
function onRenderItem(itemProps: ITermItemProps): JSX.Element {
166+
let labels = itemProps.item.labels.filter((name) => name.languageTag === currentLanguageTag && name.isDefault);
167+
if (labels.length === 0) {
168+
labels = itemProps.item.labels.filter((name) => name.languageTag === currentTermStoreInfo.defaultLanguageTag && name.isDefault);
169+
}
170+
171+
return labels.length > 0 ? (
172+
<TermItem languageTag={currentLanguageTag} termStoreInfo={currentTermStoreInfo} {...itemProps}>{labels[0].name}</TermItem>
173+
) : null;
174+
}
175+
176+
function getTextFromItem(termInfo: ITermInfo): string {
177+
let labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentLanguageTag));
178+
if (labelsWithMatchingLanguageTag.length === 0) {
179+
labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag));
180+
}
181+
return labelsWithMatchingLanguageTag[0]?.name;
182+
}
183+
184+
const calloutProps = { gapSpace: 0 };
185+
const tooltipId = useId('tooltip');
186+
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
187+
const addTermButtonStyles: IButtonStyles = {rootHovered: {backgroundColor: "inherit"}, rootPressed: {backgroundColor: "inherit"}};
188+
const termPickerStyles: IStyleFunctionOrObject<IBasePickerStyleProps, IBasePickerStyles> = { input: {minheight: 34}, text: {minheight: 34} };
189+
190+
return (
191+
<div className={styles.modernTaxonomyPicker}>
192+
{props.label && <Label required={props.required}>{props.label}</Label>}
193+
<div className={styles.termField}>
194+
<div className={styles.termFieldInput}>
195+
<ModernTermPicker
196+
{...props.termPickerProps}
197+
removeButtonAriaLabel={strings.ModernTaxonomyPickerRemoveButtonText}
198+
onResolveSuggestions={props.termPickerProps?.onResolveSuggestions ?? onResolveSuggestions}
199+
itemLimit={props.allowMultipleSelections ? undefined : 1}
200+
selectedItems={selectedOptions}
201+
disabled={props.disabled}
202+
styles={props.termPickerProps?.styles ?? termPickerStyles}
203+
onChange={(itms?: ITermInfo[]) => {
204+
setSelectedOptions(itms || []);
205+
setSelectedPanelOptions(itms || []);
206+
}}
207+
getTextFromItem={getTextFromItem}
208+
pickerSuggestionsProps={props.termPickerProps?.pickerSuggestionsProps ?? {noResultsFoundText: strings.ModernTaxonomyPickerNoResultsFound}}
209+
inputProps={props.termPickerProps?.inputProps ?? {
210+
'aria-label': props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder,
211+
placeholder: props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder
212+
}}
213+
onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem}
214+
onRenderItem={props.onRenderItem ?? onRenderItem}
215+
themeVariant={props.themeVariant}
216+
/>
217+
</div>
218+
<div className={styles.termFieldButton}>
219+
<TooltipHost
220+
content={strings.ModernTaxonomyPickerAddTagButtonTooltip}
221+
id={tooltipId}
222+
calloutProps={calloutProps}
223+
styles={hostStyles}
224+
>
225+
<IconButton disabled={props.disabled} styles={addTermButtonStyles} iconProps={{ iconName: 'Tag' } as IIconProps} onClick={onOpenPanel} aria-describedby={tooltipId} />
226+
</TooltipHost>
227+
</div>
228+
</div>
229+
230+
<Panel
231+
isOpen={panelIsOpen}
232+
hasCloseButton={true}
233+
closeButtonAriaLabel={strings.ModernTaxonomyPickerPanelCloseButtonText}
234+
onDismiss={onClosePanel}
235+
isLightDismiss={true}
236+
type={props.customPanelWidth ? PanelType.custom : PanelType.medium}
237+
customWidth={props.customPanelWidth ? `${props.customPanelWidth}px` : undefined}
238+
headerText={props.panelTitle}
239+
onRenderFooterContent={() => {
240+
const horizontalGapStackTokens: IStackTokens = {
241+
childrenGap: 10,
242+
};
243+
return (
244+
<Stack horizontal disableShrink tokens={horizontalGapStackTokens}>
245+
<PrimaryButton text={strings.ModernTaxonomyPickerApplyButtonText} value="Apply" onClick={onApply} />
246+
<DefaultButton text={strings.ModernTaxonomyPickerCancelButtonText} value="Cancel" onClick={onClosePanel} />
247+
</Stack>
248+
);
249+
}}>
250+
251+
{
252+
props.termSetId && (
253+
<div key={props.termSetId} >
254+
<TaxonomyPanelContents
255+
allowMultipleSelections={props.allowMultipleSelections}
256+
onResolveSuggestions={props.termPickerProps?.onResolveSuggestions ?? onResolveSuggestions}
257+
onLoadMoreData={taxonomyService.getTerms}
258+
anchorTermInfo={currentAnchorTermInfo}
259+
termSetInfo={currentTermSetInfo}
260+
termStoreInfo={currentTermStoreInfo}
261+
context={props.context}
262+
termSetId={Guid.parse(props.termSetId)}
263+
pageSize={50}
264+
selectedPanelOptions={selectedPanelOptions}
265+
setSelectedPanelOptions={setSelectedPanelOptions}
266+
placeHolder={props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder}
267+
onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem}
268+
onRenderItem={props.onRenderItem ?? onRenderItem}
269+
getTextFromItem={getTextFromItem}
270+
languageTag={currentLanguageTag}
271+
themeVariant={props.themeVariant}
272+
termPickerProps={props.termPickerProps}
273+
/>
274+
</div>
275+
)
276+
}
277+
</Panel>
278+
</div >
279+
);
280+
}

0 commit comments

Comments
 (0)