Skip to content

Commit 9d1b282

Browse files
authored
Merge pull request #1424 from michaelmaillot/feat/richtext-label
Enhancing RichText control (label)
2 parents ee3a550 + e9f19ae commit 9d1b282

File tree

4 files changed

+93
-22
lines changed

4 files changed

+93
-22
lines changed

docs/documentation/docs/controls/RichText.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,40 @@ private onTextChange = (newText: string) => {
4141
}
4242
```
4343

44+
- By adding `label` property, the control is better identified, especially when used in a form
45+
46+
```TypeScript
47+
<RichText label="My multiline text field" value={this.props.value} />
48+
```
49+
50+
It is also possible to customize the control label's rendering:
51+
52+
```TypeScript
53+
const richText = (
54+
<RichText id="spfxRichText" label="My multiline text field"
55+
onRenderLabel={onRenderCustomLabel}
56+
value={this.props.value} />
57+
);
58+
59+
const onRenderCustomLabel = (rtProps: IRichTextProps): JSX.Element => {
60+
return <Label htmlFor={rtProps.id}>{rtProps.label}</Label>;
61+
}
62+
```
63+
4464
## Implementation
4565

4666
The RichText control can be configured with the following properties:
4767

4868
| Property | Type | Required | Description |
4969
| ---- | ---- | ---- | ---- |
70+
| id | string | no | The ID to apply to the RichText control. |
71+
| label | string | no | The label displayed above the RichText control. |
5072
| className | string | no | The custom CSS class to apply to the RichText control. |
5173
| isEditMode | boolean | no | `true` indicates that users will be able to edit the content of the RichText control. `false` will display the rich text as read-only. |
5274
| styleOptions | StyleOptions | no | Define the styles you want to show or hide for the rich text editor |
5375
| value | string | no | Sets the rich text to display in the RichText control. |
5476
| onChange | (text: string) => string | no | onChange handler for the RichText control. The function must return a `string` containing the rich text to display in the RichText control. |
77+
| onRenderLabel | (props: IRichTextProps) => JSX.Element | no | Custom renderer for the RichText control's label. The function must return a `JSX.Element`. |
5578

5679
`StyleOptions` interface
5780

src/controls/richText/RichText.tsx

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ import RichTextPropertyPane from './RichTextPropertyPane';
55
import ReactQuill, { Quill } from 'react-quill';
66
import styles from './RichText.module.scss';
77
import { IRichTextProps, IRichTextState } from './RichText.types';
8-
import { IconButton } from 'office-ui-fabric-react/lib/Button';
98
import { Guid } from '@microsoft/sp-core-library';
9+
import * as telemetry from '../../common/telemetry';
10+
import isEqual from 'lodash/isEqual';
11+
import { IconButton } from 'office-ui-fabric-react/lib/Button';
1012
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
1113
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
1214
import { TextField } from 'office-ui-fabric-react/lib/TextField';
1315
import { Link } from 'office-ui-fabric-react/lib/Link';
1416
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
1517
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
1618
import { Icon } from 'office-ui-fabric-react/lib/Icon';
17-
import { elementContains } from 'office-ui-fabric-react/lib/Utilities';
18-
import * as telemetry from '../../common/telemetry';
19+
import { css, elementContains } from 'office-ui-fabric-react/lib/Utilities';
1920
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
20-
import isEqual from 'lodash/isEqual';
21+
import { Label } from 'office-ui-fabric-react/lib/Label';
2122

2223
const TOOLBARPADDING: number = 28;
2324
/**
@@ -36,6 +37,7 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
3637
private _wrapperRef: HTMLDivElement = undefined;
3738
private _propertyPaneRef: RichTextPropertyPane = undefined;
3839
private _toolbarId = undefined;
40+
private _richTextId = undefined;
3941

4042
private ddStyleOpts = [{
4143
key: 0,
@@ -128,6 +130,9 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
128130

129131
// Get a unique toolbar id
130132
this._toolbarId = "toolbar_" + Guid.newGuid().toString();
133+
134+
// Get a unique rich text id if not provided by props
135+
this._richTextId = props.id ?? "richText_" + Guid.newGuid().toString();
131136
}
132137

133138
/**
@@ -139,9 +144,9 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
139144
document.addEventListener('click', this.handleClickOutside);
140145
document.addEventListener('focus', this.handleClickOutside);
141146

142-
const clientRect: ClientRect = this._wrapperRef.getBoundingClientRect();
143-
const parentClientRect: ClientRect = this._wrapperRef.parentElement.getBoundingClientRect();
144-
const toolbarTop: number = clientRect.top - parentClientRect.top - TOOLBARPADDING;
147+
const domRect: DOMRect = this._wrapperRef.getBoundingClientRect();
148+
const parentDomRect: DOMRect = this._wrapperRef.parentElement.getBoundingClientRect();
149+
const toolbarTop: number = domRect.top - parentDomRect.top - TOOLBARPADDING;
145150

146151
this.setState({
147152
wrapperTop: toolbarTop
@@ -444,11 +449,18 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
444449
const { text } = this.state;
445450
const { isEditMode } = this.props;
446451

452+
const renderLabel: JSX.Element = (
453+
(this.props.onRenderLabel && this.props.onRenderLabel(this.props)) ?? this.onRenderLabel()
454+
);
455+
447456
// If we're not in edit mode, display read-only version of the html
448457
if (!isEditMode) {
449458
return (
450-
<div className={`ql-editor ${styles.richtext} ${this.props.className || ''}`}
451-
dangerouslySetInnerHTML={{ __html: text }} />
459+
<>
460+
{renderLabel}
461+
<div id={this._richTextId} className={css("ql-editor", styles.richtext, this.props.className || null)}
462+
dangerouslySetInnerHTML={{ __html: text }} />
463+
</>
452464
);
453465
}
454466

@@ -501,7 +513,8 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
501513
Quill.register(sizeClass, true);
502514

503515
return (
504-
<div ref={(ref) => { this._wrapperRef = ref; }} className={`${styles.richtext && this.state.editing ? 'ql-active' : ''} ${this.props.className}`}>
516+
<div ref={(ref) => { this._wrapperRef = ref; }} className={css(styles.richtext && this.state.editing ? 'ql-active' : null, this.props.className || null) || null}>
517+
{renderLabel}
505518
<div id={this._toolbarId} style={{ top: this.state.wrapperTop }}>
506519
{
507520
showStyles && (
@@ -511,7 +524,7 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
511524
onRenderCaretDown={() => <Icon className={styles.toolbarSubmenuCaret} iconName="CaretDownSolid8" />}
512525
selectedKey={this.state.formats.header || 0}
513526
options={this.ddStyleOpts}
514-
onChanged={this.onChangeHeading}
527+
onChange={this.onChangeHeading}
515528
onRenderOption={this.onRenderStyleOption}
516529
onRenderTitle={this.onRenderStyleTitle}
517530
/>
@@ -560,7 +573,7 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
560573
onRenderCaretDown={() => <Icon className={styles.toolbarSubmenuCaret} iconName="CaretDownSolid8" />}
561574
selectedKey={this.state.formats.align || 'left'}
562575
options={this.ddAlignOpts}
563-
onChanged={this.onChangeAlign}
576+
onChange={this.onChangeAlign}
564577
onRenderOption={this.onRenderAlignOption}
565578
onRenderTitle={this.onRenderAlignTitle}
566579
/>
@@ -575,10 +588,10 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
575588
options={this.ddListOpts}
576589
// this option is not available yet
577590
notifyOnReselect={true} // allows re-selecting selected item to turn it off
578-
onChanged={this.onChangeList}
591+
onChange={this.onChangeList}
579592
onRenderOption={this.onRenderListOption}
580593
onRenderTitle={this.onRenderListTitle}
581-
onRenderPlaceHolder={this.onRenderListPlaceholder}
594+
onRenderPlaceholder={this.onRenderListPlaceholder}
582595
/>
583596
)
584597
}
@@ -624,6 +637,7 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
624637
</div>
625638

626639
<ReactQuill ref={this.linkQuill}
640+
id={this._richTextId}
627641
placeholder={placeholder}
628642
modules={modules}
629643
value={text || ''} //property value causes issues, defaultValue does not
@@ -644,7 +658,6 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
644658
{
645659
this.renderImageDialog()
646660
}
647-
648661
</div>
649662
);
650663
}
@@ -666,17 +679,17 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
666679
const newValue = !this.state.formats.underline;
667680
this.applyFormat("underline", newValue);
668681
}
669-
private onChangeHeading = (item: IDropdownOption): void => {
682+
private onChangeHeading = (_event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption, _index?: number): void => {
670683
const newHeadingValue = item.key === 0 ? '' : item.key.toString();
671684
this.applyFormat("header", newHeadingValue);
672685
}
673686

674-
private onChangeAlign = (item: IDropdownOption): void => {
687+
private onChangeAlign = (_event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption, _index?: number): void => {
675688
const newAlignValue = item.key === 'left' ? false : item.key.toString();
676689
this.applyFormat("align", newAlignValue);
677690
}
678691

679-
private onChangeList = (item: IDropdownOption): void => {
692+
private onChangeList = (_event: React.FormEvent<HTMLDivElement>, item?: IDropdownOption, _index?: number): void => {
680693
// if we're already in list mode, toggle off
681694
const key = item.key;
682695
const newAlignValue = (key === 'bullet' && this.state.formats.list === 'bullet') || (key === 'numbered' && this.state.formats.list === 'numbered') ? false : key;
@@ -977,4 +990,21 @@ export class RichText extends React.Component<IRichTextProps, IRichTextState> {
977990
private linkPropertyPane = (e: any): void => { // eslint-disable-line @typescript-eslint/no-explicit-any
978991
this._propertyPaneRef = e;
979992
}
993+
994+
/**
995+
* Renders the label above the rich text (if specified)
996+
*/
997+
private onRenderLabel = (): JSX.Element | null => {
998+
const { label } = this.props;
999+
1000+
if (label) {
1001+
return (
1002+
<Label htmlFor={this._richTextId}>
1003+
{label}
1004+
</Label>
1005+
);
1006+
}
1007+
1008+
return null;
1009+
}
9801010
}

src/controls/richText/RichText.types.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import { ISwatchColor } from './SwatchColorPickerGroup.types';
22
export interface IRichTextProps {
33
/**
4-
* CSS class to apply to the rich text editor.
5-
* @defaultvalue null
6-
*/
4+
* ID to apply to the rich text editor.
5+
* @defaultvalue undefined
6+
*/
7+
id?: string;
8+
9+
/**
10+
* Label displayed above the rich text.
11+
* @defaultvalue undefined
12+
*/
13+
label?: string;
14+
15+
/**
16+
* CSS class to apply to the rich text editor.
17+
* @defaultvalue null
18+
*/
719
className?: string;
820

921
/**
@@ -38,6 +50,12 @@ export interface IRichTextProps {
3850
* Returns the text that will be inserted in the rich text control.
3951
*/
4052
onChange?: (text: string) => string;
53+
54+
/**
55+
* Custom renderer for the label.
56+
* Returns the custom render.
57+
*/
58+
onRenderLabel?: (props: IRichTextProps) => JSX.Element;
4159
}
4260

4361
export interface StyleOptions {

src/webparts/controlsTest/components/ControlsTest.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1204,7 +1204,7 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
12041204
maxDate={new Date("05/01/2020")} />
12051205

12061206
{/* <RichText isEditMode={this.props.displayMode === DisplayMode.Edit} onChange={value => { this.richTextValue = value; return value; }} /> */}
1207-
<RichText value={this.state.richTextValue} isEditMode={this.props.displayMode === DisplayMode.Edit} onChange={value => { this.setState({ richTextValue: value }); return value; }} />
1207+
<RichText label="My rich text field" value={this.state.richTextValue} isEditMode={this.props.displayMode === DisplayMode.Edit} onChange={value => { this.setState({ richTextValue: value }); return value; }} />
12081208
<PrimaryButton text='Reset text' onClick={() => { this.setState({ richTextValue: 'test' }); }} />
12091209

12101210
{/* <ListItemAttachments listId='0ffa51d7-4ad1-4f04-8cfe-98209905d6da'

0 commit comments

Comments
 (0)