lightning-base-components 1.14.3-alpha → 1.14.7-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/metadata/raptor.json +37 -4
- package/package.json +11 -4
- package/scopedImports/@salesforce-label-LightningModalBase.cancelandclose.js +1 -0
- package/scopedImports/@salesforce-label-LightningProgressBar.progressBar.js +1 -0
- package/src/lightning/alert/__docs__/alert.md +101 -0
- package/src/lightning/alert/__examples__disabled/basic/basic.css +7 -0
- package/src/lightning/alert/__examples__disabled/basic/basic.html +8 -0
- package/src/lightning/alert/__examples__disabled/basic/basic.js +14 -0
- package/src/lightning/alert/alert.html +3 -0
- package/src/lightning/alert/alert.js +78 -0
- package/src/lightning/alert/alert.js-meta.xml +6 -0
- package/src/lightning/ariaObserver/__component__/ariaObserver.spec.js +9 -0
- package/src/lightning/ariaObserver/ariaObserver.js +24 -35
- package/src/lightning/baseFormattedText/baseFormattedText.html +6 -1
- package/src/lightning/baseFormattedText/baseFormattedText.js +5 -0
- package/src/lightning/buttonMenu/buttonMenu.js +12 -0
- package/src/lightning/confirm/__docs__/confirm.md +100 -0
- package/src/lightning/confirm/__examples__disabled/basic/basic.css +7 -0
- package/src/lightning/confirm/__examples__disabled/basic/basic.html +8 -0
- package/src/lightning/confirm/__examples__disabled/basic/basic.js +14 -0
- package/src/lightning/confirm/confirm.html +3 -0
- package/src/lightning/confirm/confirm.js +80 -0
- package/src/lightning/confirm/confirm.js-meta.xml +6 -0
- package/src/lightning/datatable/__examples__/withInfiniteLoading/fetchDataHelper.js +21 -0
- package/src/lightning/datatable/__examples__/withInfiniteLoading/withInfiniteLoading.html +13 -0
- package/src/lightning/datatable/__examples__/withInfiniteLoading/withInfiniteLoading.js +42 -0
- package/src/lightning/datatable/autoWidthStrategy.js +170 -61
- package/src/lightning/datatable/{resizer.js → columnResizer.js} +0 -0
- package/src/lightning/datatable/columnWidthManager.js +226 -44
- package/src/lightning/datatable/columns.js +166 -71
- package/src/lightning/datatable/datatable.js +132 -60
- package/src/lightning/datatable/fixedWidthStrategy.js +43 -8
- package/src/lightning/datatable/headerActions.js +2 -2
- package/src/lightning/datatable/infiniteLoading.js +100 -28
- package/src/lightning/datatable/inlineEdit.js +21 -30
- package/src/lightning/datatable/keyboard.js +166 -131
- package/src/lightning/datatable/renderManager.js +117 -122
- package/src/lightning/datatable/{datatableResizeObserver.js → resizeObserver.js} +46 -29
- package/src/lightning/datatable/resizeSensor.js +19 -3
- package/src/lightning/datatable/rowSelection.js +1 -1
- package/src/lightning/datatable/rowSelectionShared.js +33 -20
- package/src/lightning/datatable/rows.js +7 -8
- package/src/lightning/datatable/sort.js +8 -8
- package/src/lightning/datatable/state.js +14 -2
- package/src/lightning/datatable/templates/div/div.html +127 -117
- package/src/lightning/datatable/templates/table/table.html +5 -0
- package/src/lightning/datatable/tree.js +25 -0
- package/src/lightning/datatable/types.js +77 -9
- package/src/lightning/datatable/utils.js +51 -24
- package/src/lightning/datatable/virtualization.js +319 -0
- package/src/lightning/datatable/widthManagerShared.js +27 -3
- package/src/lightning/datatable/wrapText.js +115 -48
- package/src/lightning/formattedDateTime/__docs__/formattedDateTime.md +36 -3
- package/src/lightning/formattedDateTime/__examples__/datetime/datetime.html +2 -2
- package/src/lightning/formattedDateTime/__examples__/datetime/datetime.js +3 -1
- package/src/lightning/formattedDateTime/__examples__/time/time.html +1 -1
- package/src/lightning/formattedDateTime/__examples__/time/time.js +3 -1
- package/src/lightning/formattedDateTime/formattedDateTime.js +1 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/standard/dashboard_component.html +7 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/standard/slack.html +7 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/standard/tableau.html +7 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/standard/travel_mode.html +2 -2
- package/src/lightning/iconSvgTemplates/buildTemplates/templates.js +8 -1
- package/src/lightning/iconSvgTemplates/buildTemplates/utility/data_model.html +7 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/utility/serialized_product.html +1 -1
- package/src/lightning/iconSvgTemplates/buildTemplates/utility/serialized_product_transaction.html +2 -1
- package/src/lightning/iconSvgTemplates/buildTemplates/utility/slack.html +7 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/utility/tableau.html +7 -0
- package/src/lightning/iconSvgTemplates/buildTemplates/utility/video_off.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/standard/dashboard_component.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/standard/slack.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/standard/tableau.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/standard/travel_mode.html +2 -2
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/templates.js +8 -1
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/data_model.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/serialized_product.html +1 -1
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/serialized_product_transaction.html +2 -1
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/slack.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/tableau.html +7 -0
- package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/video_off.html +7 -0
- package/src/lightning/iconSvgTemplatesStandard/buildTemplates/standard/dashboard_component.html +7 -0
- package/src/lightning/iconSvgTemplatesStandard/buildTemplates/standard/slack.html +7 -0
- package/src/lightning/iconSvgTemplatesStandard/buildTemplates/standard/tableau.html +7 -0
- package/src/lightning/iconSvgTemplatesStandard/buildTemplates/standard/travel_mode.html +2 -2
- package/src/lightning/iconSvgTemplatesStandard/buildTemplates/templates.js +4 -1
- package/src/lightning/iconSvgTemplatesStandardRtl/buildTemplates/standard/dashboard_component.html +7 -0
- package/src/lightning/iconSvgTemplatesStandardRtl/buildTemplates/standard/slack.html +7 -0
- package/src/lightning/iconSvgTemplatesStandardRtl/buildTemplates/standard/tableau.html +7 -0
- package/src/lightning/iconSvgTemplatesStandardRtl/buildTemplates/standard/travel_mode.html +2 -2
- package/src/lightning/iconSvgTemplatesStandardRtl/buildTemplates/templates.js +4 -1
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/templates.js +5 -1
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/data_model.html +7 -0
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/serialized_product.html +1 -1
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/serialized_product_transaction.html +2 -1
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/slack.html +7 -0
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/tableau.html +7 -0
- package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/video_off.html +7 -0
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/templates.js +5 -1
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/data_model.html +7 -0
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/serialized_product.html +1 -1
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/serialized_product_transaction.html +2 -1
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/slack.html +7 -0
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/tableau.html +7 -0
- package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/video_off.html +7 -0
- package/src/lightning/input/__docs__/input.md +2 -0
- package/src/lightning/input/input.html +2 -5
- package/src/lightning/interactiveDialogBase/interactiveDialogBase.css +494 -0
- package/src/lightning/interactiveDialogBase/interactiveDialogBase.html +63 -0
- package/src/lightning/interactiveDialogBase/interactiveDialogBase.js +200 -0
- package/src/lightning/menuItem/menuItem.js +4 -1
- package/src/lightning/modalBase/modalBase.css +20 -0
- package/src/lightning/modalBase/modalBase.html +54 -0
- package/src/lightning/modalBase/modalBase.js +1039 -0
- package/src/lightning/overlay/__docs__/overlay.md +90 -0
- package/src/lightning/overlay/__examples__/alert/alert.html +27 -0
- package/src/lightning/overlay/__examples__/alert/alert.js +33 -0
- package/src/lightning/overlay/__examples__/basic/basic.css +7 -0
- package/src/lightning/overlay/__examples__/basic/basic.html +18 -0
- package/src/lightning/overlay/__examples__/basic/basic.js +61 -0
- package/src/lightning/overlay/__examples__/demo/demo.html +29 -0
- package/src/lightning/overlay/__examples__/demo/demo.js +40 -0
- package/src/lightning/overlay/__examples__/panel/panel.html +17 -0
- package/src/lightning/overlay/__examples__/panel/panel.js +21 -0
- package/src/lightning/overlay/overlay.html +3 -0
- package/src/lightning/overlay/overlay.js +45 -0
- package/src/lightning/overlayContainer/__docs__/overlayContainer.md +0 -0
- package/src/lightning/overlayContainer/overlayContainer.html +3 -0
- package/src/lightning/overlayContainer/overlayContainer.js +138 -0
- package/src/lightning/overlayManager/overlayManager.js +54 -0
- package/src/lightning/overlayUtils/overlayUtils.js +17 -0
- package/src/lightning/progressBar/progressBar.html +2 -1
- package/src/lightning/progressBar/progressBar.js +18 -1
- package/src/lightning/prompt/__docs__/prompt.md +102 -0
- package/src/lightning/prompt/__examples__disabled/basic/basic.css +7 -0
- package/src/lightning/prompt/__examples__disabled/basic/basic.html +8 -0
- package/src/lightning/prompt/__examples__disabled/basic/basic.js +15 -0
- package/src/lightning/prompt/prompt.css +81 -0
- package/src/lightning/prompt/prompt.html +8 -0
- package/src/lightning/prompt/prompt.js +92 -0
- package/src/lightning/prompt/prompt.js-meta.xml +6 -0
- package/src/lightning/spinner/spinner.html +1 -1
- package/src/lightning/spinner/spinner.js +12 -0
- package/src/lightning/utilsPrivate/phonify.js +1 -1
- package/scopedImports/@salesforce-label-LightningModalBase.close.js +0 -1
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
import { api, LightningElement } from 'lwc';
|
|
2
|
+
import { classSet } from 'lightning/utils';
|
|
3
|
+
import {
|
|
4
|
+
makeEverythingExceptElementInert,
|
|
5
|
+
normalizeString,
|
|
6
|
+
synchronizeAttrs,
|
|
7
|
+
restoreInertness,
|
|
8
|
+
hasAnimation,
|
|
9
|
+
ARIA,
|
|
10
|
+
isAriaDescriptionSupported,
|
|
11
|
+
} from 'lightning/utilsPrivate';
|
|
12
|
+
import { instanceName, secure } from 'lightning/overlayUtils';
|
|
13
|
+
import { getElementWithFocus } from 'lightning/focusUtils';
|
|
14
|
+
import closeButtonAltText from '@salesforce/label/LightningModalBase.cancelandclose';
|
|
15
|
+
import disableCloseBtnMessage from '@salesforce/label/LightningModalBase.waitstate';
|
|
16
|
+
|
|
17
|
+
const DEBOUNCE_RESIZE = 300;
|
|
18
|
+
|
|
19
|
+
export default class LightningModalBase extends LightningElement {
|
|
20
|
+
// this is visible in DOM, changed from 'lightning-modal-base'
|
|
21
|
+
static [instanceName] = 'lightning-modal-base';
|
|
22
|
+
|
|
23
|
+
// private tracked state
|
|
24
|
+
initialRender = true;
|
|
25
|
+
autoFocusCompletedOnce = false;
|
|
26
|
+
windowResizeEventsBound = false;
|
|
27
|
+
timeoutId = 0;
|
|
28
|
+
disableCloseButton = false;
|
|
29
|
+
sectionAriaBusy = null;
|
|
30
|
+
closeButtonAltText = closeButtonAltText;
|
|
31
|
+
disableCloseBtnMessage = disableCloseBtnMessage;
|
|
32
|
+
// modalHeader, child
|
|
33
|
+
headerRegistered = false;
|
|
34
|
+
headerHeight = 0;
|
|
35
|
+
headerDefaultSlotIsPopulated = false;
|
|
36
|
+
headerSlotWrapperId = null;
|
|
37
|
+
headerSlotHasRendered = false;
|
|
38
|
+
headerLabelId = null;
|
|
39
|
+
headerLabelIsPopulated = null;
|
|
40
|
+
headerTitleRef = null;
|
|
41
|
+
headerTabElemRef = null;
|
|
42
|
+
|
|
43
|
+
// modalBody, child
|
|
44
|
+
bodyRegistered = false;
|
|
45
|
+
bodyDefaultSlotIsPopulated = false;
|
|
46
|
+
bodySlotHasRendered = false;
|
|
47
|
+
bodyId = null;
|
|
48
|
+
baseUpdateBodyCallback = null;
|
|
49
|
+
bodyResizeScheduled = false;
|
|
50
|
+
bodyTabElemRef = null;
|
|
51
|
+
|
|
52
|
+
// modalFooter, child
|
|
53
|
+
footerRegistered = false;
|
|
54
|
+
footerHeight = 0;
|
|
55
|
+
footerSlotHasRendered = false;
|
|
56
|
+
footerDefaultSlotIsPopulated = false;
|
|
57
|
+
footerTabElemRef = null;
|
|
58
|
+
|
|
59
|
+
// aria attributes
|
|
60
|
+
modalLabel = null;
|
|
61
|
+
modalLabelledBy = null;
|
|
62
|
+
modalDescribedBy = null;
|
|
63
|
+
// currently used for disableClose
|
|
64
|
+
showAriaLiveMessage = false;
|
|
65
|
+
ariaLiveMessage = '';
|
|
66
|
+
|
|
67
|
+
// modal features
|
|
68
|
+
isModalOpen = false;
|
|
69
|
+
isModalTransitioningIn = false;
|
|
70
|
+
_size = 'medium';
|
|
71
|
+
// modal background elements
|
|
72
|
+
savedInertElements = [];
|
|
73
|
+
// before modal opened, element previously focused
|
|
74
|
+
savedActiveElement;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* <lightning-modal> label value
|
|
78
|
+
* Text to display as the heading at the top of the modal
|
|
79
|
+
*/
|
|
80
|
+
get label() {
|
|
81
|
+
const modal = this.modal;
|
|
82
|
+
if (!modal) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
return modal.label;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* <lightning-modal> description value
|
|
90
|
+
* Text used for the accessible description of the modal. see updateAriaDescription()
|
|
91
|
+
* Note: this value is not visible in the UI, and only to screen readers
|
|
92
|
+
*/
|
|
93
|
+
get description() {
|
|
94
|
+
const modal = this.modal;
|
|
95
|
+
if (!modal) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
return modal.description;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the current modal size, calculated as a percentage of the viewport.
|
|
103
|
+
* Valid values are small, medium, and large. The default is medium.
|
|
104
|
+
* @type {string}
|
|
105
|
+
* @default medium
|
|
106
|
+
*/
|
|
107
|
+
get size() {
|
|
108
|
+
const sizeDefault = 'medium';
|
|
109
|
+
const modal = this.modal;
|
|
110
|
+
if (!modal) {
|
|
111
|
+
return sizeDefault;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// get the size value from <lightning-modal>
|
|
115
|
+
return normalizeString(modal.size, {
|
|
116
|
+
fallbackValue: sizeDefault,
|
|
117
|
+
validValues: ['small', 'medium', 'large'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* <lightning-modal> disableClose value
|
|
123
|
+
* Get attribute to trigger disabling ability to dismiss modal temporarily
|
|
124
|
+
*/
|
|
125
|
+
get disableClose() {
|
|
126
|
+
const modal = this.modal;
|
|
127
|
+
if (!modal) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return modal.disableClose;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Toggle on and off disable close button feature
|
|
135
|
+
* typically used very briefly when devs want to save form data to backend
|
|
136
|
+
* and do not want the form to be closed before the save has
|
|
137
|
+
* completed successfully
|
|
138
|
+
* toggleDisableCloseButton sets local state to
|
|
139
|
+
* (a) toggle display an aria-live message
|
|
140
|
+
* (b) toggle set disabled on the <lightning-button-icon>
|
|
141
|
+
* (c) toggle set aria-busy value on
|
|
142
|
+
* elsewhere in modalBase and modal, ESC key is also disabled, and
|
|
143
|
+
* calls to this.close() are prevented
|
|
144
|
+
*/
|
|
145
|
+
toggleDisableCloseButton() {
|
|
146
|
+
// this.disableCloseButton is local modalBase state
|
|
147
|
+
// this.disableClose is modal.js @api state
|
|
148
|
+
// we track both in order to handle transition correctly
|
|
149
|
+
const isSwitchingToDisabled =
|
|
150
|
+
!this.disableCloseButton && this.disableClose;
|
|
151
|
+
/* Future enhancement possibility - trigger setInterval to remove and
|
|
152
|
+
again add back 'Processing' text, as this will indicate to the screen
|
|
153
|
+
reader user that the interface continues to be busy
|
|
154
|
+
*/
|
|
155
|
+
const disableCloseButtonMessage = isSwitchingToDisabled
|
|
156
|
+
? this.disableCloseBtnMessage
|
|
157
|
+
: '';
|
|
158
|
+
if (isSwitchingToDisabled) {
|
|
159
|
+
// Should disable close button
|
|
160
|
+
this.ariaLiveMessage = disableCloseButtonMessage;
|
|
161
|
+
this.showAriaLiveMessage = true;
|
|
162
|
+
synchronizeAttrs(this.modalWrapper, { [`${ARIA.BUSY}`]: true });
|
|
163
|
+
synchronizeAttrs(this.modalCloseButton, { disabled: 'disabled' });
|
|
164
|
+
this.disableCloseButton = true;
|
|
165
|
+
} else {
|
|
166
|
+
// Should enable close button
|
|
167
|
+
this.ariaLiveMessage = disableCloseBtnMessage;
|
|
168
|
+
this.showAriaLiveMessage = false;
|
|
169
|
+
synchronizeAttrs(this.modalWrapper, { [`${ARIA.BUSY}`]: null });
|
|
170
|
+
synchronizeAttrs(this.modalCloseButton, { disabled: null });
|
|
171
|
+
this.disableCloseButton = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Public method to get the modal slot element
|
|
177
|
+
* @type {(HTMLElement|null)} The modal slot, currently a div elem
|
|
178
|
+
*/
|
|
179
|
+
@api
|
|
180
|
+
get defaultSlot() {
|
|
181
|
+
return this.template.querySelector('[data-slot]');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Private method to get the modal section element, the outer wrapper for modal elements
|
|
186
|
+
* @returns {(HTMLElement|null)} The section element, currently the section[role="dialog"]
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
get modalWrapper() {
|
|
190
|
+
return this.template.querySelector('[data-modal]');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Private method to get the lightning-modal element inside the div slot
|
|
195
|
+
* This element has the api for .close, size, label
|
|
196
|
+
* @returns {(HTMLElement|null)} The modal inside <div data-slot>: <lightning-modal>
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
get modal() {
|
|
200
|
+
return (this.defaultSlot && this.defaultSlot.childNodes[0]) || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Private method to get the modal span description element
|
|
205
|
+
* This span is present when the accessible 'description' api text is present,
|
|
206
|
+
* and aria-description isn't supported. Instead, aria-describedby is utilized
|
|
207
|
+
* @returns {(HTMLElement|null)} The modal span element for IDRef linkage
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
get modalDescSpan() {
|
|
211
|
+
return this.template.querySelector('[data-aria-description]');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get the lightning-button-icon (close button element)
|
|
216
|
+
* @returns {(HTMLElement|null)}
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
get modalCloseButton() {
|
|
220
|
+
return this.template.querySelector('[data-close-button]');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get the lightning-modal element within the modal slot
|
|
225
|
+
* currently modalBase's slot is a div element, not a slot element
|
|
226
|
+
* @returns {(HTMLElement|null)}
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
get modalElement() {
|
|
230
|
+
const modalSlot = this.defaultSlot;
|
|
231
|
+
if (!modalSlot) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return modalSlot.querySelector('lightning-modal');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the lightning-modal element's backdrop
|
|
239
|
+
* @returns {(HTMLElement|null)}
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
get modalBackdrop() {
|
|
243
|
+
return this.template.querySelector('[data-backdrop]');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get the background element height
|
|
248
|
+
* used to calculate max height on the modalBody using
|
|
249
|
+
* modal -> modalBody callback
|
|
250
|
+
* @returns {number}
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
get backdropHeight() {
|
|
254
|
+
const backdropElem = this.modalBackdrop;
|
|
255
|
+
const backdropRect = backdropElem
|
|
256
|
+
? backdropElem.getBoundingClientRect()
|
|
257
|
+
: {};
|
|
258
|
+
const { height } = backdropRect;
|
|
259
|
+
return height || 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Gets the CSS classes applicable to the outer modal wrapper element
|
|
264
|
+
* Modal foreground triggers on this.isModalTransitioningIn to
|
|
265
|
+
* fade in/out or animate up/down
|
|
266
|
+
* @returns {string} CSS class string
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
get modalCssClasses() {
|
|
270
|
+
let classes = classSet('slds-modal fix-slds-modal');
|
|
271
|
+
const sizeClass = this.size;
|
|
272
|
+
if (hasAnimation()) {
|
|
273
|
+
// .slds-fade-in-open not present to trigger opacity animation
|
|
274
|
+
// when later this.isModalTransitioningIn is set to TRUE
|
|
275
|
+
// animation then occurs
|
|
276
|
+
classes.add({
|
|
277
|
+
'slds-fade-in-open': this.isModalTransitioningIn,
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
// no animation occurs if the .slds-fade-in-open class
|
|
281
|
+
// is immediately present in DOM
|
|
282
|
+
classes.add({
|
|
283
|
+
'slds-fade-in-open': true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// don't add animation related css classes in this group
|
|
287
|
+
classes.add({
|
|
288
|
+
'slds-modal_medium': sizeClass === 'medium',
|
|
289
|
+
'slds-modal_large': sizeClass === 'large',
|
|
290
|
+
'slds-modal_small': sizeClass === 'small',
|
|
291
|
+
});
|
|
292
|
+
return classes.toString();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Gets the CSS classes applicable to the modal background element
|
|
297
|
+
* Backdrop triggers on this.isModalOpen to fade/animate in first
|
|
298
|
+
* @returns {string} CSS class string
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
get modalBackdropCssClasses() {
|
|
302
|
+
let classes = classSet('slds-backdrop fix-slds-backdrop');
|
|
303
|
+
if (hasAnimation()) {
|
|
304
|
+
classes.add({
|
|
305
|
+
'slds-backdrop_open': this.isModalOpen,
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
// no fading animation occurs when .slds-backdrop_open
|
|
309
|
+
// is immediately present in the DOM
|
|
310
|
+
classes = classes.add({
|
|
311
|
+
'slds-backdrop_open': true,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return classes.toString();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Gets the CSS classes for the modal close button
|
|
319
|
+
* @returns {string} CSS class string
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
get modalCloseButtonCssClasses() {
|
|
323
|
+
let classes = classSet('slds-modal__close');
|
|
324
|
+
classes.add({
|
|
325
|
+
'fix-slds-modal-close-disabled': this.disableCloseButton,
|
|
326
|
+
});
|
|
327
|
+
return classes;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Saves the current active focused element on modal creation
|
|
332
|
+
* in order to be able to set focus back to that previous element
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
saveActiveElement() {
|
|
336
|
+
this.savedActiveElement = getElementWithFocus();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Renders most background elements inert, while modal active
|
|
341
|
+
* and saves them for later setting background elements active
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
renderBackgroundInert() {
|
|
345
|
+
this.savedInertElements = makeEverythingExceptElementInert(
|
|
346
|
+
this.template.host
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Renders most background elements active, as modal closes
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
renderBackgroundActive() {
|
|
355
|
+
restoreInertness(this.savedInertElements);
|
|
356
|
+
this.savedInertElements = [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Returns focus to background element previously focused,
|
|
361
|
+
* before modal existed
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
returnFocusToBackground() {
|
|
365
|
+
const { savedActiveElement } = this;
|
|
366
|
+
const isSavedElemInDOM = document.body.contains(savedActiveElement);
|
|
367
|
+
if (savedActiveElement && isSavedElemInDOM) {
|
|
368
|
+
savedActiveElement.focus();
|
|
369
|
+
} else {
|
|
370
|
+
// eslint-disable-next-line no-console
|
|
371
|
+
console.warn('Modal :: Nothing to return focus to');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Queue the showing of the modal
|
|
377
|
+
* utilized for triggering fade in modal CSS class additions
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
queueShowModal() {
|
|
381
|
+
if (this.isModalOpen && !this.isModalTransitioningIn) {
|
|
382
|
+
this.isModalTransitioningIn = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Opening the modal involves first performing necessary steps to
|
|
388
|
+
* prepare for when the modal closes
|
|
389
|
+
* isModalOpen triggers fade in CSS class on modal background
|
|
390
|
+
* @private
|
|
391
|
+
*/
|
|
392
|
+
openModal() {
|
|
393
|
+
this.saveActiveElement();
|
|
394
|
+
this.renderBackgroundInert();
|
|
395
|
+
if (!this.isModalOpen) {
|
|
396
|
+
this.isModalOpen = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Closing the modal wraps up the modal lifecycle
|
|
402
|
+
* before it is fully removed
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
closeModal() {
|
|
406
|
+
this.returnFocusToBackground();
|
|
407
|
+
this.renderBackgroundActive(this.savedInertElements);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Unsets the aria-labelledby or aria-label values
|
|
412
|
+
* when no label value is provided
|
|
413
|
+
* modal requires 'label' value either at modalHeader or at modal
|
|
414
|
+
* @private
|
|
415
|
+
*/
|
|
416
|
+
unsetAriaLabelAndError = () => {
|
|
417
|
+
// unset any previously set aria values
|
|
418
|
+
synchronizeAttrs(this.modalWrapper, {
|
|
419
|
+
[ARIA.LABELLEDBY]: null,
|
|
420
|
+
[ARIA.LABEL]: null,
|
|
421
|
+
});
|
|
422
|
+
// console.error when label empty
|
|
423
|
+
this.errorLabelRequired();
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
get isDescriptionSet() {
|
|
427
|
+
const { description } = this;
|
|
428
|
+
// check for being set, as well as not just a description with spaces
|
|
429
|
+
// avoiding setting aria-describedby on section pointing to
|
|
430
|
+
// an empty SPAN element
|
|
431
|
+
return description && description.trim().length > 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Determines if aria-describedby should be set, and the span tag shown
|
|
436
|
+
* occurs only when aria-description is not supported.
|
|
437
|
+
* ex: when description api is set to '' or ' ',
|
|
438
|
+
* we don't want to show the span or set aria-describedby
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
get showAriaDescribedBy() {
|
|
442
|
+
return !isAriaDescriptionSupported() && this.isDescriptionSet;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Set either 'aria-describedby' or 'aria-description' value for accessibility
|
|
447
|
+
* based on the presence of 'description' api value
|
|
448
|
+
* and support of the newer ARIA 'aria-description'.
|
|
449
|
+
* At launch, modern browsers Firefox and Safari DO NOT support it. IE11 won't.
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
updateAriaDescription() {
|
|
453
|
+
const { description } = this;
|
|
454
|
+
// if aria-description is supported && description set, set aria-description
|
|
455
|
+
if (isAriaDescriptionSupported()) {
|
|
456
|
+
const descriptionToSet = this.isDescriptionSet ? description : null;
|
|
457
|
+
// set aria-description if set, otherwise unset with null
|
|
458
|
+
return synchronizeAttrs(this.modalWrapper, {
|
|
459
|
+
[ARIA.DESCRIPTION]: descriptionToSet,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// if aria-description not supported, and description is set,
|
|
463
|
+
// and span id exists, or unset aria-describedby with null
|
|
464
|
+
const spanId = (this.isDescriptionSet && this.modalDescSpan.id) || null;
|
|
465
|
+
return synchronizeAttrs(this.modalWrapper, {
|
|
466
|
+
[ARIA.DESCRIBEDBY]: spanId,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Sets the aria-labelledby or aria-label values for accessibility
|
|
472
|
+
* based on presence of modalHeader child
|
|
473
|
+
* modal requires 'label' value either at modalHeader or at modal
|
|
474
|
+
* @private
|
|
475
|
+
*/
|
|
476
|
+
updateAriaLabel() {
|
|
477
|
+
const {
|
|
478
|
+
label,
|
|
479
|
+
headerRegistered,
|
|
480
|
+
headerLabelId,
|
|
481
|
+
headerLabelIsPopulated,
|
|
482
|
+
} = this;
|
|
483
|
+
const labelIsEmpty = label === '' || label.trim().length === 0;
|
|
484
|
+
// when header is present,
|
|
485
|
+
// headerLabelIsPopulated is equivalent labelIsEmpty, but from modalHeader level
|
|
486
|
+
if (headerRegistered) {
|
|
487
|
+
if (headerLabelId && headerLabelIsPopulated) {
|
|
488
|
+
synchronizeAttrs(this.modalWrapper, {
|
|
489
|
+
[ARIA.LABELLEDBY]: headerLabelId,
|
|
490
|
+
[ARIA.LABEL]: null,
|
|
491
|
+
});
|
|
492
|
+
// if labelId not set OR header label value not set,
|
|
493
|
+
// must console.error
|
|
494
|
+
} else {
|
|
495
|
+
this.unsetAriaLabelAndError();
|
|
496
|
+
}
|
|
497
|
+
// unset if no header (gets removed dynamically, or never present)
|
|
498
|
+
} else {
|
|
499
|
+
// fallback to headless variant
|
|
500
|
+
// check label is actually set,
|
|
501
|
+
// and use aria-label instead of aria-labelledby
|
|
502
|
+
if (!labelIsEmpty) {
|
|
503
|
+
synchronizeAttrs(this.modalWrapper, {
|
|
504
|
+
[ARIA.LABELLEDBY]: null,
|
|
505
|
+
[ARIA.LABEL]: label,
|
|
506
|
+
});
|
|
507
|
+
} else {
|
|
508
|
+
// in headless variant, must have label value set
|
|
509
|
+
this.unsetAriaLabelAndError();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Construct and show console.error for missing label value
|
|
516
|
+
* Modal component requires the label attribute, either via
|
|
517
|
+
* modalHeader or modal to have accessibility set correctly
|
|
518
|
+
* @private
|
|
519
|
+
*/
|
|
520
|
+
errorLabelRequired() {
|
|
521
|
+
let errorMsg =
|
|
522
|
+
'LightningModal - Templates with <lightning-modal-header> should define the label attribute as an attribute on <lightning-modal-header label="Modal Heading"> .';
|
|
523
|
+
errorMsg +=
|
|
524
|
+
' Templates without <lightning-modal-header> should define the label attribute in the Modal.open({ label: "Modal Heading" })';
|
|
525
|
+
console.error(errorMsg);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Handle the close button click, or via ESC key
|
|
530
|
+
* @private
|
|
531
|
+
*/
|
|
532
|
+
handleCloseClick() {
|
|
533
|
+
// calls handlePrivateClose
|
|
534
|
+
if (!this.disableCloseButton) {
|
|
535
|
+
this.modal.close();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Handle privateclose event firing is prevented from occurring
|
|
540
|
+
// when this.disableClose set true in modal.js
|
|
541
|
+
handlePrivateClose(e) {
|
|
542
|
+
if (!(e.detail && e.detail[secure])) {
|
|
543
|
+
console.error('Invalid access to privateclose event');
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (hasAnimation()) {
|
|
547
|
+
/// triggers the disappearance of the foreground modal elements
|
|
548
|
+
this.modalWrapper.classList.remove('slds-fade-in-open');
|
|
549
|
+
this.modalBackdrop.classList.remove('slds-backdrop_open');
|
|
550
|
+
// wait until modalWrappers animation completes, then proceed
|
|
551
|
+
this.modalBackdrop.addEventListener('transitionend', () => {
|
|
552
|
+
e.detail.resolve();
|
|
553
|
+
});
|
|
554
|
+
} else {
|
|
555
|
+
// skip animation, resolve immediately
|
|
556
|
+
e.detail.resolve();
|
|
557
|
+
}
|
|
558
|
+
this.closeModal();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
handlePrivateDisableCloseButton(e) {
|
|
562
|
+
if (!(e.detail && e.detail[secure])) {
|
|
563
|
+
console.error('Invalid access to privatedisableclose event');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
this.toggleDisableCloseButton();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Handle Esc key down events on the modal
|
|
571
|
+
* @param {Event} e The keyboard event
|
|
572
|
+
* @private
|
|
573
|
+
*/
|
|
574
|
+
handleModalKeyDown(e) {
|
|
575
|
+
const { ctrlKey, metaKey, shiftKey, key } = e;
|
|
576
|
+
const hasModifier = ctrlKey || metaKey || shiftKey;
|
|
577
|
+
// 'Esc' is IE11 specific, remove when support is dropped
|
|
578
|
+
// when disableCloseButton set true, ESC key to close modal is deactivated
|
|
579
|
+
if (
|
|
580
|
+
!hasModifier &&
|
|
581
|
+
!this.disableCloseButton &&
|
|
582
|
+
(key === 'Esc' || key === 'Escape')
|
|
583
|
+
) {
|
|
584
|
+
e.stopPropagation();
|
|
585
|
+
e.preventDefault();
|
|
586
|
+
this.handleCloseClick();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Utilized to auto set (autofocus) the browser's focus to
|
|
592
|
+
* the first interactive element within the modal children
|
|
593
|
+
* Fires custom cancelable event 'autofocus'
|
|
594
|
+
* autoFocus should only be done during the modal's creation
|
|
595
|
+
* Needs to wait until all elements have rendered in the DOM
|
|
596
|
+
* Details on autofocus decision tree under 'Opening Dialogs' section:
|
|
597
|
+
* https://www.lightningdesignsystem.com/accessibility/guidelines/global-focus/#dialogs
|
|
598
|
+
* For modal v1, ignore multi-step modal, as not part of scope
|
|
599
|
+
* @private
|
|
600
|
+
*/
|
|
601
|
+
focusFirstElement() {
|
|
602
|
+
const { autoFocusCompletedOnce, modalElement } = this;
|
|
603
|
+
// If any of these is TRUE, exit before proceeding
|
|
604
|
+
// 1. if modal has already been autofocused once, exit immediately
|
|
605
|
+
// 2. need to wait for modalElement to be rendered to the DOM
|
|
606
|
+
if (autoFocusCompletedOnce || !modalElement) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const {
|
|
611
|
+
bodyRegistered,
|
|
612
|
+
footerRegistered,
|
|
613
|
+
headerRegistered,
|
|
614
|
+
headerTitleRef,
|
|
615
|
+
headerTabElemRef,
|
|
616
|
+
headerDefaultSlotIsPopulated,
|
|
617
|
+
headerSlotHasRendered,
|
|
618
|
+
bodyDefaultSlotIsPopulated,
|
|
619
|
+
bodySlotHasRendered,
|
|
620
|
+
bodyTabElemRef,
|
|
621
|
+
footerDefaultSlotIsPopulated,
|
|
622
|
+
footerSlotHasRendered,
|
|
623
|
+
footerTabElemRef,
|
|
624
|
+
} = this;
|
|
625
|
+
|
|
626
|
+
const waitForHeaderSlotRender =
|
|
627
|
+
headerRegistered &&
|
|
628
|
+
headerDefaultSlotIsPopulated &&
|
|
629
|
+
!headerSlotHasRendered;
|
|
630
|
+
const waitForBodySlotRender =
|
|
631
|
+
bodyRegistered &&
|
|
632
|
+
bodyDefaultSlotIsPopulated &&
|
|
633
|
+
!bodySlotHasRendered;
|
|
634
|
+
const waitForFooterSlotRender =
|
|
635
|
+
footerRegistered &&
|
|
636
|
+
footerDefaultSlotIsPopulated &&
|
|
637
|
+
!footerSlotHasRendered;
|
|
638
|
+
|
|
639
|
+
// 3. Need to make sure registered child components
|
|
640
|
+
// slots have fully rendered before proceeding
|
|
641
|
+
if (
|
|
642
|
+
waitForHeaderSlotRender ||
|
|
643
|
+
waitForBodySlotRender ||
|
|
644
|
+
waitForFooterSlotRender
|
|
645
|
+
) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// preferred autofocus-able elements (links:NOT(tooltip), inputs, buttons)
|
|
650
|
+
// located in order of preference:
|
|
651
|
+
// (1) lightning-modal-header,
|
|
652
|
+
// (2) lightning-modal-body,
|
|
653
|
+
// (3)) lightning-modal-footer
|
|
654
|
+
const preferredFocusElem =
|
|
655
|
+
headerTabElemRef || bodyTabElemRef || footerTabElemRef || null;
|
|
656
|
+
|
|
657
|
+
// fallback autofocus elements, in order of preference
|
|
658
|
+
// (1) modal heading (headless modal, not available),
|
|
659
|
+
// (2) close button (always present, current version)
|
|
660
|
+
// (3) outer modal element (always present, here ONLY as a backup,
|
|
661
|
+
// for when hide or disable close button becomes available
|
|
662
|
+
const modalHeadingElem =
|
|
663
|
+
headerRegistered && headerTitleRef ? headerTitleRef : null;
|
|
664
|
+
const closeButtonElem = this.modalCloseButton;
|
|
665
|
+
const outerModalElem = this.modalWrapper;
|
|
666
|
+
|
|
667
|
+
const fallbackFocusElem =
|
|
668
|
+
modalHeadingElem || closeButtonElem || outerModalElem;
|
|
669
|
+
|
|
670
|
+
const focusElem = preferredFocusElem
|
|
671
|
+
? preferredFocusElem
|
|
672
|
+
: fallbackFocusElem;
|
|
673
|
+
|
|
674
|
+
if (focusElem !== null) {
|
|
675
|
+
focusElem.focus();
|
|
676
|
+
this.autoFocusCompletedOnce = true;
|
|
677
|
+
} else {
|
|
678
|
+
// Should never happen since outerModalElem always present
|
|
679
|
+
console.error(
|
|
680
|
+
'LightningModal - at least one focusable element is required, none found.'
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* When child component modalBody is removed,
|
|
687
|
+
* sets private tracked state, detaches window.onresize event listeners
|
|
688
|
+
* @private
|
|
689
|
+
*/
|
|
690
|
+
unregisterBody() {
|
|
691
|
+
// FUTURE TODO mechanism to support aria-describedby
|
|
692
|
+
// aria-describedby is optional, without a good reproducible pattern
|
|
693
|
+
this.initBodyState();
|
|
694
|
+
this.detachBodyResizeEvents();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Registers modalBody with its parent modal, when present
|
|
699
|
+
* Sets private tracked state about modalBody
|
|
700
|
+
* Called when 'onprivatemodalbodyregister' event gets fired
|
|
701
|
+
* @param {Object} detail Passed details object from modalBody
|
|
702
|
+
* @private
|
|
703
|
+
*/
|
|
704
|
+
registerBody({
|
|
705
|
+
bodyId,
|
|
706
|
+
bodyIsPopulated,
|
|
707
|
+
updateBodyCallback,
|
|
708
|
+
defaultSlotHasRendered,
|
|
709
|
+
unRegisterCallback,
|
|
710
|
+
firstTabbableElemRef,
|
|
711
|
+
}) {
|
|
712
|
+
this.bodyRegistered = true;
|
|
713
|
+
this.bodyDefaultSlotIsPopulated = bodyIsPopulated;
|
|
714
|
+
this.bodySlotHasRendered = defaultSlotHasRendered;
|
|
715
|
+
this.bodyId = bodyId;
|
|
716
|
+
this.baseUpdateBodyCallback = updateBodyCallback;
|
|
717
|
+
this.bodyTabElemRef = firstTabbableElemRef || null;
|
|
718
|
+
unRegisterCallback(() => {
|
|
719
|
+
this.unregisterBody();
|
|
720
|
+
});
|
|
721
|
+
// cover case if modalBody is removed, then added back
|
|
722
|
+
// required to correctly set the CSS classes on modalBody
|
|
723
|
+
this.updateBodyHeight();
|
|
724
|
+
|
|
725
|
+
// ModalBody can register 2+ times when initially rendering
|
|
726
|
+
if (!this.windowResizeEventsBound) {
|
|
727
|
+
this.bindBodyResizeEvents();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* When modalBody present, update private tracked state
|
|
733
|
+
* @param {Event} e The private custom registration event of modalBody
|
|
734
|
+
* @private
|
|
735
|
+
*/
|
|
736
|
+
handleBodyRegister(e) {
|
|
737
|
+
const { detail } = e;
|
|
738
|
+
this.registerBody(detail);
|
|
739
|
+
e.stopPropagation();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* When modalBody removed or at startup, initialize private tracked modalBody state
|
|
744
|
+
* @private
|
|
745
|
+
*/
|
|
746
|
+
initBodyState() {
|
|
747
|
+
this.bodyRegistered = false;
|
|
748
|
+
this.bodyDefaultSlotIsPopulated = false;
|
|
749
|
+
this.bodySlotHasRendered = false;
|
|
750
|
+
this.bodyId = null;
|
|
751
|
+
this.baseUpdateBodyCallback = null;
|
|
752
|
+
this.bodyResizeScheduled = false;
|
|
753
|
+
this.bodyTabElemRef = null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Sets private tracked state related to modalHeader, when removed
|
|
758
|
+
* @private
|
|
759
|
+
*/
|
|
760
|
+
unregisterHeader() {
|
|
761
|
+
this.initHeaderState();
|
|
762
|
+
this.updateAriaLabel();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Registers modalHeader with its parent modal, when present
|
|
767
|
+
* Sets private tracked state about modalHeader
|
|
768
|
+
* Called when 'onprivatemodalheaderregister' event gets fired
|
|
769
|
+
* @param {Object} detail Passed details object from modalHeader
|
|
770
|
+
* @private
|
|
771
|
+
*/
|
|
772
|
+
registerHeader({
|
|
773
|
+
defaultSlotIsPopulated,
|
|
774
|
+
firstTabbableElemRef,
|
|
775
|
+
defaultSlotWrapperId,
|
|
776
|
+
defaultSlotHasRendered,
|
|
777
|
+
unRegisterCallback,
|
|
778
|
+
labelIsPopulated,
|
|
779
|
+
headerHeight,
|
|
780
|
+
headerRef,
|
|
781
|
+
labelId,
|
|
782
|
+
}) {
|
|
783
|
+
this.headerRegistered = true;
|
|
784
|
+
this.headerHeight = headerHeight || 0;
|
|
785
|
+
this.headerDefaultSlotIsPopulated = defaultSlotIsPopulated;
|
|
786
|
+
this.headerSlotHasRendered = defaultSlotHasRendered;
|
|
787
|
+
this.headerSlotWrapperId = defaultSlotWrapperId;
|
|
788
|
+
this.headerLabelId = labelId;
|
|
789
|
+
this.headerLabelIsPopulated = labelIsPopulated;
|
|
790
|
+
this.headerTitleRef = headerRef;
|
|
791
|
+
this.headerTabElemRef = firstTabbableElemRef;
|
|
792
|
+
unRegisterCallback(() => {
|
|
793
|
+
this.unregisterHeader();
|
|
794
|
+
});
|
|
795
|
+
// update modalBody max-height values
|
|
796
|
+
if (this.bodyRegistered) {
|
|
797
|
+
this.updateBodyHeight();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Event handler for private modalHeader registration
|
|
803
|
+
* When modalHeader present, update private tracked state
|
|
804
|
+
* @param {Event} e Private custom registration event fired
|
|
805
|
+
* @private
|
|
806
|
+
*/
|
|
807
|
+
handleHeaderRegister(e) {
|
|
808
|
+
const { detail } = e;
|
|
809
|
+
this.registerHeader(detail);
|
|
810
|
+
this.updateAriaLabel();
|
|
811
|
+
e.stopPropagation();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* When modalHeader removed or at startup, initialize
|
|
816
|
+
* private tracked modalHeader state
|
|
817
|
+
* @private
|
|
818
|
+
*/
|
|
819
|
+
initHeaderState() {
|
|
820
|
+
this.headerRegistered = false;
|
|
821
|
+
this.headerHeight = 0;
|
|
822
|
+
this.headerDefaultSlotIsPopulated = false;
|
|
823
|
+
this.headerSlotWrapperId = null;
|
|
824
|
+
this.headerSlotHasRendered = false;
|
|
825
|
+
this.headerLabelId = null;
|
|
826
|
+
this.headerLabelIsPopulated = null;
|
|
827
|
+
this.headerTitleRef = null;
|
|
828
|
+
this.headerTabElemRef = null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Sets private tracked state related to modalFooter, when removed
|
|
833
|
+
* @private
|
|
834
|
+
*/
|
|
835
|
+
unregisterFooter() {
|
|
836
|
+
this.initFooterState();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Registers modalFooter with its parent modal, when present
|
|
841
|
+
* Sets private tracked state about modalFooter
|
|
842
|
+
* Called when 'onprivatemodalfooterregister' event gets fired
|
|
843
|
+
* @param {Object} detail Passed details object from modalFooter
|
|
844
|
+
* @private
|
|
845
|
+
*/
|
|
846
|
+
registerFooter({
|
|
847
|
+
defaultSlotIsPopulated,
|
|
848
|
+
defaultSlotHasRendered,
|
|
849
|
+
footerHeight,
|
|
850
|
+
unRegisterCallback,
|
|
851
|
+
firstTabbableElemRef,
|
|
852
|
+
}) {
|
|
853
|
+
this.footerRegistered = true;
|
|
854
|
+
this.footerDefaultSlotIsPopulated = defaultSlotIsPopulated;
|
|
855
|
+
this.footerSlotHasRendered = defaultSlotHasRendered;
|
|
856
|
+
this.footerHeight = footerHeight || 0;
|
|
857
|
+
this.footerTabElemRef = firstTabbableElemRef || null;
|
|
858
|
+
unRegisterCallback(() => {
|
|
859
|
+
this.unregisterFooter();
|
|
860
|
+
});
|
|
861
|
+
// update modalBody max-height values
|
|
862
|
+
if (this.bodyRegistered) {
|
|
863
|
+
this.updateBodyHeight();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Event handler for private modalFooter registration
|
|
869
|
+
* When modalFooter present, update private tracked state
|
|
870
|
+
* @param {Event} e Private custom registration event fired
|
|
871
|
+
* @private
|
|
872
|
+
*/
|
|
873
|
+
handleFooterRegister(e) {
|
|
874
|
+
const { detail } = e;
|
|
875
|
+
this.registerFooter(detail);
|
|
876
|
+
e.stopPropagation();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* When modalFooter removed or at startup, initialize
|
|
881
|
+
* private tracked modalFooter state
|
|
882
|
+
* @private
|
|
883
|
+
*/
|
|
884
|
+
initFooterState() {
|
|
885
|
+
this.footerRegistered = false;
|
|
886
|
+
this.footerHeight = 0;
|
|
887
|
+
this.footerSlotHasRendered = false;
|
|
888
|
+
this.footerDefaultSlotIsPopulated = false;
|
|
889
|
+
this.footerTabElemRef = null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* When window is resizing, need to debounce callback
|
|
894
|
+
* Track internal variable _resizing
|
|
895
|
+
* @returns {Boolean}
|
|
896
|
+
* @private
|
|
897
|
+
*/
|
|
898
|
+
get modalResizing() {
|
|
899
|
+
if (!this._resizing) {
|
|
900
|
+
this._resizing = this.scheduleResize.bind(this);
|
|
901
|
+
}
|
|
902
|
+
return this._resizing;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* When the modalBody content height is tall, it requires max-height
|
|
907
|
+
* to be set in order to prevent overflow of the modal offscreen
|
|
908
|
+
* Throttling occurs to prevent calling this method every time
|
|
909
|
+
* the window.onresize event fires
|
|
910
|
+
* @private
|
|
911
|
+
*/
|
|
912
|
+
updateBodyHeight() {
|
|
913
|
+
clearTimeout(this.timeoutId);
|
|
914
|
+
this.timeoutId = 0;
|
|
915
|
+
const { bodyResizeScheduled, bodyRegistered, baseUpdateBodyCallback } =
|
|
916
|
+
this;
|
|
917
|
+
|
|
918
|
+
if (bodyRegistered && !bodyResizeScheduled) {
|
|
919
|
+
// eslint-disable-next-line @lwc/lwc/no-async-operation
|
|
920
|
+
requestAnimationFrame(() => {
|
|
921
|
+
this.bodyResizeScheduled = false;
|
|
922
|
+
if (bodyRegistered && baseUpdateBodyCallback) {
|
|
923
|
+
const values = {
|
|
924
|
+
footerHeight: this.footerHeight || 0,
|
|
925
|
+
headerHeight: this.headerHeight || 0,
|
|
926
|
+
backdropHeight: this.backdropHeight,
|
|
927
|
+
};
|
|
928
|
+
baseUpdateBodyCallback(values);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
this.bodyResizeScheduled = true;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Provide debounce / throttling to prevent modalBody callback
|
|
937
|
+
* from being fired every time window.onresize event fires
|
|
938
|
+
* @private
|
|
939
|
+
*/
|
|
940
|
+
scheduleResize() {
|
|
941
|
+
if (this.timeoutId === 0) {
|
|
942
|
+
// eslint-disable-next-line @lwc/lwc/no-async-operation
|
|
943
|
+
this.timeoutId = setTimeout(() => {
|
|
944
|
+
this.updateBodyHeight();
|
|
945
|
+
}, DEBOUNCE_RESIZE);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Event handler for window.onresize event
|
|
951
|
+
* @private
|
|
952
|
+
*/
|
|
953
|
+
handleWindowResize = () => {
|
|
954
|
+
this.scheduleResize();
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Bind window.onresize event listener when modalBody is present
|
|
959
|
+
* @private
|
|
960
|
+
*/
|
|
961
|
+
bindBodyResizeEvents() {
|
|
962
|
+
if (window && !this.windowResizeEventsBound) {
|
|
963
|
+
window.addEventListener('resize', this.handleWindowResize);
|
|
964
|
+
this.windowResizeEventsBound = true;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Detach window.onresize event listener when modalBody is removed
|
|
970
|
+
* @private
|
|
971
|
+
*/
|
|
972
|
+
detachBodyResizeEvents() {
|
|
973
|
+
if (window) {
|
|
974
|
+
window.removeEventListener('resize', this.handleWindowResize);
|
|
975
|
+
clearTimeout(this.timeoutId);
|
|
976
|
+
this.timeoutId = 0;
|
|
977
|
+
this.windowResizeEventsBound = false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* When modal is being created, initialize
|
|
983
|
+
* private tracked modal state
|
|
984
|
+
* @private
|
|
985
|
+
*/
|
|
986
|
+
initState() {
|
|
987
|
+
// setting initialRender true handles the case where modal
|
|
988
|
+
// is added / removed/added back to DOM
|
|
989
|
+
this.initialRender = true;
|
|
990
|
+
this.autoFocusCompletedOnce = false;
|
|
991
|
+
this.windowResizeEventsBound = false;
|
|
992
|
+
this.timeoutId = 0;
|
|
993
|
+
this.disableCloseButton = false;
|
|
994
|
+
this.modalLabel = null;
|
|
995
|
+
this.modalLabelledBy = null;
|
|
996
|
+
this.modalDescribedBy = null;
|
|
997
|
+
this.showAriaLiveMessage = false;
|
|
998
|
+
this.ariaLiveMessage = '';
|
|
999
|
+
this._size = 'medium';
|
|
1000
|
+
this.savedInertElements = [];
|
|
1001
|
+
this.savedActiveElement = null;
|
|
1002
|
+
this.isModalOpen = false;
|
|
1003
|
+
this.isModalTransitioningIn = false;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
connectedCallback() {
|
|
1007
|
+
this.initState();
|
|
1008
|
+
this.initHeaderState();
|
|
1009
|
+
this.initBodyState();
|
|
1010
|
+
this.initFooterState();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
disconnectedCallback() {
|
|
1014
|
+
if (this.windowResizeEventsBound) {
|
|
1015
|
+
this.detachBodyResizeEvents();
|
|
1016
|
+
}
|
|
1017
|
+
this.closeModal();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
renderedCallback() {
|
|
1021
|
+
if (this.initialRender) {
|
|
1022
|
+
this.openModal();
|
|
1023
|
+
this.initialRender = false;
|
|
1024
|
+
} else {
|
|
1025
|
+
// wait until lightning-modal is defined
|
|
1026
|
+
this.updateAriaLabel();
|
|
1027
|
+
this.updateAriaDescription();
|
|
1028
|
+
// queue show only once
|
|
1029
|
+
if (!this.isModalTransitioningIn) {
|
|
1030
|
+
this.queueShowModal();
|
|
1031
|
+
}
|
|
1032
|
+
// autoFocus once only
|
|
1033
|
+
if (!this.autoFocusCompletedOnce) {
|
|
1034
|
+
this.focusFirstElement();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
this.updateBodyHeight();
|
|
1038
|
+
}
|
|
1039
|
+
}
|