-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Allow edits to popup container class while the popup is not on the map #10889
Conversation
I'd be a fan of moving the |
@SnailBones I believe the failed CI tests are unrelated to your PR. I would pull in @arindam1993 's change from yesterday (780a0bb) either rebasing or merging from latest main, as you prefer. |
@arindam1993 What do you think of the implementation with another DOM element here? |
* Pin to chrome version 91 * Pin chrome version for test-browser
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.
Leaving some super nitpicky details as a review, but I rather like @arindam1993's suggestion of using a Set
here. It's not without precedence in GL JS, though I'm not 100.0% sure this usage was definitely intentional and didn't just happen to sneak through:
mapbox-gl-js/src/util/mapbox.js
Line 573 in 780a0bb
const authenticatedMaps = new Set(); |
I guess spread with ...
works as well for concatenating…
classList = new Set()
// Set(0) {}
classList.add('foo')
// Set(1) {"foo"}
classList.add('bar')
// Set(2) {"foo", "bar"}
[...classList].join(' ')
// "foo bar"
Co-authored-by: Ricky Reusser <rreusser@users.noreply.github.com>
@rreusser That's a clever implementation! I'm still struggling to understand what the use case is though. Do you have an example of when it might be necessary to manipulate classes in a popup before it has been added to the map? |
Ah, I wasn't 100% certain whether we wanted to preemptively create all of the DOM elements (I imagine they're relatively lightweight, but how many would people create?), so I thought maybe instead of lifting the DOM element's |
I can imagine a situation in which popups are set up with a default theme and users can change that theme before they're added to the map, which would make |
Merge with main
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.
Looks good! I'm somewhere between "request changes" and "approve" since the PR as-is looks good to me, but if it's reasonable to remove a source of truth and simply overwrite the container classes whenever they change, that would be my top preference, if only to avoid a potential corner case in which they get out of sync.
src/ui/popup.js
Outdated
* | ||
* @example | ||
* let popup = new mapboxgl.Popup() | ||
* popup.addClassName('some-class') | ||
*/ | ||
addClassName(className: string) { | ||
this._classList.add(className); | ||
if (this._container) { | ||
this._container.classList.add(className); |
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.
What do you think about overwriting classes with something like this._applyClassList()
which would look like:
_applyClassList () {
if (!this._container) return;
this._container.className = [...this._classList.values()].join(' ');
}
My main concern is that we have two independent sources of truth which we hope remain synchronized. But since we don't allow a user-supplied container anyway, so I don't think there should be a case where this would prevent you from accomplishing some behavior you couldn't simply achieve by using the supplied API.
However, this could be a breaking change if someone was previously manipulating the container class directly.
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 don't think there's a valid reason for anyone to manipulate the container class directly right now. (Still doesn't mean that no one's doing it.)
It wouldn't be quite as simple as overwriting: the container element has additional default classes. So this:
const popup = new Popup({className: 'some classes'});
creates this element:
<div class="mapboxgl-popup mapboxgl-popup-anchor-bottom yellow"></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.
Good point! Just wanted to be sure.
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 find one weird edge case: if the user toggles one of the default classes:
popup.toggleClassName('mapboxgl-popup');
This removes the item from the container while adding it to the list.
I'm sure we could fix this, but I'm not sure what the correct behavior would be or if it's worth the lift and added complexity just to solve a very specific edge case.
src/ui/popup.js
Outdated
if (this.options.className) { | ||
this.options.className.split(' ').forEach(name => | ||
if (this._classList) { | ||
this._classList.forEach(name => |
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.
If we overwrote classes, then I think this._container.className = [...this._classList.values()].join(' ')
would be the simplest way to accomplish this.
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.
Nice work on this, @SnailBones! Switching my review to approve. 👍 👏
|
||
t.throws(() => popup.addClassName(''), window.DOMException); |
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.
Since DOM is no longer directly updated, these functions no longer throw an error.
@@ -600,10 +619,9 @@ export default class Popup extends Evented { | |||
} | |||
|
|||
function normalizeOffset(offset: ?Offset) { | |||
if (!offset) { | |||
return normalizeOffset(new Point(0, 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.
Seemed like a strange case for recursion here
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 believe I've seen this style, especially in the symbol code, where it runs modified values through a different branch of the same function, but TBH I find it slightly hard to follow, so that if otherwise equivalent, I prefer your change 👍
@SnailBones You added a 'breaking change' label to this PR, is this breaking only in the way that popups were throwing errors on adding/removing/toggling and not anymore or is it something else? If it's breaking and requires user code changes, is there any transition that our users should follow once we release concerning this behavior? I'm interested to know the extent of this breaking change and whether it warrants an extra note on the CHANGELOG. |
Two breaking changes. First as you mention, the lack of throwing errors on invalid popup class names. Also, if a user has directly modified classList/className instead of using the popup functions, this change means that updates will reset classes added or removed in this way. The fix here would be ensuring that all modifications to the CSS class are performed with the three functions. I think that both of these breaking changes should only affect very rare edge cases. The former could be an issue if an app allowed for custom popup CSS classes (I can't think of a good reason for that, but maybe there's a reason it was in the unit tests.) The latter is something that users shouldn't be doing, but I don't think we can say with certainty that they're not. |
Just updated the changelog to: |
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.
Nice cleanup @SnailBones.
Just one more suggestion, otherwise its a 🟢 from me!
I don't think this counts as a breaking change since it would be a bit weird if someone was actually relying on classnames getting lost.
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.
Looks great! Despite having pointed out a couple nits, this looks great, and I'd opt to merge as-is.
I think @arindam1993 is probably right that we're fine not considering this a breaking change. In some sense, literally any fix is breaking if people relied upon broken behavior, so it's probably safe to explain it clearly in the changelog but consider it really just a fix for something people may have been doing which would have had undefined results.
src/ui/popup.js
Outdated
if (this._trackPointer) { | ||
classes.add('mapboxgl-popup-track-pointer'); | ||
} | ||
this._container.className = [...classes].join(' '); |
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 this could be rearranged to reduce a set allocation (spread to arrray, then .push
optional classes, then join), but it's so far down in the list of memory usage, that I fully support the above 👍
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!
@@ -539,46 +582,22 @@ export default class Popup extends Evented { | |||
this._lngLat = smartWrap(this._lngLat, this._pos, this._map.transform); | |||
} | |||
|
|||
if (this._trackPointer && !cursor) return; | |||
if (!this._trackPointer || cursor) { |
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.
Nit: Is there a reason for inverting the conditional? Early return can be nice to de-nest code when otherwise equivalent. Nothing blocking here for me though.
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.
My thinking was to remove the early return allowing this. _updateClassList()
to be called in any condition. I realize there's another call inside here, I'll double-check and see if I can remove that.
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.
Yup it was redundant! (Oops) Fixed in latest commit.
@@ -600,10 +619,9 @@ export default class Popup extends Evented { | |||
} | |||
|
|||
function normalizeOffset(offset: ?Offset) { | |||
if (!offset) { | |||
return normalizeOffset(new Point(0, 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.
I believe I've seen this style, especially in the symbol code, where it runs modified values through a different branch of the same function, but TBH I find it slightly hard to follow, so that if otherwise equivalent, I prefer your change 👍
Currently,
addClassName
,removeClassName
andtoggleClassName
all have no effect when called before the popup is added to the map, and all have an incorrect example.In this P.R. I add a
Popup._classList
Set that keeps track of the added classes, pushing them to the DOM whenever the popup is added to the map.At @arindam1993 and @rreusser's suggestion, the popup now uses a Set to keep track of its classes, rewriting the element classList on every update to the popup. This creates a single source of truth and prevents strange behavior when toggling a default class. However, for users who are bypassing the methods to directly manipulate the element classList, this is a breaking change.
Other changes:
addClassName
andremoveClassName
to allow for method chaining.Launch Checklist
addClassName
to be called when popup is not on map.Add warnings for incorrect user actions (callingremoveClassName
andtoggleClassName
before popup is on map).Fix examples forremoveClassName
andtoggleClassName
removeClassName
andtoggleClassName
to be called when popup is not on map.addClassName
.removeClassName
andtoggleClassName
.mapbox-gl-js
changelog:<changelog>Popup.addClassName, removeClassName and toggleClassName work while the popup is not on the map, and no longer throw DOM errors for invalid class names. Directly modifying the popup container CSS class no longer works, all edits should use these functions. </changelog>