-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce custom modal confirmation dialogs #9859
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
width: 100%; | ||
top: 0; | ||
left: 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change these styles to:
.confirm-dialogue {
background-color: white;
width: 400px;
padding: 15px;
}
.confirm-dialogue__message {
padding: 0px 0px 5px 0px;
}
.confirm-dialogue__actions {
display: flex;
justify-content: flex-end;
> * + * {
margin-left: 5px;
}
}
.disable-app-layer {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
top: 0;
left: 0;
bottom: 0;
right: 0;
padding-bottom: 10vh;
}
<button class="btn btn-primary" ng-click="onYes()">{{yesButtonText}}</button> | ||
<button class="btn" ng-click="onNo()">{{noButtonText}}</button> | ||
</div> | ||
</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change this markup to:
<div class="disable-app-layer">
<div class="confirm-dialogue">
<div class="confirm-dialogue__message">
{{message}}
</div>
<div class="confirm-dialogue__actions">
<button class="kuiButton kuiButton--primary" ng-click="onYes()">{{yesButtonText}}</button>
<button class="kuiButton kuiButton--basic" ng-click="onNo()">{{noButtonText}}</button>
</div>
</div>
</div>
* @return {Promise<boolean>} Returns a promise that will be resolved to true if the user | ||
* clicks the yes/okay button and rejected if the user clicks the no/cancel button. | ||
*/ | ||
return function showConfirmDialogue(message, yesButtonText = 'Okay', noButtonText = 'Cancel') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove the default for the yesButtonText
. Confirmation actions should always explicitly state what will happen to help clarify the consequences for the user.
It shouldn't be hard to update the rest of Kibana to reflect this change since it looks like it's only used in a couple other places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree on this one. :) I don't want to require callers to pass in text manually on every call site. If we ever wanted to changed that default text, we'd have to do so everywhere.
$timeout.flush(); | ||
expect($window.confirm.calledWith(message)).to.be(true); | ||
const confirmationDialogElement = document.querySelector('.confirm-dialogue'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using classes we should use a data-test-subj="confirmDialogue"
attribute selector. This uncouples our tests from our CSS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, should be confirmationDialogueElement
(sp).
$rootScope.$apply(); | ||
|
||
const confirmationDialogElement = document.querySelector('.confirm-dialogue'); | ||
const okayButton = confirmationDialogElement.getElementsByTagName('button')[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also use data-test-subj selectors to find these buttons. This will uncouple our tests from our elements and from the order in which they appear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea
@@ -29,10 +24,17 @@ describe('ui/safe_confirm', function () { | |||
promise = safeConfirm(message); | |||
}); | |||
|
|||
afterEach(function () { | |||
const confirmationDialogElement = document.querySelector('.confirm-dialogue'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be confirmationDialogueElement
(sp)?
@@ -45,35 +47,45 @@ describe('ui/safe_confirm', function () { | |||
}); | |||
|
|||
context('after timeout completes', function () { | |||
it('$window.confirm is invoked with message', function () { | |||
it('confirmation dialog is loaded to dom with message', function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"dialogue"?
dialogueScope.onYes = () => { | ||
dialogueScope.$destroy(); | ||
modalDialogue.destroy(); | ||
deferred.resolve(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need to return true
and false
when we resolve and reject? I think we can remove them since they don't seem to serve a purpose. This will allow us to simplify the tests a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did that to match the prior functionality since it was tested, and I wanted to keep it how it was before. Otherwise I have to check all the call sites and make sure all usages of safeConfirm aren't relying on true/false. I thought it was silly to, to pass this, just worried about breaking something. If you feel strongly about it though, I can do it.
* function () { | ||
* // user clicked confirm | ||
* // user clicked the okay button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the original comment was better here, because "Okay" is too coupled to the UI, but "confirm" is descriptive of the concept.
* promise is returned so consumers can handle the control flow. | ||
* | ||
* Usage: | ||
* safeConfirm('This message will be passed to window.confirm()').then( | ||
* safeConfirm('This message will be shown in a modal dialog').then( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sp: "dialogue"
@cjcenizal Addressed most of your requested changes, and also tried to implement the new style guide after discussing some stuff with @w33ble and he had some good other suggestions. Please take another look and let me know what you think! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Joe and I talked about the most recent changes to the PR and we came to a couple conclusions:
- Logic is best extracted out of an Angular service into a vanilla JS service when the resulting JS service has no Angular dependencies. This makes the service more reusable, testable, and easier to migrate into a non-Angular codebase.
- Extracting logic into a vanilla JS service creates indirection and adds complexity, which is only a good tradeoff if the above criteria can be met.
- The file
modal_dialogue.js
is more of a "teleport" for adding and removing an element to the body, and its knowledge of scope dilutes this single responsibility.
So, I think we should revert to the most recent commit, but preserve the changes made in response to my comments, as well as preserve the single point of entry in modules.js
.
Let's Zoom about this some more to make sure this makes sense. We had a long discussion and I may not be completely representing it here.
Joe's working on a POC to explore some other ideas he had as we were talking, partially inspired by the Modal demo I showed you (http://smaato.github.io/ui-framework/#/modal).
@@ -29,10 +24,15 @@ describe('ui/safe_confirm', function () { | |||
promise = safeConfirm(message); | |||
}); | |||
|
|||
afterEach(function () { | |||
const okayButton = document.body.find('[data-test-subj=confirm-dialog-okay-button]'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's a bit of a mixed-bag right now, but most of the data-test-subj selectors are camel case and I think we should update the few ones which are snake case to be camel case at some point. So let's make these new ones camel case.
message="{{message}}" | ||
confirm-button-text="{{confirmButtonText}}" | ||
cancel-button-text="{{cancelButtonText}}"> | ||
</confirm-dialogue> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we format this per the HTML style guide:
<confirm-dialogue
on-confirm="onConfirm()"
on-cancel="onCancel()"
message="{{message}}"
confirm-button-text="{{confirmButtonText}}"
cancel-button-text="{{cancelButtonText}}"
></confirm-dialogue>
ng-click="onConfirm()">{{confirmButtonText}}</button> | ||
<button class="kuiButton kuiButton--basic" | ||
data-test-subj="confirm-dialog-cancel-button" | ||
ng-click="onCancel()">{{cancelButtonText}}</button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here:
<button
class="kuiButton kuiButton--primary"
data-test-subj="confirm-dialog-okay-button"
ng-click="onConfirm()"
>
{{confirmButtonText}}
</button>
<button
class="kuiButton kuiButton--basic"
data-test-subj="confirm-dialog-cancel-button"
ng-click="onCancel()"
>
{{cancelButtonText}}
</button>
|
||
module.factory('safeConfirm', function ($timeout, ModalDialogue, Promise) { | ||
return (message, confirmButtonText = 'Okay', cancelButtonText = 'Cancel') => | ||
$timeout(() => safeConfirm(message, confirmButtonText, cancelButtonText, Promise, ModalDialogue)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I need some education here... why are we wrapping it in a $timeout? Is it just to trigger a digest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should have kept the original comment. I'm not 100% sure myself, but sounded necessary. Here is the original comment:
/*
* Angular doesn't play well with thread blocking calls such as
* unless those calls are specifically handled inside a call
* to $timeout(). Rather than litter the code with that implementation
* detail, safeConfirm() can be used.
*
* WARNING: safeConfirm only blocks the thread beginning on the next tick. For that reason, a
* promise is returned so consumers can handle the control flow.
*
* Usage:
* safeConfirm('This message will be shown in a modal dialog').then(
* function () {
* // user clicked the okay button
* },
* function () {
* // user canceled the confirmation
* }
* );
*/
It's possible it's not even necessary since we are using promises and not a thread blocking call.
const module = uiModules.get('kibana'); | ||
|
||
module.factory('safeConfirm', function ($timeout, ModalDialogue, Promise) { | ||
return (message, confirmButtonText = 'Okay', cancelButtonText = 'Cancel') => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear you... but my point is that "Okay" is generally not an acceptable label for this button, from a UX standpoint. The text should always explicitly state the action that's going to be taken, so the user only needs to read the button to know what clicking it does. So by supporting a default, we're encouraging the opposite of what we want from a UX standpoint.
I looked for the cases where safeConfirm is used to see where we'd need to make changes. Here's what I found:
- In the case of Timelion, this should be "Delete the sheet ${sheetTitle}"
- For overwriting Saved Objects, this should be "Overwrite ${objectTitle}"
- For index patterns, it should be "Overwrite"
- For Graph when clearing a workspace, it should be "Clear workspace". When removing a drill-down, it should be "Remove drilldown". When deleting a workspace, it should be "Delete workspace".
I can understand wanting to avoid bloating this PR, so we can do this in a separate step, but I think it's a really important change to make.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I understand your point better now and I'm sorry for reading your original comment too quickly - after re-reading it, you made yourself clear, so that was my bad. I thought you wanted me to propagate 'Okay' and 'Cancel' everywhere.
I'm all for it, but since this PR does match prior functionality, I'd prefer to make the change in a separate PR.
Are the new rules/suggestions using this After the Also @cjcenizal you had said in that email I don't think we've hit a) yet with this new pattern. I like it a lot, but after thinking about that email, I'm not sure I should be adopting it at this point. I guess I don't see the difference between me introducing this new pattern/style and what @weltenwort was trying to do. Both are attempts at creating new patterns in the code base but it looks like @weltenwort was held to the consistency/team adoption standard, while this pattern is being allowed to slip by. Perhaps this is my fault since I am the first person to introduce it to the existing code base, I'm just having second thoughts after that email and wondering if adopting the |
I merged @w33ble's PR and made a few small adjustments like moving safe_confirm into the modals folder. I have a question though - why the change to pass onConfirm and onCancel instead of returning a promise? Also making them default to noop feels a bit weird to me. If the default is to do nothing, there really shouldn't be two buttons. I could see making one optional, but not both. But then if you make one optional, which one? |
240a704
to
7683ed1
Compare
and implement new angular style suggestions from Joe
- Move safe_confirm into modals folder - make data-test-subj camelCase. - get rid of $timeout call, doesn’t seem necessary anymore, now that window.confirm is not being used.
7683ed1
to
e581374
Compare
This is all still very much in flux, it's just a pattern we've been happy with elsewhere. I still would have liked to break out the vanilla code, even just to make testing easier, but I see CJ's point in that the code is useless without Angular. But mocking the Angular stuff isn't that hard either, so I'm a little torn. What do you think?
I agree with what @Bargs has said. I didn't know of anyone else proposing other patterns at the moment though, and still need to dive in to what @weltenwort has been doing. Planning to sync with him this week. Like I said, the management team has been pretty happy with these patterns, but we do need to get larger buy-in from the team, and I think it's important to get feedback from more than just the three of us; that's a big part of why I brought this up with you originally. We're still very much open to other ideas. If you decide not to follow our patterns here, it's really up to you. This is your PR after all.
I though it best to let the click handlers decide what to do when the user clicks the button. This way, the modal is only concerned with executing some handler and closing itself. We may not always want to deal with a promise, and it has the added bonus of one less dependency within If you disagree, feel free to change it up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had some thoughts.
} | ||
promise.then(markAsResolved, markAsResolved); | ||
$rootScope.$apply(); | ||
expect(isResolved).to.be(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use spies here:
import sinon from 'sinon';
const callback = sinon.spy();
promise.then(callback, callback);
$rootScope.$apply();
expect(callback.called).to.be(false);
it('confirmation dialogue is loaded to dom with message', function () { | ||
$rootScope.$digest(); | ||
const confirmationDialogueElement = angular.element(document.body).find('[data-test-subj=confirmModal]'); | ||
expect(!!confirmationDialogueElement).to.be(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not expect(confirmationDialogueElement).to.not.be(undefined);
? It seems clearer to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea, much better.
$rootScope.$digest(); | ||
confirmButton.click(); | ||
|
||
expect(confirmed).to.be(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use a spy here too.
$rootScope.$digest(); | ||
noButton.click(); | ||
|
||
expect(rejected).to.be(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can also be done with a spy.
|
||
return function confirmModal(message, onConfirm = noop, onCancel = noop, confirmButtonText = 'Confirm', cancelButtonText = 'Cancel') { | ||
|
||
if (modalPopover) throw new Error('Ah ah ah, only one modal, buddy!'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this error message clearer? E.g. "You've called confirmModal but there's already a modal open. You can only have one modal open at a time."
export class Popover { | ||
constructor(element) { | ||
this.element = element; | ||
angular.element(document.body).append(this.element); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One killer improvement would be to rename this file modal_overlay.js
and rename the class ModalOverlay
. Then we could make this logic responsible for creating the modal overlay wrapper element:
this.element = /* don't remember how to create an element, but that's what we should do here, with <div class="modalOverlay"></div> */;
this.element.append(element);
@cjcenizal @w33ble thanks for the chat today, helped a lot. Code should be updated as discussed plus most recent comments. |
@@ -0,0 +1,3 @@ | |||
import './confirm_modal'; | |||
import './safe_confirm'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having safe_confirm in modals seems weird. The safe_confirm is still its own service, the fact that is uses a modal now is an implementation detail...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was always a modal, it's just that now it uses our custom modal. Even if we left it using the native modal I think it could still go in a modals folder.
There is nothing else that went in the previous safe_confirm folder (except tests) so it doesn't deserve a dedicated extra folder IMO. Is there another location you think it could go that isn't in it's own dedicated folder?
Overall, this looks great! One UX detail - it would be great if safeConfirm/confirmModal would focus the Okay/Confirm button by default, so users could just hit enter and confirm the dialog. This is how the vanilla |
@w33ble good point about default focus, fixed! |
Love this change. LGTM! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have one stupid suggestion; then merge away!
<div class="kuiModal" style="width: 450px" data-test-subj="confirmModal"> | ||
<div class="kuiModalBody"> | ||
<div class="kuiModalBodyText"> | ||
{{message}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be indented once.
Backports PR #9859 **Commit 1:** Introduce custom modal confirmation dialogs * Original sha: 6b727df * Authored by Stacey Gammon <[email protected]> on 2017-01-13T15:56:44Z **Commit 2:** address code review comments and implement new angular style suggestions from Joe * Original sha: 5ca36df * Authored by Stacey Gammon <[email protected]> on 2017-01-13T22:12:44Z **Commit 3:** refactor no point making vanilla code that's really only going to work with angular i guess... also broke apart the confirm modal from the injector, and made safeConfirm just use the confirm modal * Original sha: ce783b2 * Authored by Joe Fleming <[email protected]> on 2017-01-14T01:09:52Z * Committed by Stacey Gammon <[email protected]> on 2017-01-17T14:30:21Z **Commit 4:** Clean up from merge - Move safe_confirm into modals folder - make data-test-subj camelCase. - get rid of $timeout call, doesn’t seem necessary anymore, now that window.confirm is not being used. * Original sha: c91fbc6 * Authored by Stacey Gammon <[email protected]> on 2017-01-16T15:00:14Z **Commit 5:** Fix tests * Original sha: e581374 * Authored by Stacey Gammon <[email protected]> on 2017-01-16T15:42:41Z **Commit 6:** Use ui-framework styles * Original sha: d1e2b12 * Authored by Stacey Gammon <[email protected]> on 2017-01-17T15:22:52Z **Commit 7:** Address latest round of comments and remove adoption of new patterns. * Original sha: 0b690f1 * Authored by Stacey Gammon <[email protected]> on 2017-01-17T20:00:52Z **Commit 8:** Improve error message. Use sinon in tests. * Original sha: 92f815d * Authored by Stacey Gammon <[email protected]> on 2017-01-17T20:07:22Z **Commit 9:** Focus on the confirm button by default * Original sha: 27be4df * Authored by Stacey Gammon <[email protected]> on 2017-01-18T14:17:44Z **Commit 10:** Indent message * Original sha: 3dc6c93 * Authored by Stacey Gammon <[email protected]> on 2017-01-18T22:04:49Z
Backports PR #9859 **Commit 1:** Introduce custom modal confirmation dialogs * Original sha: 6b727df * Authored by Stacey Gammon <[email protected]> on 2017-01-13T15:56:44Z **Commit 2:** address code review comments and implement new angular style suggestions from Joe * Original sha: 5ca36df * Authored by Stacey Gammon <[email protected]> on 2017-01-13T22:12:44Z **Commit 3:** refactor no point making vanilla code that's really only going to work with angular i guess... also broke apart the confirm modal from the injector, and made safeConfirm just use the confirm modal * Original sha: ce783b2 * Authored by Joe Fleming <[email protected]> on 2017-01-14T01:09:52Z * Committed by Stacey Gammon <[email protected]> on 2017-01-17T14:30:21Z **Commit 4:** Clean up from merge - Move safe_confirm into modals folder - make data-test-subj camelCase. - get rid of $timeout call, doesn’t seem necessary anymore, now that window.confirm is not being used. * Original sha: c91fbc6 * Authored by Stacey Gammon <[email protected]> on 2017-01-16T15:00:14Z **Commit 5:** Fix tests * Original sha: e581374 * Authored by Stacey Gammon <[email protected]> on 2017-01-16T15:42:41Z **Commit 6:** Use ui-framework styles * Original sha: d1e2b12 * Authored by Stacey Gammon <[email protected]> on 2017-01-17T15:22:52Z **Commit 7:** Address latest round of comments and remove adoption of new patterns. * Original sha: 0b690f1 * Authored by Stacey Gammon <[email protected]> on 2017-01-17T20:00:52Z **Commit 8:** Improve error message. Use sinon in tests. * Original sha: 92f815d * Authored by Stacey Gammon <[email protected]> on 2017-01-17T20:07:22Z **Commit 9:** Focus on the confirm button by default * Original sha: 27be4df * Authored by Stacey Gammon <[email protected]> on 2017-01-18T14:17:44Z **Commit 10:** Indent message * Original sha: 3dc6c93 * Authored by Stacey Gammon <[email protected]> on 2017-01-18T22:04:49Z
Stop using native confirm dialogs and create a custom one. @cjcenizal is planning on further improving the UI of the dialog but here it is for now:
You can test it out in timelion by trying to delete a saved sheet. All usages of safeConfirm will now use this custom modal dialog.