first commit

This commit is contained in:
Stefan Hacker
2026-04-03 09:38:48 +02:00
commit 37ad745546
47450 changed files with 3120798 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
import * as React from 'react';
import type { IScrollablePaneContext } from '../ScrollablePane/ScrollablePane.types';
import type { IStickyProps } from './Sticky.types';
import type { JSXElement } from '@fluentui/utilities';
export interface IStickyState {
isStickyTop: boolean;
isStickyBottom: boolean;
distanceFromTop?: number;
}
export declare class Sticky extends React.Component<IStickyProps, IStickyState> {
static defaultProps: IStickyProps;
static contextType: React.Context<IScrollablePaneContext>;
context: any;
private _root;
private _stickyContentTop;
private _stickyContentBottom;
private _nonStickyContent;
private _placeHolder;
private _activeElement;
private _scrollUtils;
constructor(props: IStickyProps);
get root(): HTMLDivElement | null;
get placeholder(): HTMLDivElement | null;
get stickyContentTop(): HTMLDivElement | null;
get stickyContentBottom(): HTMLDivElement | null;
get nonStickyContent(): HTMLDivElement | null;
get canStickyTop(): boolean;
get canStickyBottom(): boolean;
syncScroll: (container: HTMLElement) => void;
componentDidMount(): void;
componentWillUnmount(): void;
componentDidUpdate(prevProps: IStickyProps, prevState: IStickyState): void;
shouldComponentUpdate(nextProps: IStickyProps, nextState: IStickyState): boolean;
render(): JSXElement;
addSticky(stickyContent: HTMLDivElement): void;
resetSticky(): void;
setDistanceFromTop(container: HTMLDivElement): void;
private _getContext;
private _getContentStyles;
private _getStickyPlaceholderHeight;
private _getNonStickyPlaceholderHeightAndWidth;
private _onScrollEvent;
private _getStickyDistanceFromTop;
private _getStickyDistanceFromTopForFooter;
private _getNonStickyDistanceFromTop;
private _getBackground;
}
+315
View File
@@ -0,0 +1,315 @@
import { __extends } from "tslib";
import * as React from 'react';
import { initializeComponentRef } from '../../Utilities';
import { hiddenContentStyle } from '../../Styling';
import { ScrollablePaneContext } from '../ScrollablePane/ScrollablePane.types';
import { StickyPositionType } from './Sticky.types';
import { getScrollUtils } from './util/scroll';
import { isLessThanInRange } from './util/comparison';
// Pixels
var COMPARISON_RANGE = 1;
var Sticky = /** @class */ (function (_super) {
__extends(Sticky, _super);
function Sticky(props) {
var _this = _super.call(this, props) || this;
_this._root = React.createRef();
_this._stickyContentTop = React.createRef();
_this._stickyContentBottom = React.createRef();
_this._nonStickyContent = React.createRef();
_this._placeHolder = React.createRef();
_this.syncScroll = function (container) {
var nonStickyContent = _this.nonStickyContent;
if (nonStickyContent && _this.props.isScrollSynced) {
nonStickyContent.scrollLeft = container.scrollLeft;
}
};
_this._getContext = function () { return _this.context; };
_this._onScrollEvent = function (container, footerStickyContainer) {
var _a, _b;
if (_this.root && _this.nonStickyContent) {
var distanceFromTop = _this._getNonStickyDistanceFromTop(container);
var isStickyTop = false;
var isStickyBottom = false;
// eslint-disable-next-line no-restricted-globals
var doc = (_b = ((_a = _this._getContext().window) !== null && _a !== void 0 ? _a : window)) === null || _b === void 0 ? void 0 : _b.document;
if (_this.canStickyTop) {
var distanceToStickTop = distanceFromTop - _this._getStickyDistanceFromTop();
var containerScrollTop = container.scrollTop;
isStickyTop = isLessThanInRange(distanceToStickTop, containerScrollTop, COMPARISON_RANGE);
}
// Can sticky bottom if the scrollablePane - total sticky footer height is smaller than the sticky's distance
// from the top of the pane
if (_this.canStickyBottom && container.clientHeight - footerStickyContainer.offsetHeight <= distanceFromTop) {
isStickyBottom =
distanceFromTop - _this._scrollUtils.getScrollTopInRange(container, COMPARISON_RANGE) >=
_this._getStickyDistanceFromTopForFooter(container, footerStickyContainer);
}
if ((doc === null || doc === void 0 ? void 0 : doc.activeElement) &&
_this.nonStickyContent.contains(doc === null || doc === void 0 ? void 0 : doc.activeElement) &&
(_this.state.isStickyTop !== isStickyTop || _this.state.isStickyBottom !== isStickyBottom)) {
_this._activeElement = doc === null || doc === void 0 ? void 0 : doc.activeElement;
}
else {
_this._activeElement = undefined;
}
_this.setState({
isStickyTop: _this.canStickyTop && isStickyTop,
isStickyBottom: isStickyBottom,
distanceFromTop: distanceFromTop,
});
}
};
_this._getStickyDistanceFromTop = function () {
var distance = 0;
if (_this.stickyContentTop) {
distance = _this.stickyContentTop.offsetTop;
}
return distance;
};
_this._getStickyDistanceFromTopForFooter = function (container, footerStickyVisibleContainer) {
var distance = 0;
if (_this.stickyContentBottom) {
distance =
container.clientHeight - footerStickyVisibleContainer.offsetHeight + _this.stickyContentBottom.offsetTop;
}
return distance;
};
_this._getNonStickyDistanceFromTop = function (container) {
var distance = 0;
var currElem = _this.root;
if (currElem) {
while (currElem && currElem.offsetParent !== container) {
distance += currElem.offsetTop;
currElem = currElem.offsetParent;
}
if (currElem && currElem.offsetParent === container) {
distance += currElem.offsetTop;
}
}
return distance;
};
initializeComponentRef(_this);
_this.state = {
isStickyTop: false,
isStickyBottom: false,
distanceFromTop: undefined,
};
_this._activeElement = undefined;
_this._scrollUtils = getScrollUtils();
return _this;
}
Object.defineProperty(Sticky.prototype, "root", {
get: function () {
return this._root.current;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Sticky.prototype, "placeholder", {
get: function () {
return this._placeHolder.current;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Sticky.prototype, "stickyContentTop", {
get: function () {
return this._stickyContentTop.current;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Sticky.prototype, "stickyContentBottom", {
get: function () {
return this._stickyContentBottom.current;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Sticky.prototype, "nonStickyContent", {
get: function () {
return this._nonStickyContent.current;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Sticky.prototype, "canStickyTop", {
get: function () {
return (this.props.stickyPosition === StickyPositionType.Both || this.props.stickyPosition === StickyPositionType.Header);
},
enumerable: false,
configurable: true
});
Object.defineProperty(Sticky.prototype, "canStickyBottom", {
get: function () {
return (this.props.stickyPosition === StickyPositionType.Both || this.props.stickyPosition === StickyPositionType.Footer);
},
enumerable: false,
configurable: true
});
Sticky.prototype.componentDidMount = function () {
var scrollablePane = this._getContext().scrollablePane;
if (!scrollablePane) {
return;
}
scrollablePane.subscribe(this._onScrollEvent);
scrollablePane.addSticky(this);
};
Sticky.prototype.componentWillUnmount = function () {
var scrollablePane = this._getContext().scrollablePane;
if (!scrollablePane) {
return;
}
scrollablePane.unsubscribe(this._onScrollEvent);
scrollablePane.removeSticky(this);
};
Sticky.prototype.componentDidUpdate = function (prevProps, prevState) {
var scrollablePane = this._getContext().scrollablePane;
if (!scrollablePane) {
return;
}
var _a = this.state, isStickyBottom = _a.isStickyBottom, isStickyTop = _a.isStickyTop, distanceFromTop = _a.distanceFromTop;
var syncScroll = false;
if (prevState.distanceFromTop !== distanceFromTop) {
scrollablePane.sortSticky(this, true /*sortAgain*/);
syncScroll = true;
}
if (prevState.isStickyTop !== isStickyTop || prevState.isStickyBottom !== isStickyBottom) {
if (this._activeElement) {
this._activeElement.focus();
}
scrollablePane.updateStickyRefHeights();
syncScroll = true;
}
if (syncScroll) {
// Sync Sticky scroll position with content container on each update
scrollablePane.syncScrollSticky(this);
}
};
Sticky.prototype.shouldComponentUpdate = function (nextProps, nextState) {
if (!this.context.scrollablePane) {
return true;
}
var _a = this.state, isStickyTop = _a.isStickyTop, isStickyBottom = _a.isStickyBottom, distanceFromTop = _a.distanceFromTop;
return (isStickyTop !== nextState.isStickyTop ||
isStickyBottom !== nextState.isStickyBottom ||
this.props.stickyPosition !== nextProps.stickyPosition ||
this.props.children !== nextProps.children ||
distanceFromTop !== nextState.distanceFromTop ||
_isOffsetHeightDifferent(this._nonStickyContent, this._stickyContentTop) ||
_isOffsetHeightDifferent(this._nonStickyContent, this._stickyContentBottom) ||
_isOffsetHeightDifferent(this._nonStickyContent, this._placeHolder));
};
Sticky.prototype.render = function () {
var _a = this.state, isStickyTop = _a.isStickyTop, isStickyBottom = _a.isStickyBottom;
var _b = this.props, stickyClassName = _b.stickyClassName, children = _b.children;
if (!this.context.scrollablePane) {
return React.createElement("div", null, this.props.children);
}
return (React.createElement("div", { ref: this._root },
this.canStickyTop && (React.createElement("div", { ref: this._stickyContentTop, style: { pointerEvents: isStickyTop ? 'auto' : 'none' } },
React.createElement("div", { style: this._getStickyPlaceholderHeight(isStickyTop) }))),
this.canStickyBottom && (React.createElement("div", { ref: this._stickyContentBottom, style: { pointerEvents: isStickyBottom ? 'auto' : 'none' } },
React.createElement("div", { style: this._getStickyPlaceholderHeight(isStickyBottom) }))),
React.createElement("div", { style: this._getNonStickyPlaceholderHeightAndWidth(), ref: this._placeHolder },
(isStickyTop || isStickyBottom) && React.createElement("span", { style: hiddenContentStyle }, children),
React.createElement("div", { ref: this._nonStickyContent, className: isStickyTop || isStickyBottom ? stickyClassName : undefined, style: this._getContentStyles(isStickyTop || isStickyBottom) }, children))));
};
Sticky.prototype.addSticky = function (stickyContent) {
if (this.nonStickyContent) {
stickyContent.appendChild(this.nonStickyContent);
}
};
Sticky.prototype.resetSticky = function () {
if (this.nonStickyContent && this.placeholder) {
this.placeholder.appendChild(this.nonStickyContent);
}
};
Sticky.prototype.setDistanceFromTop = function (container) {
var distanceFromTop = this._getNonStickyDistanceFromTop(container);
this.setState({ distanceFromTop: distanceFromTop });
};
Sticky.prototype._getContentStyles = function (isSticky) {
return {
backgroundColor: this.props.stickyBackgroundColor || this._getBackground(),
overflow: isSticky ? 'hidden' : '',
};
};
Sticky.prototype._getStickyPlaceholderHeight = function (isSticky) {
var height = this.nonStickyContent ? this.nonStickyContent.offsetHeight : 0;
return {
visibility: isSticky ? 'hidden' : 'visible',
height: isSticky ? 0 : height,
};
};
Sticky.prototype._getNonStickyPlaceholderHeightAndWidth = function () {
var _a = this.state, isStickyTop = _a.isStickyTop, isStickyBottom = _a.isStickyBottom;
if (isStickyTop || isStickyBottom) {
var height = 0;
var width = 0;
// Why is placeholder width needed?
// ScrollablePane's content container is reponsible for providing scrollbars depending on content overflow.
// - If the overflow is caused by content of sticky component when it is in non-sticky state, the container will
// provide horizontal scrollbar.
// - If the component becomes sticky, i.e., when state.isStickyTop || state.isStickyBottom becomes true,
// its actual content is no longer inside the container, so the container will see no need for horizontal
// scrollbar (assuming no other content is causing overflow). The complete content of sticky component will
// not be viewable. So it is necessary to provide a placeholder of a certain width (height is already being set)
// in the container, to get a horizontal scrollbar & be able to view the complete content of sticky component.
if (this.nonStickyContent && this.nonStickyContent.firstElementChild) {
height = this.nonStickyContent.offsetHeight;
// What value should be substituted for placeholder width?
// Assumptions:
// 1. Content inside <Sticky> should always be wrapped in a single div.
// <Sticky><div id={'firstElementChild'}>{intended_content}</div><Sticky/>
// 2. -ve padding, margin, etc. are not be used.
// 3. scrollWidth of a parent is greater than or equal to max of scrollWidths of its children, and same holds
// for children.
// placeholder width should be computed in the best possible way to prevent overscroll/underscroll.
width =
this.nonStickyContent.firstElementChild.scrollWidth +
(this.nonStickyContent.firstElementChild.offsetWidth -
this.nonStickyContent.firstElementChild.clientWidth);
}
return {
height: height,
width: width,
};
}
else {
return {};
}
};
// Gets background of nearest parent element that has a declared background-color attribute
Sticky.prototype._getBackground = function () {
var _a;
if (!this.root) {
return undefined;
}
var curr = this.root;
// eslint-disable-next-line no-restricted-globals
var win = (_a = this._getContext().window) !== null && _a !== void 0 ? _a : window;
while (win.getComputedStyle(curr).getPropertyValue('background-color') === 'rgba(0, 0, 0, 0)' ||
win.getComputedStyle(curr).getPropertyValue('background-color') === 'transparent') {
if (curr.tagName === 'HTML') {
// Fallback color if no element has a declared background-color attribute
return undefined;
}
if (curr.parentElement) {
curr = curr.parentElement;
}
}
return win.getComputedStyle(curr).getPropertyValue('background-color');
};
Sticky.defaultProps = {
stickyPosition: StickyPositionType.Both,
isScrollSynced: true,
};
Sticky.contextType = ScrollablePaneContext;
return Sticky;
}(React.Component));
export { Sticky };
function _isOffsetHeightDifferent(a, b) {
return (a && b && a.current && b.current && a.current.offsetHeight !== b.current.offsetHeight);
}
//# sourceMappingURL=Sticky.js.map
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
import { Sticky } from './Sticky';
import type { IReactProps, IRefObject } from '../../Utilities';
export interface IStickyProps extends IReactProps<Sticky> {
/**
* Gets ref to component interface.
*/
componentRef?: IRefObject<IStickyProps>;
/**
* Class name to apply to the sticky element if component is sticky.
*/
stickyClassName?: string;
/**
* color to apply as 'background-color' style for sticky element.
*/
stickyBackgroundColor?: string;
/**
* Region to render sticky component in.
* @defaultvalue Both
*/
stickyPosition?: StickyPositionType;
/**
* If true, then match scrolling position of placeholder element in Sticky.
* @defaultvalue true
*/
isScrollSynced?: boolean;
}
export declare enum StickyPositionType {
Both = 0,
Header = 1,
Footer = 2
}
+7
View File
@@ -0,0 +1,7 @@
export var StickyPositionType;
(function (StickyPositionType) {
StickyPositionType[StickyPositionType["Both"] = 0] = "Both";
StickyPositionType[StickyPositionType["Header"] = 1] = "Header";
StickyPositionType[StickyPositionType["Footer"] = 2] = "Footer";
})(StickyPositionType || (StickyPositionType = {}));
//# sourceMappingURL=Sticky.types.js.map
@@ -0,0 +1 @@
{"version":3,"file":"Sticky.types.js","sourceRoot":"../src/","sources":["components/Sticky/Sticky.types.ts"],"names":[],"mappings":"AAgCA,MAAM,CAAN,IAAY,kBAIX;AAJD,WAAY,kBAAkB;IAC5B,2DAAQ,CAAA;IACR,+DAAU,CAAA;IACV,+DAAU,CAAA;AACZ,CAAC,EAJW,kBAAkB,KAAlB,kBAAkB,QAI7B","sourcesContent":["import { Sticky } from './Sticky';\nimport type { IReactProps, IRefObject } from '../../Utilities';\n\nexport interface IStickyProps extends IReactProps<Sticky> {\n /**\n * Gets ref to component interface.\n */\n componentRef?: IRefObject<IStickyProps>;\n\n /**\n * Class name to apply to the sticky element if component is sticky.\n */\n stickyClassName?: string;\n\n /**\n * color to apply as 'background-color' style for sticky element.\n */\n stickyBackgroundColor?: string;\n\n /**\n * Region to render sticky component in.\n * @defaultvalue Both\n */\n stickyPosition?: StickyPositionType;\n\n /**\n * If true, then match scrolling position of placeholder element in Sticky.\n * @defaultvalue true\n */\n isScrollSynced?: boolean;\n}\n\nexport enum StickyPositionType {\n Both = 0,\n Header = 1,\n Footer = 2,\n}\n"]}
+2
View File
@@ -0,0 +1,2 @@
export * from './Sticky';
export * from './Sticky.types';
+3
View File
@@ -0,0 +1,3 @@
export * from './Sticky';
export * from './Sticky.types';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"../src/","sources":["components/Sticky/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC","sourcesContent":["export * from './Sticky';\nexport * from './Sticky.types';\n"]}
@@ -0,0 +1 @@
export declare const isLessThanInRange: (a: number, b: number, range: number) => boolean;
@@ -0,0 +1,8 @@
var inRange = function (a, b, range) {
var r = range < 0 ? 0 : range;
return Math.abs(a - b) <= r;
};
export var isLessThanInRange = function (a, b, range) {
return a < b && !inRange(a, b, range);
};
//# sourceMappingURL=comparison.js.map
@@ -0,0 +1 @@
{"version":3,"file":"comparison.js","sourceRoot":"../src/","sources":["components/Sticky/util/comparison.ts"],"names":[],"mappings":"AAAA,IAAM,OAAO,GAAG,UAAC,CAAS,EAAE,CAAS,EAAE,KAAa;IAClD,IAAM,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAChC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC,CAAC;AAEF,MAAM,CAAC,IAAM,iBAAiB,GAAG,UAAC,CAAS,EAAE,CAAS,EAAE,KAAa;IACnE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;AACxC,CAAC,CAAC","sourcesContent":["const inRange = (a: number, b: number, range: number): boolean => {\n const r = range < 0 ? 0 : range;\n return Math.abs(a - b) <= r;\n};\n\nexport const isLessThanInRange = (a: number, b: number, range: number): boolean => {\n return a < b && !inRange(a, b, range);\n};\n"]}
+5
View File
@@ -0,0 +1,5 @@
export type GetScrollTopInRange = (el: HTMLElement, range: number) => number;
export type ScrollUtils = {
getScrollTopInRange: GetScrollTopInRange;
};
export declare const getScrollUtils: () => ScrollUtils;
+17
View File
@@ -0,0 +1,17 @@
export var getScrollUtils = function () {
var scrollTopElements = new Map();
var getScrollTopInRange = function (el, range) {
var _a;
var currentScrollTop = el.scrollTop;
var prevScrollTop = (_a = scrollTopElements.get(el)) !== null && _a !== void 0 ? _a : NaN;
if (prevScrollTop - range <= currentScrollTop && prevScrollTop + range >= currentScrollTop) {
return prevScrollTop;
}
scrollTopElements.set(el, currentScrollTop);
return currentScrollTop;
};
return {
getScrollTopInRange: getScrollTopInRange,
};
};
//# sourceMappingURL=scroll.js.map
@@ -0,0 +1 @@
{"version":3,"file":"scroll.js","sourceRoot":"../src/","sources":["components/Sticky/util/scroll.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,IAAM,cAAc,GAAsB;IAC/C,IAAM,iBAAiB,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEzD,IAAM,mBAAmB,GAAwB,UAAC,EAAE,EAAE,KAAK;;QACzD,IAAM,gBAAgB,GAAG,EAAE,CAAC,SAAS,CAAC;QACtC,IAAM,aAAa,GAAG,MAAA,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,mCAAI,GAAG,CAAC;QAEvD,IAAI,aAAa,GAAG,KAAK,IAAI,gBAAgB,IAAI,aAAa,GAAG,KAAK,IAAI,gBAAgB,EAAE,CAAC;YAC3F,OAAO,aAAa,CAAC;QACvB,CAAC;QAED,iBAAiB,CAAC,GAAG,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;QAC5C,OAAO,gBAAgB,CAAC;IAC1B,CAAC,CAAC;IAEF,OAAO;QACL,mBAAmB,qBAAA;KACpB,CAAC;AACJ,CAAC,CAAC","sourcesContent":["export type GetScrollTopInRange = (el: HTMLElement, range: number) => number;\n\nexport type ScrollUtils = {\n getScrollTopInRange: GetScrollTopInRange;\n};\n\nexport const getScrollUtils: () => ScrollUtils = () => {\n const scrollTopElements = new Map<HTMLElement, number>();\n\n const getScrollTopInRange: GetScrollTopInRange = (el, range) => {\n const currentScrollTop = el.scrollTop;\n const prevScrollTop = scrollTopElements.get(el) ?? NaN;\n\n if (prevScrollTop - range <= currentScrollTop && prevScrollTop + range >= currentScrollTop) {\n return prevScrollTop;\n }\n\n scrollTopElements.set(el, currentScrollTop);\n return currentScrollTop;\n };\n\n return {\n getScrollTopInRange,\n };\n};\n"]}