Skip to content

Commit 92aaebe

Browse files
authored
Expose ScrollObserver as a hook (#115)
1 parent 4eaf3a7 commit 92aaebe

File tree

4 files changed

+113
-62
lines changed

4 files changed

+113
-62
lines changed

src/Lumi/Components/Table.js

-52
Original file line numberDiff line numberDiff line change
@@ -26,55 +26,3 @@ exports.isRightClick = function(e) {
2626
exports.hasWindowSelection = function() {
2727
return window.getSelection().type === "Range";
2828
};
29-
30-
exports.scrollObserver = function ScrollObserver(props) {
31-
var yArr = React.useState(false);
32-
var hasScrolledY = yArr[0];
33-
var setHasScrolledY = yArr[1];
34-
var xArr = React.useState(false);
35-
var hasScrolledX = xArr[0];
36-
var setHasScrolledX = xArr[1];
37-
React.useEffect(
38-
function() {
39-
var scrollParent = getScrollParent(props.node);
40-
if (scrollParent == null) return;
41-
function onScroll() {
42-
setHasScrolledY(scrollParent.scrollTop > 0);
43-
setHasScrolledX(scrollParent.scrollLeft > 0);
44-
}
45-
onScroll();
46-
scrollParent.addEventListener("scroll", onScroll, { passive: true });
47-
return function() {
48-
scrollParent.removeEventListener("scroll", onScroll, { passive: true });
49-
};
50-
},
51-
[props.root]
52-
);
53-
return props.render({
54-
hasScrolledY: hasScrolledY,
55-
hasScrolledX: hasScrolledX
56-
});
57-
};
58-
59-
// Walks up the DOM tree starting at the given node to find
60-
// the first parent with a scroll bar (including auto, which
61-
// on some devices hides the scroll bar until hovered).
62-
function getScrollParent(node) {
63-
var isElement = node instanceof HTMLElement;
64-
var computedStyle = isElement && window.getComputedStyle(node);
65-
var overflowY = computedStyle && computedStyle.overflowY;
66-
var overflowX = computedStyle && computedStyle.overflowX;
67-
var isScrollable =
68-
(overflowY &&
69-
!(overflowY.includes("visible") || overflowY.includes("hidden"))) ||
70-
(overflowX &&
71-
!(overflowX.includes("visible") || overflowX.includes("hidden")));
72-
73-
if (!node) {
74-
return null;
75-
} else if (isScrollable && node.scrollHeight >= node.clientHeight) {
76-
return node;
77-
}
78-
79-
return getScrollParent(node.parentNode) || document.body;
80-
}

src/Lumi/Components/Table.purs

+7-10
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@ import Data.Maybe (Maybe(..), fromMaybe, isJust, isNothing, maybe)
1717
import Data.Monoid (guard)
1818
import Data.Newtype (class Newtype, un)
1919
import Data.Nullable (Nullable, toMaybe)
20+
import Data.Nullable as Nullable
2021
import Data.String (contains, joinWith, Pattern(..))
2122
import Effect (Effect)
2223
import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn1, runEffectFn2)
2324
import JSS (JSS, jss)
25+
import Lumi.Components (lumiElement)
2426
import Lumi.Components.Color (colors)
2527
import Lumi.Components.Icon (IconType(ArrowUp, ArrowDown), icon_)
2628
import Lumi.Components.Input (CheckboxState(..), checkbox, input)
2729
import Lumi.Components.Link as Link
2830
import Lumi.Components.Table.FilterDropdown (Item, filterDropdown)
2931
import Lumi.Components.Text (subtext_)
3032
import Lumi.Components.ZIndex (ziTableHeader, ziTableHeaderMenu, ziTableLockedColumn, ziTableLockedColumnHeader)
31-
import React.Basic (Component, JSX, ReactComponent, createComponent, element, empty, keyed, make, readProps, readState)
33+
import Lumi.Components2.ScrollObserver (scrollObserver)
34+
import React.Basic (Component, JSX, createComponent, element, empty, keyed, make, readProps, readState)
3235
import React.Basic.DOM as R
3336
import React.Basic.DOM.Components.GlobalEvents (windowEvent)
3437
import React.Basic.DOM.Components.Ref (QuerySelector(..), selectorRef)
@@ -263,9 +266,9 @@ table = make component
263266
else self.props.columns
264267
in
265268
renderLumiTable self columns \tableRef ->
266-
[ element scrollObserver
267-
{ node: tableRef
268-
, render: \{ hasScrolledY, hasScrolledX } ->
269+
[ lumiElement scrollObserver _
270+
{ node = Nullable.notNull tableRef
271+
, content = \{ hasScrolledY, hasScrolledX } ->
269272
R.table
270273
{ className:
271274
let
@@ -612,12 +615,6 @@ foreign import isRightClick :: Event -> Boolean
612615

613616
foreign import hasWindowSelection :: Effect Boolean
614617

615-
foreign import scrollObserver
616-
:: ReactComponent
617-
{ node :: Node
618-
, render :: { hasScrolledY :: Boolean, hasScrolledX :: Boolean } -> JSX
619-
}
620-
621618
styles :: JSS
622619
styles = jss
623620
{ "@global":
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use strict";
2+
3+
// Walks up the DOM tree starting at the given node to find
4+
// the first parent with a scroll bar (including auto, which
5+
// on some devices hides the scroll bar until hovered).
6+
exports.getScrollParent = node => () => {
7+
const isElement = node instanceof HTMLElement;
8+
const computedStyle = isElement && window.getComputedStyle(node);
9+
const overflowY = computedStyle && computedStyle.overflowY;
10+
const overflowX = computedStyle && computedStyle.overflowX;
11+
const isScrollable =
12+
(overflowY &&
13+
!(overflowY.includes("visible") || overflowY.includes("hidden"))) ||
14+
(overflowX &&
15+
!(overflowX.includes("visible") || overflowX.includes("hidden")));
16+
17+
if (!node) {
18+
return document.body;
19+
} else if (isScrollable && node.scrollHeight >= node.clientHeight) {
20+
return node;
21+
} else {
22+
return getScrollParent(node.parentNode);
23+
}
24+
};
25+
26+
exports.addPassiveEventListener = type => listener => capture => target => () =>
27+
target.addEventListener(type, listener, { passive: true, capture });
28+
29+
exports.removePassiveEventListener = type => listener => capture => target => () =>
30+
target.removeEventListener(type, listener, { passive: true, capture });
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module Lumi.Components2.ScrollObserver where
2+
3+
import Prelude
4+
import Data.Newtype (class Newtype)
5+
import Data.Nullable (Nullable)
6+
import Data.Nullable as Nullable
7+
import Effect (Effect)
8+
import Effect.Unsafe (unsafePerformEffect)
9+
import Lumi.Components (LumiComponent, lumiComponent)
10+
import React.Basic (JSX)
11+
import React.Basic.Hooks (Hook, UnsafeReference(..), UseEffect, UseState, coerceHook, useEffect, useState, (/\))
12+
import React.Basic.Hooks as React
13+
import Web.DOM (Element, Node)
14+
import Web.DOM.Element (scrollLeft, scrollTop)
15+
import Web.DOM.Element as Element
16+
import Web.Event.Event (EventType(..))
17+
import Web.Event.EventTarget (EventListener, EventTarget, eventListener)
18+
19+
newtype UseScrollObserver hooks
20+
= UseScrollObserver (UseEffect (UnsafeReference (Nullable Node)) (UseState Boolean (UseState Boolean hooks)))
21+
22+
derive instance ntUseScrollObserver :: Newtype (UseScrollObserver hooks) _
23+
24+
useScrollObserver :: Nullable Node -> Hook UseScrollObserver { hasScrolledX :: Boolean, hasScrolledY :: Boolean }
25+
useScrollObserver root =
26+
coerceHook React.do
27+
hasScrolledY /\ setHasScrolledY <- useState false
28+
hasScrolledX /\ setHasScrolledX <- useState false
29+
useEffect (UnsafeReference root) do
30+
scrollParent <- getScrollParent root
31+
let
32+
onScroll = do
33+
top <- scrollTop scrollParent
34+
left <- scrollLeft scrollParent
35+
setHasScrolledY \_ -> top > 0.0
36+
setHasScrolledX \_ -> left > 0.0
37+
onScrollListener <- eventListener \_ -> onScroll
38+
onScroll
39+
Element.toEventTarget scrollParent # addPassiveEventListener (EventType "scroll") onScrollListener false
40+
pure do
41+
Element.toEventTarget scrollParent # removePassiveEventListener (EventType "scroll") onScrollListener false
42+
pure { hasScrolledY, hasScrolledX }
43+
44+
type ScrollObserverProps
45+
= ( node :: Nullable Node
46+
, content :: { hasScrolledX :: Boolean, hasScrolledY :: Boolean } -> JSX
47+
)
48+
49+
scrollObserver :: LumiComponent ScrollObserverProps
50+
scrollObserver =
51+
unsafePerformEffect do
52+
lumiComponent "ScrollObserver" defaults \props -> React.do
53+
hasScrolled <- useScrollObserver props.node
54+
pure $ props.content hasScrolled
55+
where
56+
defaults = { node: Nullable.null, content: \_ -> mempty }
57+
58+
foreign import getScrollParent :: Nullable Node -> Effect Element
59+
60+
-- | Adds a listener to an event target. The boolean argument indicates whether
61+
-- | the listener should be added for the "capture" phase.
62+
foreign import addPassiveEventListener ::
63+
EventType ->
64+
EventListener ->
65+
Boolean ->
66+
EventTarget ->
67+
Effect Unit
68+
69+
-- | Removes a listener to an event target. The boolean argument indicates
70+
-- | whether the listener should be removed for the "capture" phase.
71+
foreign import removePassiveEventListener ::
72+
EventType ->
73+
EventListener ->
74+
Boolean ->
75+
EventTarget ->
76+
Effect Unit

0 commit comments

Comments
 (0)