Skip to content

Commit 105f0e2

Browse files
Added translations, tooltip, placeholder, fix for loading terms
1 parent 8e9fdc6 commit 105f0e2

File tree

6 files changed

+103
-76
lines changed

6 files changed

+103
-76
lines changed

src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { BaseComponentContext } from '@microsoft/sp-component-base';
33
import { Guid } from '@microsoft/sp-core-library';
44
import { IIconProps } from 'office-ui-fabric-react/lib/components/Icon';
5-
import { PrimaryButton, DefaultButton, IconButton } from 'office-ui-fabric-react/lib/Button';
5+
import { PrimaryButton, DefaultButton, IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button';
66
import { Label } from 'office-ui-fabric-react/lib/Label';
77
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
88
import { ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers';
@@ -15,19 +15,9 @@ import FieldErrorMessage from '../errorMessage/ErrorMessage';
1515
import { TaxonomyForm } from './taxonomyForm';
1616
import styles from './ModernTaxonomyPicker.module.scss';
1717
import * as strings from 'ControlStrings';
18-
19-
// TODO: remove/replace interface IPickerTerm
20-
export interface IPickerTerm {
21-
name: string;
22-
key: string;
23-
path: string;
24-
termSet: string;
25-
termSetName?: string;
26-
}
27-
28-
// TODO: remove/replace interface IPickerTerms
29-
export interface IPickerTerms extends Array<IPickerTerm> { }
30-
18+
import { TooltipHost } from '@microsoft/office-ui-fabric-react-bundle';
19+
import { useId } from '@uifabric/react-hooks';
20+
import { ITooltipHostStyles } from 'office-ui-fabric-react';
3121
export interface IModernTaxonomyPickerProps {
3222
allowMultipleSelections: boolean;
3323
termSetId: string;
@@ -39,15 +29,15 @@ export interface IModernTaxonomyPickerProps {
3929
errorMessage?: string; // TODO: is this needed?
4030
disabled?: boolean;
4131
required?: boolean;
32+
onChange?: (newValue?: ITag[]) => void;
33+
placeHolder?: string;
4234
}
4335

4436
export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
4537
const [termsService] = React.useState(() => new SPTaxonomyService(props.context));
46-
const [terms, setTerms] = React.useState<ITermInfo[]>([]);
4738
const [errorMessage, setErrorMessage] = React.useState(props.errorMessage);
4839
const [internalErrorMessage, setInternalErrorMessage] = React.useState<string>();
4940
const [panelIsOpen, setPanelIsOpen] = React.useState(false);
50-
const [loading, setLoading] = React.useState(false); // was called loaded
5141
const [selectedOptions, setSelectedOptions] = React.useState<ITag[]>([]);
5242
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITag[]>([]);
5343

@@ -58,31 +48,38 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
5848
}, []);
5949

6050
React.useEffect(() => {
61-
setSelectedOptions(props.initialValues || []);
51+
if(Object.prototype.toString.call(props.initialValues) === '[object Array]' ) {
52+
setSelectedOptions(props.initialValues);
53+
}
54+
else {
55+
setSelectedOptions([]);
56+
}
6257
}, [props.initialValues]);
6358

6459
React.useEffect(() => {
6560
setErrorMessage(props.errorMessage);
6661
}, [props.errorMessage]);
6762

63+
React.useEffect(() => {
64+
if (props.onChange) {
65+
props.onChange(selectedOptions);
66+
}
67+
}, [selectedOptions]);
68+
6869
async function onOpenPanel(): Promise<void> {
6970
if (props.disabled === true) {
7071
return;
7172
}
72-
setLoading(true);
73-
const siteUrl = props.context.pageContext.site.absoluteUrl;
74-
const newTerms = await termsService.getTerms(Guid.parse(props.termSetId), Guid.empty, '', true, 50);
75-
setTerms(newTerms.value);
76-
setLoading(false);
73+
setSelectedPanelOptions(selectedOptions);
7774
setPanelIsOpen(true);
7875
}
7976

8077
function onClosePanel(): void {
81-
setLoading(false);
78+
setSelectedPanelOptions([]);
8279
setPanelIsOpen(false);
8380
}
8481

85-
function onSave(): void {
82+
function onApply(): void {
8683
setSelectedOptions([...selectedPanelOptions]);
8784
onClosePanel();
8885
}
@@ -111,30 +108,42 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
111108
return filteredTags;
112109
}
113110

114-
const { label, disabled, allowMultipleSelections, panelTitle, required } = props;
111+
const { label, disabled, allowMultipleSelections, panelTitle, required, placeHolder } = props;
112+
const calloutProps = { gapSpace: 0 };
113+
const tooltipId = useId('tooltip');
114+
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: 'inline-block' } };
115+
115116
return (
116117
<div className={styles.modernTaxonomyPicker}>
117118
{label && <Label required={required}>{label}</Label>}
118119
<div className={styles.termField}>
119120
<div className={styles.termFieldInput}>
120121
<TagPicker
121-
removeButtonAriaLabel="Remove"
122+
removeButtonAriaLabel={strings.ModernTaxonomyPickerRemoveButtonText}
122123
onResolveSuggestions={onResolveSuggestions}
123124
itemLimit={allowMultipleSelections ? undefined : 1}
124125
selectedItems={selectedOptions}
126+
disabled={disabled}
125127
onChange={(itms?: ITag[]) => {
126128
setSelectedOptions(itms || []);
127129
setSelectedPanelOptions(itms || []);
128130
}}
129131
getTextFromItem={(tag: ITag, currentValue?: string) => tag.name}
130132
inputProps={{
131-
'aria-label': 'Tag Picker',
132-
placeholder: 'Ange en term som du vill tagga'
133+
'aria-label': placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder,
134+
placeholder: placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder
133135
}}
134136
/>
135137
</div>
136138
<div className={styles.termFieldButton}>
137-
<IconButton disabled={disabled} iconProps={{ iconName: 'Tag' } as IIconProps} onClick={onOpenPanel} />
139+
<TooltipHost
140+
content={strings.ModernTaxonomyPickerAddTagButtonTooltip}
141+
id={tooltipId}
142+
calloutProps={calloutProps}
143+
styles={hostStyles}
144+
>
145+
<IconButton disabled={disabled} iconProps={{ iconName: 'Tag' } as IIconProps} onClick={onOpenPanel} aria-describedby={tooltipId} />
146+
</TooltipHost>
138147
</div>
139148
</div>
140149

@@ -153,22 +162,17 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
153162
};
154163
return (
155164
<Stack horizontal disableShrink tokens={horizontalGapStackTokens}>
156-
<PrimaryButton text={strings.SaveButtonLabel} value="Save" onClick={onSave} />
157-
<DefaultButton text={strings.CancelButtonLabel} value="Cancel" onClick={onClosePanel} />
165+
<PrimaryButton text={strings.ModernTaxonomyPickerApplyButtonText} value="Apply" onClick={onApply} />
166+
<DefaultButton text={strings.ModernTaxonomyPickerCancelButtonText} value="Cancel" onClick={onClosePanel} />
158167
</Stack>
159168
);
160169
}}>
161170

162171
{
163-
/* Show spinner in the panel while retrieving terms */
164-
loading === true ? <Spinner size={SpinnerSize.medium} /> : ''
165-
}
166-
{
167-
loading === false && props.termSetId && (
172+
props.termSetId && (
168173
<div key={props.termSetId} >
169174
<TaxonomyForm
170175
allowMultipleSelections={allowMultipleSelections}
171-
terms={terms}
172176
onResolveSuggestions={onResolveSuggestions}
173177
onLoadMoreData={termsService.getTerms}
174178
getTermSetInfo={termsService.getTermSetInfo}
@@ -177,6 +181,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
177181
pageSize={50}
178182
selectedPanelOptions={selectedPanelOptions}
179183
setSelectedPanelOptions={setSelectedPanelOptions}
184+
placeHolder={placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder}
180185
/>
181186
</div>
182187
)

src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import * as React from 'react';
22
import styles from './TaxonomyForm.module.scss';
3-
import { Checkbox, ChoiceGroup, classNamesFunction, DetailsRow, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionProps, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IChoiceGroupStyleProps, IChoiceGroupStyles, IColumn, IGroup, IGroupedList, IGroupFooterProps, IGroupHeaderCheckboxProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Spinner, TagPicker } from 'office-ui-fabric-react';
3+
import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupedList, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Spinner, TagPicker } from 'office-ui-fabric-react';
44
import { ITermInfo, ITermSetInfo } from '@pnp/sp/taxonomy';
55
import { Guid } from '@microsoft/sp-core-library';
66
import { BaseComponentContext } from '@microsoft/sp-component-base';
77
import { css } from '@uifabric/utilities/lib/css';
8+
import * as strings from 'ControlStrings';
89

910
export interface ITaxonomyFormProps {
1011
context: BaseComponentContext;
1112
allowMultipleSelections: boolean;
12-
terms: ITermInfo[];
1313
termSetId: Guid;
1414
pageSize: number;
1515
selectedPanelOptions: ITag[];
1616
setSelectedPanelOptions: React.Dispatch<React.SetStateAction<ITag[]>>;
1717
onResolveSuggestions: (filter: string, selectedItems?: ITag[]) => ITag[] | PromiseLike<ITag[]>;
1818
onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>;
1919
getTermSetInfo: (termSetId: Guid) => Promise<ITermSetInfo | undefined>;
20+
placeHolder: string;
2021
}
2122

2223
export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITaxonomyFormProps> {
@@ -27,16 +28,16 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
2728

2829

2930
React.useEffect(() => {
30-
setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, props.termSetId.toString()]);
31-
3231
props.getTermSetInfo(props.termSetId)
3332
.then((termSetInfo) => {
3433
const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName;
3534

3635
const termSetName = termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag)[0].name;
37-
const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: false };
36+
const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: termSetInfo.childrenCount > 0 };
3837
setGroups([rootGroup]);
39-
props.onLoadMoreData(props.termSetId, Guid.empty, '', true)
38+
setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, termSetInfo.id]);
39+
if (termSetInfo.childrenCount > 0) {
40+
props.onLoadMoreData(props.termSetId, Guid.empty, '', true)
4041
.then((terms) => {
4142
const grps: IGroup[] = terms.value.map(term => {
4243
const g: IGroup = {
@@ -60,7 +61,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
6061
setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== props.termSetId.toString()));
6162
setGroups([rootGroup]);
6263
});
63-
64+
}
6465
});
6566
}, []);
6667

@@ -154,8 +155,9 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
154155

155156
const onRenderTitle = (groupHeaderProps: IGroupHeaderProps) => {
156157
if (groupHeaderProps.group.level === 0) {
158+
const labelStyles: IStyleFunctionOrObject<ILabelStyleProps, ILabelStyles> = {root: {fontWeight: "normal"}};
157159
return (
158-
<Label>{groupHeaderProps.group.name}</Label>
160+
<Label styles={labelStyles}>{groupHeaderProps.group.name}</Label>
159161
);
160162
}
161163
if (props.allowMultipleSelections) {
@@ -179,21 +181,18 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
179181
}
180182
else {
181183
const isSelected = props.selectedPanelOptions?.[0]?.key === groupHeaderProps.group.key;
182-
const selectedStyle: IStyleFunctionOrObject<IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles> = isSelected ? { choiceFieldWrapper: { fontWeight: 'bold' } } : { choiceFieldWrapper: { fontWeight: 'normal' } };
183-
const getClassNames = classNamesFunction<IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles>();
184-
185-
const classNames = getClassNames(selectedStyle!, {
186-
theme: undefined,
187-
hasIcon: false,
188-
hasImage: false,
189-
checked: false,
190-
disabled: false,
191-
imageIsLarge: false,
192-
imageSize: undefined,
193-
focused: false,
194-
});
184+
const selectedStyle: IStyleFunctionOrObject<IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles> = isSelected ? { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'bold', } } : { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'normal' } };
195185
const isDisabled = groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false;
196-
const options: IChoiceGroupOption[] = [{ key: groupHeaderProps.group.key, text: groupHeaderProps.group.name, styles: selectedStyle, onRenderLabel: (p) => <Label htmlFor={p.id} className={classNames.field}><span id={p.labelId} className={css(styles.choiceOption, isSelected && styles.selectedChoiceOption)}>{p.text}</span></Label> }];
186+
const options: IChoiceGroupOption[] = [{
187+
key: groupHeaderProps.group.key,
188+
text: groupHeaderProps.group.name,
189+
styles: selectedStyle,
190+
onRenderLabel: (p) =>
191+
<span id={p.labelId} className={css(styles.choiceOption, isSelected && styles.selectedChoiceOption)}>
192+
{p.text}
193+
</span>
194+
}];
195+
197196
return (
198197
<ChoiceGroup
199198
options={options}
@@ -206,16 +205,13 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
206205
};
207206

208207
const onRenderHeader = (headerProps: IGroupHeaderProps): JSX.Element => {
209-
const headerCountStyle = { display: 'none' };
210-
const checkButtonStyle = { display: 'none' };
211-
const expandStyle = { visibility: 'hidden' };
212208
const groupHeaderStyles: IStyleFunctionOrObject<IGroupHeaderStyleProps, IGroupHeaderStyles> = {
213-
expand: !headerProps.group.children || headerProps.group.level === 0 ? expandStyle : null,
214-
expandIsCollapsed: !headerProps.group.children || headerProps.group.level === 0 ? expandStyle : null,
215-
check: checkButtonStyle,
216-
headerCount: headerCountStyle,
209+
expand: { height: 42, visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible" },
210+
expandIsCollapsed: { visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible" },
211+
check: { display: 'none' },
212+
headerCount: { display: 'none' },
217213
groupHeaderContainer: { height: 36, paddingTop: 3, paddingBottom: 3, paddingLeft: 3, paddingRight: 3, alignItems: 'center', },
218-
root: { height: 42 }
214+
root: { height: 42 },
219215
};
220216

221217
return (
@@ -243,6 +239,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
243239
return (
244240
<div style={{ height: '48px', lineHeight: '48px' }}>
245241
<Link onClick={() => {
242+
setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, footerProps.group.key]);
246243
props.onLoadMoreData(props.termSetId, footerProps.group.key === props.termSetId.toString() ? Guid.empty : Guid.parse(footerProps.group.key), footerProps.group.data.skiptoken, true)
247244
.then((terms) => {
248245
const grps: IGroup[] = terms.value.map(term => {
@@ -268,7 +265,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
268265
});
269266
}}
270267
styles={linkStyles}>
271-
Load more...
268+
{strings.ModernTaxonomyPickerLoadMoreText}
272269
</Link>
273270
</div>
274271
);
@@ -295,28 +292,28 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement<ITax
295292
props.setSelectedPanelOptions(itms || []);
296293
};
297294

298-
const tagPickerStyles: IStyleFunctionOrObject<IBasePickerStyleProps, IBasePickerStyles> = { text: { borderStyle: 'none', borderWidth: '0px' } };
295+
const tagPickerStyles: IStyleFunctionOrObject<IBasePickerStyleProps, IBasePickerStyles> = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4}, input: {height: 34}, text: { borderStyle: 'none', borderWidth: '0px' } };
299296

300297
return (
301298
<div className={styles.taxonomyForm}>
302299
<div className={styles.taxonomyTreeSelector}>
303300
<div>
304301
<TagPicker
305-
removeButtonAriaLabel="Remove"
302+
removeButtonAriaLabel={strings.ModernTaxonomyPickerRemoveButtonText}
306303
onResolveSuggestions={props.onResolveSuggestions}
307304
itemLimit={props.allowMultipleSelections ? undefined : 1}
308305
selectedItems={props.selectedPanelOptions}
309306
onChange={onPickerChange}
310307
getTextFromItem={getTagText}
311308
styles={tagPickerStyles}
312309
inputProps={{
313-
'aria-label': 'Tag Picker',
314-
placeholder: 'Ange en term som du vill tagga'
310+
'aria-label': props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder,
311+
placeholder: props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder
315312
}}
316313
/>
317314
</div>
318315
</div>
319-
<Label className={styles.taxonomyTreeLabel}>Välj en tagg</Label>
316+
<Label className={styles.taxonomyTreeLabel}>{strings.ModernTaxonomyPickerTreeTitle}</Label>
320317
<div>
321318
<GroupedList
322319
componentRef={groupedListRef}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ export * from './Accordion';
3737
export * from './AnimatedDialog';
3838
export * from './DynamicForm';
3939
export * from './Carousel';
40+
export * from './ModernTaxonomyPicker';

src/loc/en-us.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,14 @@ define([], () => {
387387
ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment",
388388
ListItemCommentsLabel: "Comments",
389389
ListItemCommentsNoCommentsLabel: "There is no Comments",
390-
OrgAssetsLinkLabel: "Your organisation"
390+
OrgAssetsLinkLabel: "Your organisation",
391+
392+
ModernTaxonomyPickerDefaultPlaceHolder: "Type term to tag",
393+
ModernTaxonomyPickerTreeTitle: "Select one or more tags",
394+
ModernTaxonomyPickerAddTagButtonTooltip: "Add Tag",
395+
ModernTaxonomyPickerApplyButtonText: "Apply",
396+
ModernTaxonomyPickerCancelButtonText: "Cancel",
397+
ModernTaxonomyPickerLoadMoreText: "Load more",
398+
ModernTaxonomyPickerRemoveButtonText: "Remove"
391399
};
392400
});

src/loc/mystrings.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,15 @@ declare interface IControlStrings {
363363

364364
// Location picker
365365
customDisplayName:string;
366+
367+
// Modern taxonomy picker
368+
ModernTaxonomyPickerDefaultPlaceHolder: string;
369+
ModernTaxonomyPickerTreeTitle: string;
370+
ModernTaxonomyPickerAddTagButtonTooltip: string;
371+
ModernTaxonomyPickerApplyButtonText: string;
372+
ModernTaxonomyPickerCancelButtonText: string;
373+
ModernTaxonomyPickerLoadMoreText: string;
374+
ModernTaxonomyPickerRemoveButtonText: string;
366375
}
367376

368377
declare interface IDateTimeStrings {

0 commit comments

Comments
 (0)