Scroll to top of a bubble in botframework webchat - javascript

Some answers of our chatbot are very long. The webchat scrolls automatically to the bottom so users have to scroll up to get to the top of the bubble and start reading.
I've implemented a custom renderer (react) to wrap the answers into a custom component which simply wraps the answer into a div-tag. I also implemented a simple piece of code to scroll to the top of the bubble.
const MyCustomActivityContainer = ({ children }) => {
const triggerScrollTo = () => {
if (scrollRef && scrollRef.current) {
(scrollRef.current as any).scrollIntoView({
behavior: 'smooth',
block: 'start',
})
}
}
const scrollRef: React.RefObject<HTMLDivElement> = React.createRef()
return (
<div ref={ scrollRef } onClick={ triggerScrollTo }>
{ children }
</div>
)
}
export const activityMiddleware = () => next => card => {
if (/* some conditions */) {
return (
<MyCustomActivityContainer>
{ next(card) }
</MyCustomActivityContainer>
);
} else {
return (
{ next(card) }
)
}
};
But this only works if the scrollbar slider is not at its lowest position (there is at least 1 pixel left to scroll down, see here). The problem is the useScrollToBottom hook which always scrolls to bottom automatically if the scrollbar is completely scrolled down.
Is there any way to overwrite the scroll behavior or to temporarily disable the scrollToBottom feature?

As there is no reproducible example I can only guess.
And I'll have to make some guesses on the question too.
Because it's not clear what exactly in not working:
Do you mean that click on the <div> of MyCustomActivityContainer and subsequent call to triggerScrollTo doesn't result into a scroll?
That would be strange, but who knows. In this case I doubt anyone will help you without reproducible example.
Or do you mean that you can scroll the message into view, but if it is already in the view then new messages can result into a scroll while user is still reading a message.
That's so, but it contradicts with you statement that your messages are very long, because that would be the problem with short messages, not with the long ones.
But anyway, you should be able to fix that.
If it works fine with 1 pixel off the lowest position, then just scroll that 1 pixel. You'll need to find the scrollable element. And do scrollable_element.scrollTop -= 1. I tested this approach here. And it worked (there the scrollable element is the grandparent of <p>'s)
Or do you try to scroll automatically at the moment the message arrives? Аnd that is the real issue, but you forgot to mention it, and didn't posted the code that tries to auto-scroll?
In that case you can try to use setTimeout() and defer the scroll by, let's say, 200ms.
This number in based on what I gathered from the source:
BotFramework-WebChat uses react-scroll-to-bottom
In react-scroll-to-bottom there are some timeouts 100ms and 34ms
BotFramework-WebChat doesn't redefine them
There are some heuristics in react-scroll-to-bottom that probably coursing the trouble
https://github.com/compulim/react-scroll-to-bottom/blob/3eb21bc469ee5f5095a431ac584be29a0d2da950/packages/component/src/ScrollToBottom/Composer.js
Currently, there are no reliable way to check if the "scroll" event is trigger due to user gesture, programmatic scrolling, or Chrome-synthesized "scroll" event to compensate size change. Thus, we use our best-effort to guess if it is triggered by user gesture, and disable sticky if it is heading towards the start direction.
And
https://github.com/compulim/react-scroll-to-bottom/blob/f19b14d6db63dcb07ffa45b4433e72284a9d53b6/packages/component/src/ScrollToBottom/Composer.js#L91
For what we observed, #1 is fired about 20ms before #2. There is a chance that this stickyCheckTimeout is being scheduled between 1 and 2. That means, if we just look at #1 to decide if we should scroll, we will always scroll, in oppose to the user's intention.
That's why I think you should use setTimeout()

Since there isn't a reproducible code for me tweak and show you. My suggestion is tweak your code slightly. Chatbot requires constant streaming of data when a new message arrives calculate the height of the div element created for the message. If the div element is greater than the widget height scroll to the top else you can choose to leave it as it is.

Related

How to animate a button when it is visible for the user by scrolling

When you scroll on a page, the page shows an element, I want to be able to specify this element and
do code with it using JS, is this possible? I tried something but it had another problem..
What I tried was,
let section = document.getElementById('out');
window.onscroll = function() {
if (window.scrollY >= 678) {
document.getElementById('out').style.color = "red";
} else {
document.getElementById('out').style.color = "black"
}
}
I didn't use animate here, I just made sure it works, and it did, well almost, because if you zoom in/out it ruins it, I think that's because I got the 678 by going to the button and printing scrollY manually, is there anyway to make that automatic, so it works on any element I need?
I searched a lot and can't seem to find what I need, the solutions need jQuery, I need a solution only with html, css, and javascript.
In the future the solution will be css scroll timelines, but as that feature is at the time of writing experimental and is not supported by major browsers you can use intersection observers.
Quoted from MDN:
The Intersection Observer API lets code register a callback function that is executed whenever an element they wish to monitor enters or exits another element (or the viewport), or when the amount by which the two intersect changes by a requested amount.
To animate a component when it is in or out of view, you can give animated elements a .hidden class in your html markup and create an intersection observer which appends the .shown class to .hidden elements when they are in view.
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => entry.target.classList.toggle(“shown”, entry.isIntersecting))
})
const hiddenElements = document.querySelectorAll(“.hidden”)
hiddenElements.forEach((el) => observer.observe(el))
Then you can just apply transitions under a <selector>.shown css rule.

Can't scroll when testing using intern.js

I use moveMouseTo but it doesn't seem to work. This is my code. Does anyone able to see what is wrong with it? The assert should work and not return error, because if you try to scroll down the page at www.keylocation.sg, it will shows a navigation bar.
Thanks before.
define([
'intern!object',
'intern/chai!assert',
'./util',
'intern/dojo/node!fs'
], function(registerSuite, assert, util, fs) {
var suite = {
name: 'home-navbar',
afterEach: util.checkJSErrors,
// testing the visibility of navigation bar in the home page
'Home page navigation bar: navigation bar visibility': function() {
var remote = this.remote
.setWindowSize(1024, 768)
.get('about:blank')
.get('https://www.keylocation.sg');
this.timeout = 300000;
return remote
// check: Home page loads, navbar is not visible
.findById('header-menu').isDisplayed().then(assert.isFalse).end()
// check: Scroll down to next page, navbar becomes visible
.moveMouseTo(0,1000).end()
.findById('header-menu').isDisplayed().then(assert.isTrue).end();
}
};
registerSuite(suite);
});
Your test is probably failing because of how the WebDriver server you're using determines element visibility. The header menu on that page is initially not visible because it has a negative top margin, which moves it outside of the viewport. However, according to the WebDriver spec, an element that's outside the viewport because of a negative margin isn't necessarily considered invisible. Chrome's and Firefox's WebDriver servers, at least, say it's visible. This is a WebDriver issue rather than an Intern issue; Intern is basically just asking the WebDriver server "is this element visible" and telling you the answer.
Since isDisplayed doesn't seem like it will work in this case, you could instead check whether the element has the disabled class, which is what causes it to have the negative margin.
Unfortunately, trying to simply move the mouse 1000 pixels outside of an element context doesn't scroll the page. When you don't give moveMouseTo an element, it moves within the current context element (the last thing that was found). When there's no context, it's moving within the outermost element, which in this case is only 632px high. You'll need to set the context to an element that's tall enough to contain your movement offset, or you can find an element at the bottom of the page, like the footer, and move the mouse to that:
.findByCssSelector('.wrapper')
.moveMouseTo(0, 1500) // 1000 pixels is too small to show the scrollbar
.end()
or
.findByTagName('footer')
.then(function (footer) {
return this.parent.moveMouseTo(footer);
})
.end()
Your test has a few other issues you may want to correct as well. The initial get('about:blank') isn't necessary. There's no reason to split the two halves of your command chain; the timeout applies to the whole chain whether it's split or whole. You don't need an end after the moveMouseTo; end is for popping elements off the command chain's context, and moveMouseTo doesn't add anything to the context.

Force mobile browser zoom out with Javascript

In my web app, I have some thumbnails that open a lightbox when clicked. On mobile, the thumbnails are small and the user typically zooms in. The problem is that when they click to play it, the lightbox is outside of the viewable area (they have to scroll to the lightbox to see the video). Is it possible to force a mobile browser to zoom out so they can see the whole page?
Making the page more responsive is not an option right now; it is a fairly large web application and it would take a huge amount of time to refactor.
Dug through a lot of other questions trying to get something to zoom out to fit the entire page. This question was the most relevant to my needs, but had no answers. I found this similar question which had a solution, although implemented differently, and not what I needed.
I came up with this, which seems to work in Android at least.
initial-scale=0.1: Zooms out really far. Should reveal your whole website (and then some)
width=1200: Overwrites initial-scale, sets the device width to 1200.
You'll want to change 1200 to be the width of your site. If your site is responsive then you can probably just use initial-scale=1. But if your site is responsive, you probably don't need this in the first place.
function zoomOutMobile() {
var viewport = document.querySelector('meta[name="viewport"]');
if ( viewport ) {
viewport.content = "initial-scale=0.1";
viewport.content = "width=1200";
}
}
zoomOutMobile();
Similar to Radley Sustaire's solution I managed to force unzoom whenever the device is turned in React with
zoomOutMobile = () => {
const viewport = document.querySelector('meta[name="viewport"]');
if ( viewport ) {
viewport.content = 'initial-scale=1';
viewport.content = 'width=device-width';
}
}
and inside my render
this.zoomOutMobile();
1 edge case I found was this did not work on the Firefox mobile browser
I ran in a similar problem, rather the opposite, I guess, but the solution is most certainly the same. In my case, I have a thumbnail that people click, that opens a "popup" where users are likely to zoom in to see better and once done I want to return to the normal page with a scale of 1.0.
To do that I looked around quite a bit until I understood what happens and could then write the correct code.
The viewport definition in the meta data is a live value. When changed, the system takes the new value in consideration and fixes the rendering accordingly. However, the "when changed" is detected by the GUI and while the JavaScript code is running, the GUI thread is mostly blocked...
With that in mind, it meant that doing something like this would fail:
viewport = jQuery("meta[name='viewport']");
original = viewport.attr("content");
force_scale = original + ", maximum-scale=1";
viewport.attr("content", force_scale); // IGNORED!
viewport.attr("content", original);
So, since the only way I found to fix the scale is to force it by making a change that I do not want to keep, I have to reset back to the original. But the intermediary changes are not viewed and act upon (great optimization!) so how do we resolve that issue? I used the setTimeout() function:
viewport = jQuery("meta[name='viewport']");
original = viewport.attr("content");
force_scale = original + ", maximum-scale=1";
viewport.attr("content", force_scale);
setTimeout(function()
{
viewport.attr("content", original);
}, 100);
Here I sleep 100ms before resetting the viewport back to what I consider normal. That way the viewport takes the maximum-scale=1 parameter in account, then it times out and removes that parameter. The scale was changed back to 1 in the process and restoring my original (which does not have a maximum-scale parameter) works as expected (i.e. I can scale the interface again.)
WARNING 1: If you have a maximum-scale parameter in your original, you probably want to replace it instead of just appending another value at the end like in my sample code. (i.e. force_scale = original.replace(/maximum-scale=[^,]+/, "maximum-scale=1") would do the replace--but that works only if there is already a maximum-scale, so you may first need to check to allow for either case.)
WARNING 2: I tried with 0ms instead of 100ms and it fails. This may differ from browser to browser, but the Mozilla family runs the immediately timed out timer code back to back, meaning that the GUI process would never get a chance to reset the scale back to 1 before executing the function to reset the viewport. Also I do know of a way to know that the current viewport values were worked on by the GUI... (i.e. this is a hack, unfortunately.)
This one works for me
let sw = window.innerWidth;
let bw = $('body').width();
let ratio = sw / bw - 0.01;
$('html').css('zoom', ratio);
$('html').css('overflow-x', 'hidden');
Its fits html to screen and prevents from scrolling. But this is not a good idea and work not everywhere.
var zoomreset = function() {
var viewport = document.querySelector("meta[name='viewport']");
viewport.content = "width=650, maximum-scale=0.635";
setTimeout(function() {
viewport.content = "width=650, maximum-scale=1";
}, 350);
}
setTimeout(zoomreset, 150);
replace 650 with the width of your page

javascript bind an event handler to horizontal scroll

Is there a way in javascript to bind an event handler to a horizontal scroll as opposed to the generic scroll event which is fired when the user scrolls horizontally and vertically? I want to trigger an event only when the user scrolls horizontally.
I searched around for an answer to this question, but couldn't seem to find anything.
Thanks!
P.S. My apologies if I'm using some terminology incorrectly. I'm fairly new to javascript.
UPDATE
Thanks so much for all your answers! In summary, it looks like you are all saying that this isn't supported in javascript, but I that I can accomplish the functionality with something like this (using jQuery) (jsFiddle):
var oldScrollTop = $(window).scrollTop();
$(window).bind('scroll', function () {
if (oldScrollTop == $(window).scrollTop())
//scrolled horizontally
else {
//scrolled vertically
oldScrollTop = $(window).scrollTop();
}
});​
That's all I needed to know. Thanks again!
Answering from my phone, so unable to provide code at the moment.
What you'll need to do is subscribe to the scroll event. There isn't a specific one for vertical/horizontal.
Next, you'll need to get some measurements about the current display area. You'll need to measure the window.clientHeight and window.clientWidth.
Next, get window.top and window.left. This will tell you where position of the viewport is, ie if it's greater than 0 then scroll bars have been used.
It's pretty simple math from here to get what you need. If no one else has provided a code example in the next few hours I'll try to do so.
Edit:
A bit further information.
You must capture the scroll event. You also need to store the initial window.top and window.left properties somewhere. Whenever the scroll event happens, do a simple check to see if the current top/left values differ from the stores value.
At this point, if either are different you can trigger your own custom events to indicate vertical or horizontal scrolling. If you are using jQuery, this is very easy. If you are writing js without library assistance, it's easy too but a little more involved.
Do some searches for event dispatching in js.
You can then write any other code you want to subscribe to your custom events without needing to tie them together with method calls.
I wrote a jQuery plugin for you that lets you attach functions to the scrollh event.
See it in action at jsfiddle.net.
/* Enable "scrollh" event jQuery plugin */
(function ($) {
$.fn.enableHScroll = function() {
function handler(el) {
var lastPos = el
.on('scroll', function() {
var newPos = $(this).scrollLeft();
if (newPos !== lastPos) {
$(this).trigger('scrollh', newPos - lastPos);
lastPos = newPos;
}
})
.scrollLeft();
}
return this.each(function() {
var el = $(this);
if (!el.data('hScrollEnabled')) {
el.data('hScrollEnabled', true);
handler(el);
}
});
}
}(jQuery));
It's this easy to use:
$('#container')
.enableHScroll()
.on('scrollh', function(obj, offset) {
$('#info').val(offset);
});
Please note that scroll events come very fast. Even if you click in the scrollbar to jump to a new position, many scroll events are generated. You may want to adjust this code to wait a short time and accumulate all the changes in position during that time before firing the hscroll event.
You can use the same scroll event, but within your handler use the scrollLeft function to see if the scrollbar moved horizontally from the last time the event was fired. If the scrollbar did not move then just return from your handler. Otherwise update your variable to the new position and take action.
You can check if the the x value of the page changes and ignore your y value.
If the x value changes: There is your horizontal scroll.
With page-load, store the initial scrollbar positions for both in two variables (presumably both will be 0). Next, whenever a scroll event occurs, find the scrollleft and scrolltop properties. If the scrollleft property's value is different and scrolltop's value is same as compared to their earlier values, that's a horizontal scroll. Then set the values of the variables to the new scroll values.
No, there is no special event for scroll horizontal (it is for global scroll), but you can try to check the position of content by property .scrollLeft and if it's different from the previous value it means that the user scrolled content horizontally.

How to create a dynamic navigation bar which follows you when you reach certain location

I want to create a navigation bar similar to this site's:
http://www.mysupermarket.co.uk/#/shelves/top_offers_in_asda.html
Can anyone tell me how to create that navigation bar, which follows you as you scroll the page down, but not following you at the initial loading of page?
When you access to the given website, try to scrolling down and you will understand what I am talking about. The navigation bar that consists of MY SHOP, OFFERS, IDEAS & LIFESTYLE, BAKERY and so-on...
I have really no idea what it's called. At least tell me what it's called, so I'll be able to search.
Here is the solution I've done
window.onscroll = function(){
if(getScrollTop()>140) {
document.getElementById("menu").style.position="fixed";
} else {
document.getElementById("menu").style.position="";
}
}
function getScrollTop() {
if (window.onscroll) {
// Most browsers
return window.pageYOffset;
}
var d = document.documentElement;
if (d.clientHeight) {
// IE in standards mode
return d.scrollTop;
}
// IE in quirks mode
return document.body.scrollTop;
}
Holding an element on same position can be achieved by fixed position styling.
If you want your navigation bar to stay on exact same location, position:fixed; is enough. (At least non IE6)
You can find a working example and some details here
However, if you want your navigation bar to move from it's initial location to the top border of page as you scroll the page down, you must implement some JavaScript to catch page scroll event and move the <div> accordingly.
See this question for an example on how to do that.
Note: this won't work with the Android 2.3 browser; position:fixed will not behave as expected - it kinda of temporarily attaches its position to the scrolling element before jumping back to the top.
if you want you could just set the z-index to be a specific No. and that should work.
example
z-index:100;

Categories

Resources