@khanacademy/wonder-blocks-layout 3.1.62 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/components/scrollable-view.d.ts +64 -0
- package/dist/es/index.js +7 -2
- package/dist/hooks/use-scroll-overflow.d.ts +44 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-layout
|
|
2
2
|
|
|
3
|
+
## 3.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8b93331: Add `ScrollableView`, a view that scrolls its content vertically when it doesn't fit (e.g. on small screens or at high zoom levels). It fades the content out at the edges while there's more to scroll to, and becomes keyboard focusable while it overflows.
|
|
8
|
+
|
|
3
9
|
## 3.1.62
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { AriaProps, StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
/**
|
|
4
|
+
* A view that scrolls its content vertically when it doesn't fit in the
|
|
5
|
+
* available space (e.g. on small screens or at high zoom levels).
|
|
6
|
+
*
|
|
7
|
+
* When its content overflows:
|
|
8
|
+
* - The content fades out at the top and/or bottom edges to hint that there's
|
|
9
|
+
* more content to scroll to in that direction.
|
|
10
|
+
* - The view becomes keyboard focusable, so keyboard users can scroll it too.
|
|
11
|
+
* It doesn't add an extra tab stop when everything fits.
|
|
12
|
+
*
|
|
13
|
+
* The fades blend the content into the default background color
|
|
14
|
+
* (`semanticColor.core.background.base.default`), so the view should be
|
|
15
|
+
* placed on that background.
|
|
16
|
+
*
|
|
17
|
+
* ### Usage
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import {ScrollableView} from "@khanacademy/wonder-blocks-layout";
|
|
21
|
+
*
|
|
22
|
+
* <ScrollableView style={{maxBlockSize: 200}}>
|
|
23
|
+
* <View style={{flexShrink: 0}}>Some long content</View>
|
|
24
|
+
* </ScrollableView>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare const ScrollableView: React.ForwardRefExoticComponent<Omit<AriaProps, "aria-label" | "aria-labelledby"> & {
|
|
28
|
+
/**
|
|
29
|
+
* An accessible name for the view. Since the view becomes keyboard
|
|
30
|
+
* focusable while its content overflows, give it a name that describes
|
|
31
|
+
* its content, so screen readers can announce it when it's focused.
|
|
32
|
+
*
|
|
33
|
+
* While the content overflows, a labeled view is exposed as a `region`.
|
|
34
|
+
*/
|
|
35
|
+
"aria-label"?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The id of an element that labels the view (an alternative to
|
|
38
|
+
* `aria-label`).
|
|
39
|
+
*/
|
|
40
|
+
"aria-labelledby"?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The content to render inside the scrollable view.
|
|
43
|
+
*
|
|
44
|
+
* NOTE: The children should keep their natural size (e.g. with
|
|
45
|
+
* `flexShrink: 0`) so the content overflows and scrolls, instead of being
|
|
46
|
+
* squashed to fit the view.
|
|
47
|
+
*/
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Custom styles applied to the scrollable view.
|
|
51
|
+
*
|
|
52
|
+
* NOTE: Avoid setting `gap` on the view itself. The fades are drawn with
|
|
53
|
+
* `::before`/`::after` pseudo-elements, which are laid out as the view's
|
|
54
|
+
* first and last flex items, so a `gap` would also be added between them
|
|
55
|
+
* and the content. Put the layout styles (e.g. `gap`) on a child that
|
|
56
|
+
* wraps the content instead.
|
|
57
|
+
*/
|
|
58
|
+
style?: StyleType;
|
|
59
|
+
/**
|
|
60
|
+
* Test ID used for e2e testing.
|
|
61
|
+
*/
|
|
62
|
+
testId?: string;
|
|
63
|
+
} & React.RefAttributes<HTMLElement>>;
|
|
64
|
+
export default ScrollableView;
|
package/dist/es/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { InitialFallback, View } from '@khanacademy/wonder-blocks-core';
|
|
4
|
-
import { remToPx, tokenValue, sizing } from '@khanacademy/wonder-blocks-tokens';
|
|
4
|
+
import { remToPx, tokenValue, sizing, semanticColor } from '@khanacademy/wonder-blocks-tokens';
|
|
5
5
|
import { StyleSheet } from 'aphrodite';
|
|
6
|
+
import { focusStyles } from '@khanacademy/wonder-blocks-styles';
|
|
6
7
|
|
|
7
8
|
const VALID_MEDIA_SIZES=["small","medium","large"];const SIZE_160_PX=parseInt(remToPx(tokenValue(sizing.size_160)));const SIZE_240_PX=parseInt(remToPx(tokenValue(sizing.size_240)));const SIZE_320_PX=parseInt(remToPx(tokenValue(sizing.size_320)));const SIZE_480_PX=parseInt(remToPx(tokenValue(sizing.size_480)));const mediaDefaultSpecLargeMarginWidth=SIZE_240_PX;const MEDIA_DEFAULT_SPEC={small:{query:"(max-width: 767px)",totalColumns:4,gutterWidth:SIZE_160_PX,marginWidth:SIZE_160_PX},medium:{query:"(min-width: 768px) and (max-width: 1023px)",totalColumns:8,gutterWidth:SIZE_320_PX,marginWidth:SIZE_240_PX},large:{query:"(min-width: 1024px)",totalColumns:12,gutterWidth:SIZE_320_PX,marginWidth:mediaDefaultSpecLargeMarginWidth,maxWidth:1120+mediaDefaultSpecLargeMarginWidth*2}};const MEDIA_INTERNAL_SPEC={large:{query:"(min-width: 1px)",totalColumns:12,gutterWidth:SIZE_320_PX,marginWidth:SIZE_160_PX}};const MEDIA_MODAL_SPEC={small:{query:"(max-width: 767px)",totalColumns:4,gutterWidth:SIZE_160_PX,marginWidth:SIZE_160_PX},large:{query:"(min-width: 768px)",totalColumns:12,gutterWidth:SIZE_320_PX,marginWidth:SIZE_480_PX}};
|
|
8
9
|
|
|
@@ -10,10 +11,14 @@ const defaultContext={ssrSize:"large",mediaSpec:MEDIA_DEFAULT_SPEC};const MediaL
|
|
|
10
11
|
|
|
11
12
|
const mediaQueryLists={};const DEFAULT_SIZE="large";class MediaLayoutInternal extends React.Component{componentDidMount(){const entries=Object.entries(this.props.mediaSpec);for(const[size,spec]of entries){const mql=mediaQueryLists[spec.query];if(!mql){continue}const listener=e=>{if(e.matches){this.setState({size});}};mql.addListener(listener);this.cleanupThunks.push(()=>mql.removeListener(listener));}}componentWillUnmount(){this.cleanupThunks.forEach(cleaup=>cleaup());}getCurrentSize(spec){if(this.state.size){return this.state.size}else {const entries=Object.entries(this.props.mediaSpec);for(const[size,spec]of entries){const mql=mediaQueryLists[spec.query];if(mql.matches){return size}}}return DEFAULT_SIZE}isUnsupportedEnvironment(){return typeof window==="undefined"||!window.matchMedia}getMockStyleSheet(mediaSize){const{styleSheets}=this.props;const mockStyleSheet={};if(!styleSheets){return mockStyleSheet}for(const styleSize of Object.keys(styleSheets)){const styleSheet=styleSheets[styleSize];if(!styleSheet){continue}for(const name of Object.keys(styleSheet)){if(Object.prototype.hasOwnProperty.call(mockStyleSheet,name)){continue}mockStyleSheet[name]=[styleSheets.all&&styleSheets.all[name],mediaSize==="small"&&[styleSheets.mdOrSmaller&&styleSheets.mdOrSmaller[name],styleSheets.small&&styleSheets.small[name]],mediaSize==="medium"&&[styleSheets.mdOrSmaller&&styleSheets.mdOrSmaller[name],styleSheets.mdOrLarger&&styleSheets.mdOrLarger[name],styleSheets.medium&&styleSheets.medium[name]],mediaSize==="large"&&[styleSheets.mdOrLarger&&styleSheets.mdOrLarger[name],styleSheets.large&&styleSheets.large[name]]];}}return mockStyleSheet}renderContent(initialRender){const{children,mediaSpec,ssrSize,overrideSize}=this.props;const queries=[...Object.values(MEDIA_DEFAULT_SPEC).map(spec=>spec.query),...Object.values(MEDIA_INTERNAL_SPEC).map(spec=>spec.query),...Object.values(MEDIA_MODAL_SPEC).map(spec=>spec.query),...Object.values(mediaSpec).map(spec=>spec.query)];if(!initialRender){for(const query of queries.filter(query=>!mediaQueryLists[query])){mediaQueryLists[query]=window.matchMedia(query);}}const mediaSize=overrideSize||initialRender&&ssrSize||this.getCurrentSize(mediaSpec);const styles=this.getMockStyleSheet(mediaSize);return children({mediaSize,mediaSpec,styles})}render(){return jsx(InitialFallback,{fallback:()=>this.renderContent(true),children:()=>this.renderContent(this.isUnsupportedEnvironment())})}constructor(props){super(props);this.state={size:undefined};this.cleanupThunks=[];}}class MediaLayout extends React.Component{render(){return jsx(MediaLayoutContext.Consumer,{children:({overrideSize,ssrSize,mediaSpec})=>jsx(MediaLayoutInternal,{...this.props,overrideSize:overrideSize,ssrSize:ssrSize,mediaSpec:mediaSpec})})}}
|
|
12
13
|
|
|
14
|
+
const initialState={isOverflowingVertically:false,hasContentAbove:false,hasContentBelow:false};function useScrollOverflow(element){const[state,setState]=React.useState(initialState);React.useEffect(()=>{if(!element||!window.ResizeObserver){setState(initialState);return}const update=()=>{const{scrollTop,scrollHeight,clientHeight}=element;const next={isOverflowingVertically:scrollHeight-clientHeight>1,hasContentAbove:scrollTop>0,hasContentBelow:scrollTop+clientHeight<scrollHeight-1};setState(prev=>prev.isOverflowingVertically===next.isOverflowingVertically&&prev.hasContentAbove===next.hasContentAbove&&prev.hasContentBelow===next.hasContentBelow?prev:next);};const resizeObserver=new ResizeObserver(update);resizeObserver.observe(element);Array.from(element.children).forEach(child=>resizeObserver.observe(child));const mutationObserver=new MutationObserver(mutations=>{mutations.forEach(mutation=>mutation.addedNodes.forEach(node=>{if(node instanceof Element){resizeObserver.observe(node);}}));update();});mutationObserver.observe(element,{childList:true});element.addEventListener("scroll",update,{passive:true});update();return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();element.removeEventListener("scroll",update);}},[element]);return state}
|
|
15
|
+
|
|
16
|
+
const ScrollableView=React.forwardRef(function ScrollableView({children,role,style,testId,...ariaProps},forwardedRef){const[element,setElement]=React.useState(null);const{isOverflowingVertically,hasContentAbove,hasContentBelow}=useScrollOverflow(element);const[isFocused,setIsFocused]=React.useState(false);const handleRef=React.useCallback(node=>{setElement(node);if(typeof forwardedRef==="function"){forwardedRef(node);}else if(forwardedRef){forwardedRef.current=node;}},[forwardedRef]);const isFocusable=isOverflowingVertically||isFocused;React.useLayoutEffect(()=>{element?.style.setProperty(FADE_TOP_OPACITY,hasContentAbove?"1":"0");element?.style.setProperty(FADE_BOTTOM_OPACITY,hasContentBelow?"1":"0");},[element,hasContentAbove,hasContentBelow]);return jsx(View,{...ariaProps,role:role??(isFocusable&&(ariaProps["aria-label"]||ariaProps["aria-labelledby"])?"region":undefined),ref:handleRef,testId:testId,style:[styles$1.scrollableView,isOverflowingVertically&&styles$1.fades,style],tabIndex:isFocusable?0:undefined,onFocus:event=>{if(event.target===event.currentTarget){setIsFocused(true);}},onBlur:event=>{if(event.target===event.currentTarget){setIsFocused(false);}},children:children})});ScrollableView.displayName="ScrollableView";const FADE_TOP_OPACITY="--wb-scrollable-view-fade-top-opacity";const FADE_BOTTOM_OPACITY="--wb-scrollable-view-fade-bottom-opacity";const fadeColor=semanticColor.core.background.base.default;const fadeSize=sizing.size_480;const fadeOverlay={content:"''",display:"block",position:"sticky",flexShrink:0,blockSize:fadeSize,pointerEvents:"none",zIndex:1};const styles$1=StyleSheet.create({scrollableView:{overflowY:"auto",overflowAnchor:"none",scrollPaddingBlock:fadeSize,...focusStyles.focus},fades:{"::before":{...fadeOverlay,insetBlockStart:0,marginBlockEnd:`calc(-1 * ${fadeSize})`,background:`linear-gradient(to bottom, ${fadeColor}, transparent)`,borderStartStartRadius:"inherit",borderStartEndRadius:"inherit",opacity:`var(${FADE_TOP_OPACITY}, 0)`},"::after":{...fadeOverlay,insetBlockEnd:0,marginBlockStart:`calc(-1 * ${fadeSize})`,background:`linear-gradient(to top, ${fadeColor}, transparent)`,borderEndStartRadius:"inherit",borderEndEndRadius:"inherit",opacity:`var(${FADE_BOTTOM_OPACITY}, 0)`}}});
|
|
17
|
+
|
|
13
18
|
class Spring extends React.Component{render(){const{style}=this.props;return jsx(View,{"aria-hidden":"true",style:[styles.grow,style]})}}const styles=StyleSheet.create({grow:{flexGrow:1}});
|
|
14
19
|
|
|
15
20
|
class Strut extends React.Component{render(){const{size,style}=this.props;return jsx(View,{"aria-hidden":"true",style:[strutStyle(size),style]})}}const strutStyle=size=>{return {width:size,MsFlexBasis:size,MsFlexPreferredSize:size,WebkitFlexBasis:size,flexBasis:size,flexShrink:0}};
|
|
16
21
|
|
|
17
22
|
const queryMatchesSize=(mediaQuery,mediaSize)=>{switch(mediaQuery){case"all":return true;case"small":return mediaSize==="small";case"mdOrSmaller":return mediaSize==="medium"||mediaSize==="small";case"medium":return mediaSize==="medium";case"mdOrLarger":return mediaSize==="medium"||mediaSize==="large";case"large":return mediaSize==="large";default:throw new Error(`Unsupported mediaSize: ${mediaSize}`)}};
|
|
18
23
|
|
|
19
|
-
export { MEDIA_DEFAULT_SPEC, MEDIA_INTERNAL_SPEC, MEDIA_MODAL_SPEC, MediaLayout, MediaLayoutContext, Spring, Strut, VALID_MEDIA_SIZES, queryMatchesSize };
|
|
24
|
+
export { MEDIA_DEFAULT_SPEC, MEDIA_INTERNAL_SPEC, MEDIA_MODAL_SPEC, MediaLayout, MediaLayoutContext, ScrollableView, Spring, Strut, VALID_MEDIA_SIZES, queryMatchesSize };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type ScrollOverflow = {
|
|
2
|
+
/**
|
|
3
|
+
* Whether the element's content overflows its block size (i.e. whether
|
|
4
|
+
* it can be scrolled vertically).
|
|
5
|
+
*/
|
|
6
|
+
isOverflowingVertically: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether there's content above the visible area (the element is scrolled
|
|
9
|
+
* down from the top).
|
|
10
|
+
*/
|
|
11
|
+
hasContentAbove: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether there's content below the visible area (the element isn't
|
|
14
|
+
* scrolled all the way to the bottom).
|
|
15
|
+
*/
|
|
16
|
+
hasContentBelow: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Tracks whether the element's content overflows vertically, and whether there
|
|
20
|
+
* is content to scroll to above and/or below the visible area.
|
|
21
|
+
*
|
|
22
|
+
* The check runs right away, again when the element is scrolled, whenever the
|
|
23
|
+
* element or any of its direct children are resized, and whenever direct
|
|
24
|
+
* children are added or removed.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: CSS scroll-driven animations (`animation-timeline: scroll()`) could
|
|
27
|
+
* replace the scroll tracking, but they're not supported in all the browsers
|
|
28
|
+
* we support yet (e.g. Safari < 26).
|
|
29
|
+
*
|
|
30
|
+
* NOTE: It only handles vertical overflow.
|
|
31
|
+
*
|
|
32
|
+
* `ScrollableView` uses this hook to show fades at the edges of its content.
|
|
33
|
+
*
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // Store the element in state (e.g. via a callback ref), so the hook
|
|
36
|
+
* // re-attaches if the element mounts later or changes.
|
|
37
|
+
* const [element, setElement] = React.useState<HTMLElement | null>(null);
|
|
38
|
+
* const {isOverflowingVertically, hasContentAbove, hasContentBelow} =
|
|
39
|
+
* useScrollOverflow(element);
|
|
40
|
+
*
|
|
41
|
+
* <View ref={setElement}>...</View>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function useScrollOverflow(element: HTMLElement | null): ScrollOverflow;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Context } from "./components/media-layout-context";
|
|
|
3
3
|
import type { MockStyleSheet } from "./components/media-layout";
|
|
4
4
|
export { default as MediaLayout } from "./components/media-layout";
|
|
5
5
|
export { default as MediaLayoutContext } from "./components/media-layout-context";
|
|
6
|
+
export { default as ScrollableView } from "./components/scrollable-view";
|
|
6
7
|
export { default as Spring } from "./components/spring";
|
|
7
8
|
export { default as Strut } from "./components/strut";
|
|
8
9
|
export * from "./util/specs";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var React = require('react');
|
|
|
7
7
|
var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
|
|
8
8
|
var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
|
|
9
9
|
var aphrodite = require('aphrodite');
|
|
10
|
+
var wonderBlocksStyles = require('@khanacademy/wonder-blocks-styles');
|
|
10
11
|
|
|
11
12
|
function _interopNamespace(e) {
|
|
12
13
|
if (e && e.__esModule) return e;
|
|
@@ -34,6 +35,10 @@ const defaultContext={ssrSize:"large",mediaSpec:MEDIA_DEFAULT_SPEC};const MediaL
|
|
|
34
35
|
|
|
35
36
|
const mediaQueryLists={};const DEFAULT_SIZE="large";class MediaLayoutInternal extends React__namespace.Component{componentDidMount(){const entries=Object.entries(this.props.mediaSpec);for(const[size,spec]of entries){const mql=mediaQueryLists[spec.query];if(!mql){continue}const listener=e=>{if(e.matches){this.setState({size});}};mql.addListener(listener);this.cleanupThunks.push(()=>mql.removeListener(listener));}}componentWillUnmount(){this.cleanupThunks.forEach(cleaup=>cleaup());}getCurrentSize(spec){if(this.state.size){return this.state.size}else {const entries=Object.entries(this.props.mediaSpec);for(const[size,spec]of entries){const mql=mediaQueryLists[spec.query];if(mql.matches){return size}}}return DEFAULT_SIZE}isUnsupportedEnvironment(){return typeof window==="undefined"||!window.matchMedia}getMockStyleSheet(mediaSize){const{styleSheets}=this.props;const mockStyleSheet={};if(!styleSheets){return mockStyleSheet}for(const styleSize of Object.keys(styleSheets)){const styleSheet=styleSheets[styleSize];if(!styleSheet){continue}for(const name of Object.keys(styleSheet)){if(Object.prototype.hasOwnProperty.call(mockStyleSheet,name)){continue}mockStyleSheet[name]=[styleSheets.all&&styleSheets.all[name],mediaSize==="small"&&[styleSheets.mdOrSmaller&&styleSheets.mdOrSmaller[name],styleSheets.small&&styleSheets.small[name]],mediaSize==="medium"&&[styleSheets.mdOrSmaller&&styleSheets.mdOrSmaller[name],styleSheets.mdOrLarger&&styleSheets.mdOrLarger[name],styleSheets.medium&&styleSheets.medium[name]],mediaSize==="large"&&[styleSheets.mdOrLarger&&styleSheets.mdOrLarger[name],styleSheets.large&&styleSheets.large[name]]];}}return mockStyleSheet}renderContent(initialRender){const{children,mediaSpec,ssrSize,overrideSize}=this.props;const queries=[...Object.values(MEDIA_DEFAULT_SPEC).map(spec=>spec.query),...Object.values(MEDIA_INTERNAL_SPEC).map(spec=>spec.query),...Object.values(MEDIA_MODAL_SPEC).map(spec=>spec.query),...Object.values(mediaSpec).map(spec=>spec.query)];if(!initialRender){for(const query of queries.filter(query=>!mediaQueryLists[query])){mediaQueryLists[query]=window.matchMedia(query);}}const mediaSize=overrideSize||initialRender&&ssrSize||this.getCurrentSize(mediaSpec);const styles=this.getMockStyleSheet(mediaSize);return children({mediaSize,mediaSpec,styles})}render(){return jsxRuntime.jsx(wonderBlocksCore.InitialFallback,{fallback:()=>this.renderContent(true),children:()=>this.renderContent(this.isUnsupportedEnvironment())})}constructor(props){super(props);this.state={size:undefined};this.cleanupThunks=[];}}class MediaLayout extends React__namespace.Component{render(){return jsxRuntime.jsx(MediaLayoutContext.Consumer,{children:({overrideSize,ssrSize,mediaSpec})=>jsxRuntime.jsx(MediaLayoutInternal,{...this.props,overrideSize:overrideSize,ssrSize:ssrSize,mediaSpec:mediaSpec})})}}
|
|
36
37
|
|
|
38
|
+
const initialState={isOverflowingVertically:false,hasContentAbove:false,hasContentBelow:false};function useScrollOverflow(element){const[state,setState]=React__namespace.useState(initialState);React__namespace.useEffect(()=>{if(!element||!window.ResizeObserver){setState(initialState);return}const update=()=>{const{scrollTop,scrollHeight,clientHeight}=element;const next={isOverflowingVertically:scrollHeight-clientHeight>1,hasContentAbove:scrollTop>0,hasContentBelow:scrollTop+clientHeight<scrollHeight-1};setState(prev=>prev.isOverflowingVertically===next.isOverflowingVertically&&prev.hasContentAbove===next.hasContentAbove&&prev.hasContentBelow===next.hasContentBelow?prev:next);};const resizeObserver=new ResizeObserver(update);resizeObserver.observe(element);Array.from(element.children).forEach(child=>resizeObserver.observe(child));const mutationObserver=new MutationObserver(mutations=>{mutations.forEach(mutation=>mutation.addedNodes.forEach(node=>{if(node instanceof Element){resizeObserver.observe(node);}}));update();});mutationObserver.observe(element,{childList:true});element.addEventListener("scroll",update,{passive:true});update();return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();element.removeEventListener("scroll",update);}},[element]);return state}
|
|
39
|
+
|
|
40
|
+
const ScrollableView=React__namespace.forwardRef(function ScrollableView({children,role,style,testId,...ariaProps},forwardedRef){const[element,setElement]=React__namespace.useState(null);const{isOverflowingVertically,hasContentAbove,hasContentBelow}=useScrollOverflow(element);const[isFocused,setIsFocused]=React__namespace.useState(false);const handleRef=React__namespace.useCallback(node=>{setElement(node);if(typeof forwardedRef==="function"){forwardedRef(node);}else if(forwardedRef){forwardedRef.current=node;}},[forwardedRef]);const isFocusable=isOverflowingVertically||isFocused;React__namespace.useLayoutEffect(()=>{element?.style.setProperty(FADE_TOP_OPACITY,hasContentAbove?"1":"0");element?.style.setProperty(FADE_BOTTOM_OPACITY,hasContentBelow?"1":"0");},[element,hasContentAbove,hasContentBelow]);return jsxRuntime.jsx(wonderBlocksCore.View,{...ariaProps,role:role??(isFocusable&&(ariaProps["aria-label"]||ariaProps["aria-labelledby"])?"region":undefined),ref:handleRef,testId:testId,style:[styles$1.scrollableView,isOverflowingVertically&&styles$1.fades,style],tabIndex:isFocusable?0:undefined,onFocus:event=>{if(event.target===event.currentTarget){setIsFocused(true);}},onBlur:event=>{if(event.target===event.currentTarget){setIsFocused(false);}},children:children})});ScrollableView.displayName="ScrollableView";const FADE_TOP_OPACITY="--wb-scrollable-view-fade-top-opacity";const FADE_BOTTOM_OPACITY="--wb-scrollable-view-fade-bottom-opacity";const fadeColor=wonderBlocksTokens.semanticColor.core.background.base.default;const fadeSize=wonderBlocksTokens.sizing.size_480;const fadeOverlay={content:"''",display:"block",position:"sticky",flexShrink:0,blockSize:fadeSize,pointerEvents:"none",zIndex:1};const styles$1=aphrodite.StyleSheet.create({scrollableView:{overflowY:"auto",overflowAnchor:"none",scrollPaddingBlock:fadeSize,...wonderBlocksStyles.focusStyles.focus},fades:{"::before":{...fadeOverlay,insetBlockStart:0,marginBlockEnd:`calc(-1 * ${fadeSize})`,background:`linear-gradient(to bottom, ${fadeColor}, transparent)`,borderStartStartRadius:"inherit",borderStartEndRadius:"inherit",opacity:`var(${FADE_TOP_OPACITY}, 0)`},"::after":{...fadeOverlay,insetBlockEnd:0,marginBlockStart:`calc(-1 * ${fadeSize})`,background:`linear-gradient(to top, ${fadeColor}, transparent)`,borderEndStartRadius:"inherit",borderEndEndRadius:"inherit",opacity:`var(${FADE_BOTTOM_OPACITY}, 0)`}}});
|
|
41
|
+
|
|
37
42
|
class Spring extends React__namespace.Component{render(){const{style}=this.props;return jsxRuntime.jsx(wonderBlocksCore.View,{"aria-hidden":"true",style:[styles.grow,style]})}}const styles=aphrodite.StyleSheet.create({grow:{flexGrow:1}});
|
|
38
43
|
|
|
39
44
|
class Strut extends React__namespace.Component{render(){const{size,style}=this.props;return jsxRuntime.jsx(wonderBlocksCore.View,{"aria-hidden":"true",style:[strutStyle(size),style]})}}const strutStyle=size=>{return {width:size,MsFlexBasis:size,MsFlexPreferredSize:size,WebkitFlexBasis:size,flexBasis:size,flexShrink:0}};
|
|
@@ -45,6 +50,7 @@ exports.MEDIA_INTERNAL_SPEC = MEDIA_INTERNAL_SPEC;
|
|
|
45
50
|
exports.MEDIA_MODAL_SPEC = MEDIA_MODAL_SPEC;
|
|
46
51
|
exports.MediaLayout = MediaLayout;
|
|
47
52
|
exports.MediaLayoutContext = MediaLayoutContext;
|
|
53
|
+
exports.ScrollableView = ScrollableView;
|
|
48
54
|
exports.Spring = Spring;
|
|
49
55
|
exports.Strut = Strut;
|
|
50
56
|
exports.VALID_MEDIA_SIZES = VALID_MEDIA_SIZES;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.2.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"types": "dist/index.d.ts",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@khanacademy/wonder-blocks-core": "12.5.0",
|
|
24
|
+
"@khanacademy/wonder-blocks-styles": "0.4.0",
|
|
24
25
|
"@khanacademy/wonder-blocks-tokens": "18.0.0"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|