Does onGestureEvent of TapGestureHandler never fire? - react-native-gesture-handler

Here is a Snack of the issue below - https://snack.expo.io/#noitidart/tapgesturehandler-ongestureevent
Does onGestureEvent for TapGestureHandler never trigger? The docs do not make this clear. This seems to be happening to me:
import Reanimated from 'react-native-reanimated';
const handleStateChange = () => console.log('state changed');
const handleGestureEvent = () => console.log('got gesture event');
<TapGestureHandler
onHandlerStateChange={handleStateChange}
onGestureEvent={handleGestureEvent}>
<Reanimated.View
style={{ width: 100, height: 100, backgroundColor: 'green' }}
/>
</TapGestureHandler>
You will see on tap it never triggers ('gesture event')

I ran into this as well, the docs in gesture handler on discrete vs continuous gestures mentions it:
Keep in mind that onGestureEvent is only generated in continuous gesture handlers and shouldn't be used in the TapGestureHandler and other discrete handlers.
source: https://docs.swmansion.com/react-native-gesture-handler/docs/about-handlers
This is very confusing since examples using TapGestureHandler in Reanimated appear to use onGestureEvent, but I have found that handling the state change myself is the way to go.
Good news is, it is incredibly simple to manage since it is a discrete gesture.

Related

SwiftUI - Use .refreshable on ForEach to empower pull to refresh experience

I'm making use of the new .refreshable() function. For Testing purposes I'm having this simple sleep function:
func load() async {
await Task.sleep(2 * 1_000_000_000)
}
Appending this to a list works fine:
.refreshable {
await load()
}
However: When I'm trying to use it with a ForEach Loop it's not working. Do you know how to overcome this issue? Background: I need ForEach to ensure custom styling. In the List I'm too limited. I already tried to style it using some attributes like
.buttonStyle(.plain)
.listRowSeparator(.hidden)
But I'm not able to remove the badges or the distance in blue as well as impacting the spacing between the list objects.
Would be great if you can let me know about your thoughts on this. I wan to avoid building up my own pull to refresh view with coordinators etc. like it was done back in SwiftUI 2.0 times :D Really love the .refreshable action.
Many Thanks!
If you want to use refreshable on anything besides List, it's up to you to provide the user interface for it. The refreshable documentation only says it works for List:
When you apply this modifier to a view, you set the refresh value in the view’s environment to the specified action. Controls that detect this action can change their appearance and provide a way for the user to execute a refresh.
When you apply this modifier on iOS and iPadOS to a List, the list provides a standard way for the user to refresh the content. When the user drags the top of the scrollable content area downward, the view reveals a refresh control and executes the provided action. Use an await expression inside the action to refresh your data. The refresh indicator remains visible for the duration of the awaited operation.
To fully customize the look of a List, I did the following:
Use ForEach to iterate within the List.
Add .listStyle(.plain) to List.
Use .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) with ForEach to get rid of any inner paddings.
Use .listRowSeparator(.hidden, edges: .all) with ForEach to hide separators. I found it a little bit annoying to customize it to my needs. However, there are some way in iOS 16 to do that as mentioned here.
Finally use overlay with ForEach as in the code block below to get rid of arrows on the right.
You can now whatever design you want within the list and get benefits of its capabilities.
List {
ForEach(presenter.items) { item in
ItemView(item: item)
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowSeparator(.hidden, edges: .all)
.overlay {
NavigationLink(destination: { Text("Detail View") }, label: { EmptyView() })
.opacity(0)
}
}
.listStyle(.plain)

Control width of marker line in Nivo bar chart

I'm using the Nivo stacked bar chart to show a score range on the bar. To do this, I've used the markers prop. I've almost got it looking the way I'd like except the markers extend beyond the bar and it's not what I need.
When someone asked the same question on Github, it looks as though there isn't currently an easy way to do this. plouc, the creator of nivo said the following:
short answer, you cannot :/ However you can use an extra layer to achieve the same result, using the layers property and adding an extra component.
The photo attached is from the Nivo documentation on adding a marker (clicking on the Story tab will show basic code).
Here is the source code for the markers item. If you search for width throughout the document, you can see that it's set in the x2 of the line. Scrolling down further, you can see there is a strokeWidth property, but from what I can tell it only controls the thickness of the line, not how wide it is.
Can someone please let me know what I missed?
Here is my code. I am displaying two markers on my bar chart so there are two marker objects passed in. I've removed unrelated Bar props for simplification.
<NivoBar
data={data}
keys={['floor', 'pattern', 'ceiling']}
markers={[
{
axis: 'y',
position: 'right',
legendOffsetX: -34,
legendOffsetY: 0,
value: ceiling,
lineStyle: {stroke: 'red', strokeWidth: 2},
legend: `${ceiling}%`,
legendOrientation: 'horizontal',
textStyle: {fill: 'orange', fontWeight: 'bold'}
},
{
axis: 'y',
position: 'right',
legendOffsetX: -34,
legendOffsetY: 0,
value: floor,
lineStyle: {stroke: 'red, strokeWidth: 2},
legend: `${floor}%`,
legendOrientation: 'horizontal',
textStyle: {fill: 'orange, fontWeight: 'bold'}
}
]}
/>

Ember Test: input focus

I really don't understand the chain of events that's happening here. Trying to follow the guide as well as possible. I have:
test('Tab focus', function(assert) {
visit('/demo/form');
click('input[type=text]');
andThen(function() {
assert.equal(
find('input[type=text]').css('borderTopColor'), 'rgb(0, 125, 164)', 'Text input has focus'
);
});
});
only to have it fail:
There are no transitions on the color change, and if I hit rerun, it DOES pass.
for anyone still looking for an answer - you have to trigger "focus" event manually in your test:
triggerEvent(<alement selector>, 'focus');
more info: https://guides.emberjs.com/v2.14.0/testing/acceptance/#toc_asynchronous-helpers

Testing: how to assert between two sequential promises/run.laters? How to skip `run.later` waiting in tests?

Here's a simple component:
App.FooBarComponent = Ember.Component.extend({
tagName: "button",
status: "Ready",
revertStatusPeriodMs: 2000,
click: function() {
this.set('status', 'Pending');
// A fake ajax
this.run()
.then( function() {
this.updateStatus('Finished');
}.bind(this))
.catch( function() {
this.updateStatus('Error');
}.bind(this));
},
run: function() {
return new Ember.RSVP.Promise( function(resolve) {
Ember.run.later( function() {
resolve();
}, 500);
});
},
updateStatus: function(statusText) {
this.set('status', statusText);
var periodMs = this.get('revertStatusPeriodMs') || 1000;
Ember.run.later( function() {
this.set('status', 'Ready');
}.bind(this), periodMs);
}
});
It does a simple thing: when clicked, it displays some text. Later replaces the text with another one. Even later, it reverts the text to the initial one.
The code works fine. My problem is that i'm unable to write a test for it.
test('clicking the button should set the label', function(assert) {
expect(4);
visit('/');
assert.equal( find('button').text().trim(), 'Ready', 'Should initially be "Ready"');
andThen(function() {
click('button');
assert.equal( find('button').text().trim(), 'Pending', 'Should "Pending" right after click');
});
// This one fires too late!
andThen(function() {
assert.equal( find('button').text().trim(), 'Finished', 'Should become "Finished" after promise fulfills');
});
andThen(function() {
assert.equal( find('button').text().trim(), 'Ready', 'Should eventually return to the "Ready" state');
});
});
I have two problems:
I'm unable to test the Finished state. It seems that andThen waits for all promises and run.laters to finish, while i want to test an intermediate state. How do i run an assertion between two sequential promises/run.laters?
Times can be long, and tests can take forever to complete. I generally don't mind refactoring the code for better testability, but i refuse to adjust times in the app based on the environment (e. g. 2000 for dev/prod, 0 for test). Instead, i would like to use a timer mock or some other solution.
I've tried Sinon and failed: when i mock the timer, andThen never returns. Neither of these solutions helped me.
JSBin: http://emberjs.jsbin.com/fohava/2/edit?html,js,output (Sinon is included)
None of your test code seems to use asynch – which would seem to be crucial since without anything to distinguish them, the last 2 tests will execute in the same tick (tests 3 & 4 can't both be true at the same time). I'm not familiar with QUnit, but I did find it's async method, which would seem to be pertinent. However when I tried calling assert.async, it blew up. I tried updating QUnit 1.17, still no dice.
So I don't have a solution for you. The problem, though, is that Ember will only execute both andThen tests once all the asynchronous execution has finished – this means that test 3 will never be true.
I did some code where I basically got it working but it seems really finicky and not something you would want to be do be doing..
I did this a few days ago but never replied because I wasn't happy with it - it doesn't give you enough control..
The reason I'm replying now however is because I've found a PR that may be of use to you: PR 10463
I didn't look into too much detail but apparently it "allows custom async test helpers to pause execution by returning a promise"
I'm not sure what version of Ember you're using - but if you can hold out for this PR it may help you..

Binding 'style' to a computed property

I have a component which is inserted into the DOM as a '' tag (e.g., default behaviour). The component's job is to wrap a 3rd party jQuery tool and I'm trying to ensure it is responsive to "resize" events so I would like to explicitly set width and height style attributes.
In the component, it is easy enough to being to the style attribute:
attributeBindings: ['style'],
style: function() {
return "width: auto";
}.property('widthCalc'),
In this case, this works but doesn't do anything useful because style just returns a static string (width: auto).
Instead what I want to do is -- based on any change to the computed property widthCalc -- set the width based on the new value. So here's the next logical step:
style: function() {
var width = $('body')[0].offsetWidth;
return 'width: ' + width + 'px';
}.property('widthCalc'),
This too works, dynamically setting the DIV to the width of the body's width (note: this isn't really what I want but it does prove that this simple binding works). Now what I really want is to get the value of width from a computed property on the component but I don't even have to go that far to run into trouble; notice that instead of a global jQuery selector I switch to a localised component-scoped selector:
style: function() {
var width = this.$().offsetWidth;
return 'width: ' + width + 'px';
}.property('widthCalc'),
Unfortunately this causes the page NOT to load and gives the following error:
Uncaught Error: Something you did caused a view to re-render after it rendered but before it was inserted into the DOM.
I imagine this is Ember run-loop juju but I'm not sure how to proceed. Any help would be appreciated.
Since it is not possible to call this.$() in the component before it has been added to the dom, provide an initial value until the component is ready.
For example,
Setting a default value to the property style and on didInsertElement event reopen the class and define style as a calculated property using this.$()
http://emberjs.jsbin.com/delexoqize/1/edit?html,js,output
js
App.MyCompComponent = Em.Component.extend({
attributeBindings:["style"],
style:"visibility:hidden",
prop1:null,
initializeThisStyle:function(){
this.set("style","visibility:visible");
this.reopen({
style:function(){
// var thisOffsetWidth = this.$().get(0).offsetWidth;
return "visibility:visible;color:red;background-color:lightgrey;width:"+this.get("prop1")+"px";
}.property("prop1")
});
}.on("didInsertElement")
});
Alternatively handle the error raised by this.$() and provide a default value. Afterwards when the component will be added the property will be calculated as planned.
http://emberjs.jsbin.com/hilalapoce/1/edit?html,js,output
js
App.MyCompComponent = Em.Component.extend({
attributeBindings:["style"],
style:function(){
try{
this.$();//this will throw an erro initialy
return "visibility:visible;color:red;background-color:lightgrey;width:"+this.get("prop1")+"px";
}catch(e){
return "color:blue";
}
}.property("prop1"),
prop1:null
});
With the component I was trying to solve for I ended coming up with an solution that seems effective to me which I will share below. For an understanding of the why I was getting the error and how one might more directly address that error please see the comment from #melc above.
My Solution
What I'm solving for is resizing a jQuery component wrapped in an Ember component. In many cases, resizing is handled gracefully by CSS alone but some jQuery components -- including the very nice knob component from aterrien -- has JS which gets directly involved and therefore needs the containers width and height properties to be set explicitly by the Ember component so that it reacts appropriately.
When solving for this I realised my use-case had two problems:
Solving for a page resize event
Adjusting to the fact that my knob component was -- at times -- in the DOM but in a part of the DOM which was not visible (more explicitly it was in Bootstrap tab which wasn't visible).
The Resize Listener
The first part of the solution is to listen for a page-level resize of the page. I do this with the following:
resizeListener: function() {
var self = this;
self.$(window).on('resize', Ember.run.bind(self, self.resizeDidHappen));
}.on('didInsertElement'),
Page Resize Handler
When a resize is done at the "page" level I now want my component to inspect what the resize impact has been on the component:
resizeDidHappen: function() {
Ember.run.debounce(this, function() {
// get dimensions
var newWidth = Number(this.$().parent().get(0).offsetWidth);
var newHeight = Number(this.$().parent().get(0).offsetHeight);
// set instance variables
this.set('width', newWidth);
this.set('height', newWidth);
// reconfigure knob
this.$('.knob').trigger(
'configure',
{
width: newWidth,
height: newWidth
}
);
}, 300);
}
This solves the page resize problem if it exists in isolation but to make the component it is probably a good idea to solve for the visibility use case as well (certainly in my case it was critical).
Visibility Handler
Why? Well, for two reasons that I can think of:
Many jQuery components refuse to load or perform badly if they aren't loaded
The ember component appears to not be able to establish a "resize" event when it is not visible in the DOM
The one problem is that there is no DOM-level event for visibility changes, so how do we react to a change in visibility without polling on an interval? Well in most cases there will be a UI element which is controlling the state of visibility. In my case it's Bootstrap's tab bar and in this case they have events that fire on the tabs when they become visible. Great. Here's a selector for Bootstrap's selector (assuming you're inside the content area of the newly visible tab):
visibilityEventEmitter: function(context) {
// since there is no specific DOM event for a change in visibility we must rely on
// whatever component is creating this change to notify us via a bespoke event
// this function is setup for a Bootstrap tab pane; for other event emmitters you will have to build your own
try {
var thisTabPane = context.$().closest('.tab-pane').attr('id');
var $emitter = context.$().closest('.tab-content').siblings('[role=tabpanel]').find('li a[aria-controls=' + thisTabPane + ']');
return $emitter;
} catch(e) {
console.log('Problem getting event emitter: %o', e);
}
return false;
},
visibilityEventName: 'shown.bs.tab',
then we just need to add the following code:
_init: function() {
var isVisible = this.$().get(0).offsetWidth > 0;
if (isVisible) {
this.visibilityDidHappen();
}
}.on('didInsertElement'),
visibilityListener: function() {
// Listen for visibility event and signal a resize when it happens
// note: this listener is placed on a DOM element which is assumed
// to always be visibile so no need to wait on placing this listener
var self = this;
Ember.run.schedule('afterRender', function() {
var $selector = self.get('visibilityEventEmitter')(self);
$selector.on(self.get('visibilityEventName'), Ember.run.bind(self, self.visibilityDidHappen ));
});
}.on('didInsertElement'),
visibilityDidHappen: function() {
// On the first visibility event, the component must be initialised
if(!this.get('isInitialised')) {
this.initiateKnob();
} else {
// force a resize assessment as window sizing may have changed
// since last time component was visible
this.resizeDidHappen();
}
},
Note that this also results in a tiny refactor of our resize listener, removing it's trigger from the didInsertElement event and instead being triggered by initiateKnob which will happen not when the Ember component loads but instead lazy load at the first point of visibility in the DOM.
initiateKnob: function() {
var self = this;
this.set('isInitialised', true);
var options = this.buildOptions();
this.$('.knob').knob(options);
this.syncValue();
this.resizeDidHappen(); // get dimensions initialised on load
console.log('setting resize listener for %s', self.elementId);
self.resizeListener(); // add a listener for future resize events
},
resizeListener: function() {
this.$(window).on('resize', Ember.run.bind(this, this.resizeDidHappen));
},
Does it work?
To a large degree but not completely. Here's what works:
the first 'tab' which is visible at load resizes on demand
all tabs resize when they are switched to (aka, when they gain visibility)
what doesn't work is:
tabs other than the first tab do not resize (aka, the onresize callback appears broken)
The error I get is:
vendor.js:13693 Uncaught TypeError: undefined is not a function
Backburner.run vendor.js:13716
Backburner.join vendor.js:34296
run.join vendor.js:34349
run.bind vendor.js:4759
jQuery.event.dispatch vendor.js:4427
jQuery.event.add.elemData.handle
Not sure what to make of this ... any help would be appreciated. Full code can be found here:
https://gist.github.com/295e7e05c3f2ec92fb45.git