Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

feat(Popup): close popup on click#410

Merged
kuzhelov merged 14 commits intomasterfrom
feat/popup-close-on-outside-click
Nov 8, 2018
Merged

feat(Popup): close popup on click#410
kuzhelov merged 14 commits intomasterfrom
feat/popup-close-on-outside-click

Conversation

@kuzhelov
Copy link
Contributor

@kuzhelov kuzhelov commented Oct 31, 2018

TODO

  • update CHANGELOG

Close Popup on mouse click

This set of changes introduces functionality that will close Popup on click. Currently Portal is not reused, as there is some refactoring planned for this component before reusing it in Popup. At the same time, provided changes introduce full set of functionality client would expect, so this remains to be just a question of further refactoring (Portal) and generalization (reuse Portal).

EventStack subscription API adjustments

Event stack subscriptions were reworked, to make the API for subscribing and unsubscribing to be safer. Please, consider to take a look on the following comment for more details on that: #391 (comment)

@codecov
Copy link

codecov bot commented Oct 31, 2018

Codecov Report

Merging #410 into master will decrease coverage by 1.38%.
The diff coverage is 20.58%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #410      +/-   ##
==========================================
- Coverage   91.79%   90.41%   -1.39%     
==========================================
  Files          41       41              
  Lines        1329     1356      +27     
  Branches      193      198       +5     
==========================================
+ Hits         1220     1226       +6     
- Misses        105      126      +21     
  Partials        4        4
Impacted Files Coverage Δ
src/components/Portal/Portal.tsx 98.18% <100%> (+1.88%) ⬆️
src/components/Popup/Popup.tsx 33.33% <12.9%> (-7.66%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 34bf5f1...641361c. Read the comment docs.

@kuzhelov kuzhelov changed the title feat(Popup): close popup on outside area click feat(Popup): close popup on click Oct 31, 2018
// ------------------------------------

_find = (target, autoCreate = true) => {
private _find = (target, autoCreate = true) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add also typings for methods' parameters, in regards to refactoring of event stack?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, you might consider it as separate PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would rather focus on functionality in this PR - those haven't existed before, would like to devote PR to introduce them

Copy link
Collaborator

@bmdalex bmdalex Nov 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kuzhelov - I think whenever we claim we'll add something in a follow-up PR we should create tickets or issues to actually track the work so that it won't slip through the cracks and other devs have access to it and can pick it up


private closeAndFocusTriggerOnClickIfOpen() {
if (this.state.open) {
setTimeout(() => this.clickSubscription.replaceWith('click', this.closeAndFocusTrigger))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in your previous refactoring setTimeout was part of EventStack and, as I understood, a motivation of it was performance reason (please, correct me, if I am wrong). So the question is, should we leave it as part of the library so everyone who uses it can benefit from it? It can be as a separate method subscribeAsync.

Copy link
Contributor Author

@kuzhelov kuzhelov Oct 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a very good point. Actually, for now have decided to move this aspect to the client's code, as have discovered few additional subtle problems that may arise with eventStack's logic (and the fact that subscription handler could be called at the same JS frame).

The simplest example - suppose that there is a following client code:

foo() {
  // note, with 'setTimeout' this subscription will be made async
  const subscription = EventStack.subscribe('click', ...)
   
  // so there is, in fact, no active subscription at this point
  subscription.unsubscribe()
}

Note that if subscription logic is async, this will result in subscription still being active at the end of the method - as 'unsubscribe' will be invoked at the moment when there is no subscription made yet.

One simplest solution would be to make all the necessary methods async, but this will complicate client's code. What I am thinking about, however, is a bit more elaborative approach where all these bits will be handled properly by eventStack, at the same time without switch to async model requested from the client (as async methods are quite error and race condition prone to be used in React event handlers). Now am working on the PR where would like to address this problem.


private static isBrowserContext = isBrowser()

private clickSubscription = EventStackSubscription.empty()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be a good idea to add a prop to choose if the popup should be closed onClick. We may want to have it true by default

Copy link
Contributor Author

@kuzhelov kuzhelov Oct 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just as a counterargument, we might argue that as long as Popup supports controlled mode, there are all the necessary tools to customize Popup's behavior.

Please, don't get me wrong - personally, see this suggestion as being absolutely reasonable. At the same time, would like to keep this PR simple and targeted to specific thing that it is aimed to address, so that we would be able to decide on this proposed prop later and, if necessary, introduce separate PR for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

const { forceFocusInsideTrap, isClickableOutsideFocusTrap } = this.props
if (forceFocusInsideTrap) {
eventStack.sub('focus', this._handleOutsideFocus, {
this._focusSubscription.unsubscribe()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the main point to call unsubscribe before actual subscription? Can't we assure that we are unsubscribed from any events at the initialization EventStack.noSubscription?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+100 it doesn't look like a good pattern, we don't want to attempt teaching developers to do this, they will forget; I find the eventStack util difficult to use now

Copy link
Contributor Author

@kuzhelov kuzhelov Nov 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, be specific about 'do what', thanks! Because see the following as a counterargument to the statement 'hard to use now':

now

// safe subscription
const subscription = EventStack.subscribe('onclick', () => { .. /* processing logic here */ })

// - this object has IntelliSense support
// - safe unsubscribe instruction
subscription.unsubscribe() 

before

// ooops
eventStack.sub('onclick', () => { ... })

// ooops
eventStack.sub('onclick', this.handleClick.bind(this))

eventStack.sub('onclick', this.handleClickWithBoundThis, { useCapture: true })
// oooops 
// - what were the arguments I've subscribed with? 
// - some of them are critical for proper unsubscription
eventStack.unsub('onclick', this.handleClickWithBoundThis)

Sorry, but now cannot agree with the point mentioned

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it looks much better now, that it was. The only my concern was about calling unsubscribe before actual subscribe, as it seems to me not very intuitive.

Copy link
Contributor Author

@kuzhelov kuzhelov Nov 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only my concern was about calling unsubscribe before actual subscribe, as it seems to me not very intuitive.

Here I would note that this is not something that is directly eventStack-related, as what we are doing is the following:

  • we would like to subscribe and store subscription in the object's field
  • there is (maybe) a pending subscription stored in this field - thus, we need to unsubscribe for it
    • as it is safe to call 'unsubscribe' on 'noSubscription', we are not making any null checks for it. If null value will be used as initialiser, we would write something like subscription && subscription.unsubscribe()
  • then we are making new subscription, storing reference on it

*Note that initially the following API was suggested for it, but we've agreed to cancel/postpone it:

// terminates previous subscription and replaces with new one
this.clickSubscription.replaceWith('click', ...)

Copy link
Collaborator

@bmdalex bmdalex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls take a look at comments

const { forceFocusInsideTrap, isClickableOutsideFocusTrap } = this.props
if (forceFocusInsideTrap) {
eventStack.sub('focus', this._handleOutsideFocus, {
this._focusSubscription.unsubscribe()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+100 it doesn't look like a good pattern, we don't want to attempt teaching developers to do this, they will forget; I find the eventStack util difficult to use now

// ------------------------------------

_find = (target, autoCreate = true) => {
private _find = (target, autoCreate = true) => {
Copy link
Collaborator

@bmdalex bmdalex Nov 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kuzhelov - I think whenever we claim we'll add something in a follow-up PR we should create tickets or issues to actually track the work so that it won't slip through the cracks and other devs have access to it and can pick it up

@miroslavstastny miroslavstastny added needs author feedback Author's opinion is asked and removed 🚀 ready for review labels Nov 5, 2018
@kuzhelov kuzhelov added 🚀 ready for review and removed needs author feedback Author's opinion is asked labels Nov 8, 2018
Copy link
Contributor

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I briefly took a loot into this, but seems that other developers already review it. Approving as all comments are resolved.

@kuzhelov kuzhelov merged commit affbec0 into master Nov 8, 2018
@layershifter layershifter deleted the feat/popup-close-on-outside-click branch November 9, 2018 09:23
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants