@txstate-mws/svelte-components 1.0.4 → 1.1.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.
Files changed (123) hide show
  1. package/.eslintrc.cjs +37 -0
  2. package/docs/CollapsingTable.md +79 -0
  3. package/docs/ConditionalWrapper.md +35 -0
  4. package/docs/Modal.md +63 -0
  5. package/docs/PopupMenu.md +31 -0
  6. package/docs/ScreenReaderOnly.md +10 -0
  7. package/docs/actions.md +84 -0
  8. package/docs/stores.md +203 -0
  9. package/package.json +15 -37
  10. package/playwright.config.js +112 -0
  11. package/src/app.html +14 -0
  12. package/src/global.d.ts +1 -0
  13. package/src/lib/actions/buttonify.ts +27 -0
  14. package/src/lib/actions/glue.ts +96 -0
  15. package/src/lib/actions/index.ts +7 -0
  16. package/src/lib/actions/offset.ts +43 -0
  17. package/src/lib/actions/portal.ts +13 -0
  18. package/src/lib/actions/resize.ts +72 -0
  19. package/src/lib/actions/sticky.ts +105 -0
  20. package/src/lib/actions/stickyfixed.ts +160 -0
  21. package/src/lib/components/Card.svelte +25 -0
  22. package/src/lib/components/CardLayout.svelte +188 -0
  23. package/{components → src/lib/components}/CollapsingTable.svelte +65 -63
  24. package/{components → src/lib/components}/ConditionalWrapper.svelte +7 -6
  25. package/src/lib/components/FocusLock.svelte +102 -0
  26. package/{components → src/lib/components}/Modal.svelte +24 -22
  27. package/src/lib/components/MultiSelect.svelte +174 -0
  28. package/src/lib/components/PopupMenu.svelte +285 -0
  29. package/{components → src/lib/components}/ScreenReaderOnly.svelte +4 -3
  30. package/src/lib/components/index.ts +9 -0
  31. package/src/lib/index.ts +4 -0
  32. package/src/lib/types/CardLayout.ts +14 -0
  33. package/src/lib/types/CollapsingTable.ts +8 -0
  34. package/src/lib/types/PopupMenu.ts +5 -0
  35. package/src/lib/types/index.ts +3 -0
  36. package/src/lib/util/bodyoffset.ts +77 -0
  37. package/src/lib/util/index.ts +3 -0
  38. package/src/lib/util/repositioned.ts +91 -0
  39. package/src/lib/util/util.ts +36 -0
  40. package/src/routes/__layout.svelte +125 -0
  41. package/src/routes/actions/index.svelte +1 -0
  42. package/src/routes/actions/offset.svelte +4 -0
  43. package/src/routes/actions/offsetdebounce.svelte +4 -0
  44. package/src/routes/actions/resize.svelte +4 -0
  45. package/src/routes/actions/resizedebounce.svelte +4 -0
  46. package/src/routes/actions/sticky.svelte +4 -0
  47. package/src/routes/cards/index.svelte +4 -0
  48. package/src/routes/cards/preserve.svelte +4 -0
  49. package/src/routes/collapsingtable/index.svelte +4 -0
  50. package/src/routes/collapsingtable/stickyheader.svelte +4 -0
  51. package/src/routes/index.svelte +2 -0
  52. package/src/routes/modal.svelte +4 -0
  53. package/src/routes/multiselect.svelte +4 -0
  54. package/src/routes/popupmenu/body.svelte +4 -0
  55. package/src/routes/popupmenu/complex.svelte +4 -0
  56. package/src/routes/popupmenu/empty.svelte +5 -0
  57. package/src/routes/popupmenu/index.svelte +4 -0
  58. package/src/routes/popupmenu/parent.svelte +4 -0
  59. package/src/routes/popupmenu/scrollable.svelte +27 -0
  60. package/src/routes/popupmenu/showselected.svelte +4 -0
  61. package/src/test/Cards.svelte +64 -0
  62. package/src/test/CollapsingTableTest.svelte +36 -0
  63. package/src/test/ModalTest.svelte +58 -0
  64. package/src/test/MultiSelectTest.svelte +26 -0
  65. package/src/test/Offset.svelte +32 -0
  66. package/src/test/PopupMenuBody.svelte +34 -0
  67. package/src/test/PopupMenuButtonIsParent.svelte +36 -0
  68. package/src/test/PopupMenuComplex.svelte +34 -0
  69. package/src/test/PopupMenuSimple.svelte +36 -0
  70. package/src/test/Resize.svelte +29 -0
  71. package/src/test/Sticky.svelte +65 -0
  72. package/static/layout.css +263 -0
  73. package/svelte.config.js +15 -0
  74. package/tests/.eslintrc.cjs +19 -0
  75. package/tests/CardLayout.test.js +41 -0
  76. package/tests/CollapsingTable.test.js +37 -0
  77. package/tests/Modal.test.js +18 -0
  78. package/tests/MultiSelect.test.js +58 -0
  79. package/tests/PopupMenu.test.js +55 -0
  80. package/tests/common.js +4 -0
  81. package/tsconfig.json +31 -0
  82. package/actions/buttonify.d.ts +0 -8
  83. package/actions/buttonify.js +0 -26
  84. package/actions/glue.d.ts +0 -16
  85. package/actions/glue.js +0 -82
  86. package/actions/index.d.ts +0 -7
  87. package/actions/index.js +0 -7
  88. package/actions/offset.d.ts +0 -15
  89. package/actions/offset.js +0 -35
  90. package/actions/portal.d.ts +0 -4
  91. package/actions/portal.js +0 -13
  92. package/actions/resize.d.ts +0 -20
  93. package/actions/resize.js +0 -45
  94. package/actions/sticky.d.ts +0 -33
  95. package/actions/sticky.js +0 -85
  96. package/actions/stickyfixed.d.ts +0 -25
  97. package/actions/stickyfixed.js +0 -157
  98. package/components/CollapsingTable.svelte.d.ts +0 -59
  99. package/components/ConditionalWrapper.svelte.d.ts +0 -23
  100. package/components/FocusLock.svelte +0 -107
  101. package/components/FocusLock.svelte.d.ts +0 -26
  102. package/components/Modal.svelte.d.ts +0 -27
  103. package/components/PopupMenu.svelte +0 -279
  104. package/components/PopupMenu.svelte.d.ts +0 -44
  105. package/components/ScreenReaderOnly.svelte.d.ts +0 -20
  106. package/components/index.d.ts +0 -6
  107. package/components/index.js +0 -6
  108. package/index.d.ts +0 -4
  109. package/index.js +0 -4
  110. package/types/CollapsingTable.d.ts +0 -8
  111. package/types/CollapsingTable.js +0 -1
  112. package/types/PopupMenu.d.ts +0 -5
  113. package/types/PopupMenu.js +0 -1
  114. package/types/index.d.ts +0 -2
  115. package/types/index.js +0 -2
  116. package/util/bodyoffset.d.ts +0 -14
  117. package/util/bodyoffset.js +0 -72
  118. package/util/index.d.ts +0 -3
  119. package/util/index.js +0 -3
  120. package/util/repositioned.d.ts +0 -12
  121. package/util/repositioned.js +0 -81
  122. package/util/util.d.ts +0 -2
  123. package/util/util.js +0 -10
@@ -0,0 +1,55 @@
1
+ import { test, expect } from '@playwright/test'
2
+ import { goto } from './common.js'
3
+
4
+ test.beforeEach(async ({ page }) => {
5
+ await goto(page, '/popupmenu')
6
+ })
7
+
8
+ test('menu toggles when button is clicked', async ({ page }) => {
9
+ const button = page.locator('#test-button')
10
+ const monday = page.locator('text="Monday"')
11
+ await expect(button).toHaveCount(1)
12
+ await expect(monday).toHaveCount(0)
13
+ button.click()
14
+ await expect(monday).toHaveCount(1)
15
+ button.click()
16
+ await expect(monday).toHaveCount(0)
17
+ button.press('Enter')
18
+ await expect(monday).toHaveCount(1)
19
+ button.press('Enter')
20
+ await expect(monday).toHaveCount(0)
21
+ })
22
+
23
+ test('has five menu items', async ({ page }) => {
24
+ const button = page.locator('#test-button')
25
+ await expect(button).toHaveCount(1)
26
+ button.click()
27
+ await expect(page.locator('text="Monday"')).toHaveCount(1)
28
+ await expect(page.locator('text="Tuesday"')).toHaveCount(1)
29
+ await expect(page.locator('text="Wednesday"')).toHaveCount(1)
30
+ await expect(page.locator('text="Thursday"')).toHaveCount(1)
31
+ await expect(page.locator('text="Friday"')).toHaveCount(1)
32
+ })
33
+
34
+ test('clicking a menu option selects it', async ({ page }) => {
35
+ const button = page.locator('#test-button')
36
+ await expect(button).toHaveCount(1)
37
+ button.click()
38
+ const option = page.locator('text="Monday"')
39
+ await expect(option).toHaveCount(1)
40
+ await expect(page.locator('text=selected: 1')).toHaveCount(0)
41
+ option.click()
42
+ await expect(page.locator('text=selected: 1')).toHaveCount(1)
43
+ })
44
+
45
+ test('may select a menu option with the keyboard', async ({ page }) => {
46
+ const button = page.locator('#test-button')
47
+ await expect(button).toHaveCount(1)
48
+ button.click()
49
+ const option = page.locator('text="Monday"')
50
+ await expect(option).toHaveCount(1)
51
+ await expect(page.locator('text=selected: 1')).toHaveCount(0)
52
+ await button.press('ArrowDown')
53
+ await button.press('Enter')
54
+ await expect(page.locator('text=selected: 1')).toHaveCount(1)
55
+ })
@@ -0,0 +1,4 @@
1
+ export async function goto (page, href) {
2
+ await page.goto(href, { waitUntil: 'commit' })
3
+ await page.evaluate(() => new Promise(resolve => window.addEventListener('sveltekit:start', resolve)))
4
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "moduleResolution": "node",
4
+ "module": "es2020",
5
+ "lib": ["es2020", "DOM"],
6
+ "target": "es2020",
7
+ /**
8
+ svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
9
+ to enforce using \`import type\` instead of \`import\` for Types.
10
+ */
11
+ "importsNotUsedAsValues": "error",
12
+ "isolatedModules": true,
13
+ "resolveJsonModule": true,
14
+ /**
15
+ To have warnings/errors of the Svelte compiler at the correct position,
16
+ enable source maps by default.
17
+ */
18
+ "sourceMap": true,
19
+ "esModuleInterop": true,
20
+ "skipLibCheck": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "baseUrl": ".",
23
+ "allowJs": true,
24
+ "checkJs": true,
25
+ "paths": {
26
+ "$lib": ["src/lib"],
27
+ "$lib/*": ["src/lib/*"]
28
+ }
29
+ },
30
+ "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
31
+ }
@@ -1,8 +0,0 @@
1
- /**
2
- * Turns any HTML element into an accessible button that responds to keyboard events and has
3
- * role="button" and the tabindex attribute set. You are still responsible for making sure it has
4
- * readable content or a good label.
5
- */
6
- export declare function buttonify(node: HTMLElement): {
7
- destroy(): void;
8
- };
@@ -1,26 +0,0 @@
1
- /**
2
- * Turns any HTML element into an accessible button that responds to keyboard events and has
3
- * role="button" and the tabindex attribute set. You are still responsible for making sure it has
4
- * readable content or a good label.
5
- */
6
- export function buttonify(node) {
7
- function keydown(e) {
8
- if (e.key === 'Enter') {
9
- if (!e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
10
- e.preventDefault();
11
- e.stopPropagation();
12
- node.dispatchEvent(new CustomEvent('click'));
13
- }
14
- }
15
- }
16
- node.addEventListener('keydown', keydown);
17
- if (node.tabIndex < 0)
18
- node.tabIndex = 0;
19
- node.setAttribute('role', 'button');
20
- node.style.cursor = 'pointer';
21
- return {
22
- destroy() {
23
- node.removeEventListener('keydown', keydown);
24
- }
25
- };
26
- }
package/actions/glue.d.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { SettableSubject } from '@txstate-mws/svelte-store';
2
- export declare type GlueAlignOpts = 'auto' | 'bottomleft' | 'bottomright' | 'topleft' | 'topright';
3
- export interface GlueArgs<T extends GlueAlignStore = GlueAlignStore> {
4
- target: HTMLElement;
5
- align?: GlueAlignOpts;
6
- cover?: boolean;
7
- store?: SettableSubject<T>;
8
- }
9
- export interface GlueAlignStore {
10
- valign: 'bottom' | 'top';
11
- halign: 'left' | 'right';
12
- }
13
- export declare function glue(el: HTMLElement, { target, align, cover, store }: GlueArgs): {
14
- update({ target: utarget, align: ualign, cover: ucover, store: ustore }: GlueArgs): void;
15
- destroy: () => void;
16
- };
package/actions/glue.js DELETED
@@ -1,82 +0,0 @@
1
- import { sharedOffsetParent, targetOffset, watchForPositionChangeInContainer } from '../util/index.js';
2
- let setBodyRelative = false;
3
- export function glue(el, { target, align = 'auto', cover = false, store }) {
4
- if (!setBodyRelative) {
5
- setBodyRelative = true;
6
- document.body.style.position = 'relative';
7
- }
8
- let halign;
9
- let valign;
10
- let sharedOffset = sharedOffsetParent(el, target);
11
- function reposition(offset) {
12
- if (!target || !sharedOffset || !(el.offsetParent instanceof HTMLElement))
13
- return;
14
- let autoalign = align;
15
- const parentoffset = el.offsetParent === sharedOffset && target !== sharedOffset ? { top: 0, left: 0, bottom: 0, right: 0 } : targetOffset(el.offsetParent, sharedOffset);
16
- if (align === 'auto') {
17
- const rect = target.getBoundingClientRect();
18
- const leftright = window.innerWidth - rect.right > rect.left ? 'left' : 'right';
19
- const topbottom = window.innerHeight - rect.bottom > rect.top ? 'bottom' : 'top';
20
- autoalign = topbottom + leftright;
21
- }
22
- const targetHeight = target.offsetHeight;
23
- if (autoalign === 'bottomleft') {
24
- requestAnimationFrame(() => {
25
- el.style.top = `${offset.top + (cover ? 0 : targetHeight) - parentoffset.top}px`;
26
- el.style.left = `${offset.left - parentoffset.left}px`;
27
- el.style.bottom = '';
28
- el.style.right = '';
29
- });
30
- valign = 'bottom';
31
- halign = 'left';
32
- }
33
- else if (autoalign === 'bottomright') {
34
- requestAnimationFrame(() => {
35
- el.style.top = `${offset.top + (cover ? 0 : targetHeight) - parentoffset.top}px`;
36
- el.style.left = '';
37
- el.style.bottom = '';
38
- el.style.right = `${offset.right - parentoffset.right}px`;
39
- });
40
- valign = 'bottom';
41
- halign = 'right';
42
- }
43
- else if (autoalign === 'topleft') {
44
- requestAnimationFrame(() => {
45
- el.style.top = '';
46
- el.style.left = `${offset.left - parentoffset.left}px`;
47
- el.style.bottom = `${offset.bottom + (cover ? 0 : targetHeight) - parentoffset.bottom}px`;
48
- el.style.right = '';
49
- });
50
- valign = 'top';
51
- halign = 'left';
52
- }
53
- else if (autoalign === 'topright') {
54
- requestAnimationFrame(() => {
55
- el.style.top = '';
56
- el.style.left = '';
57
- el.style.bottom = `${offset.bottom + (cover ? 0 : targetHeight) - parentoffset.bottom}px`;
58
- el.style.right = `${offset.right - parentoffset.right}px`;
59
- });
60
- valign = 'top';
61
- halign = 'right';
62
- }
63
- store?.update(v => ({ ...v, valign, halign }));
64
- }
65
- const { destroy, update } = watchForPositionChangeInContainer(target, sharedOffset, reposition);
66
- return {
67
- update({ target: utarget, align: ualign = 'auto', cover: ucover = false, store: ustore }) {
68
- align = ualign;
69
- cover = ucover;
70
- if (ustore && ustore !== store) {
71
- ustore.update(v => ({ ...v, valign, halign }));
72
- store = ustore;
73
- }
74
- if (target !== utarget) {
75
- sharedOffset = sharedOffsetParent(el, utarget);
76
- target = utarget;
77
- update(target, sharedOffset, reposition);
78
- }
79
- },
80
- destroy
81
- };
82
- }
@@ -1,7 +0,0 @@
1
- export * from './buttonify.js';
2
- export * from './glue.js';
3
- export * from './offset.js';
4
- export * from './portal.js';
5
- export * from './resize.js';
6
- export * from './sticky.js';
7
- export * from './stickyfixed.js';
package/actions/index.js DELETED
@@ -1,7 +0,0 @@
1
- export * from './buttonify.js';
2
- export * from './glue.js';
3
- export * from './offset.js';
4
- export * from './portal.js';
5
- export * from './resize.js';
6
- export * from './sticky.js';
7
- export * from './stickyfixed.js';
@@ -1,15 +0,0 @@
1
- import { Store } from '@txstate-mws/svelte-store';
2
- import type { WritableSubject } from '@txstate-mws/svelte-store';
3
- import type { ElementOffsets } from '../util/index.js';
4
- interface OffsetConfig<T extends ElementOffsets = ElementOffsets> {
5
- debounce?: boolean | number;
6
- store?: WritableSubject<T>;
7
- }
8
- export declare class OffsetStore extends Store<ElementOffsets> {
9
- constructor(initialState?: ElementOffsets);
10
- }
11
- export declare function offset(el: HTMLElement, config?: OffsetConfig): {
12
- update(newConfig?: OffsetConfig): void;
13
- destroy: () => void;
14
- };
15
- export {};
package/actions/offset.js DELETED
@@ -1,35 +0,0 @@
1
- import { Store } from '@txstate-mws/svelte-store';
2
- import { debounced, watchForPositionChange } from '../util/index.js';
3
- export class OffsetStore extends Store {
4
- constructor(initialState) {
5
- super(initialState ?? { top: 0, right: 0, bottom: 0, left: 0 });
6
- }
7
- }
8
- export function offset(el, config) {
9
- if (config?.debounce === true)
10
- config.debounce = 100;
11
- let lastOffset = {};
12
- function lookforoffsetchange(current) {
13
- lastOffset = current;
14
- if (config?.store)
15
- config.store.update(v => ({ ...v, ...current }));
16
- el.dispatchEvent(new CustomEvent('offset', { detail: current }));
17
- }
18
- let resolvedchange = config?.debounce ? debounced(lookforoffsetchange, config.debounce) : lookforoffsetchange;
19
- const { destroy, update } = watchForPositionChange(el, resolvedchange);
20
- return {
21
- update(newConfig) {
22
- if (newConfig?.debounce === true)
23
- newConfig.debounce = 100;
24
- if (newConfig?.store !== config?.store) {
25
- newConfig?.store?.update(v => ({ ...v, ...lastOffset }));
26
- }
27
- if (newConfig?.debounce !== config?.debounce) {
28
- resolvedchange = newConfig?.debounce ? debounced(lookforoffsetchange, newConfig.debounce) : lookforoffsetchange;
29
- update(el, resolvedchange);
30
- }
31
- config = newConfig;
32
- },
33
- destroy
34
- };
35
- }
@@ -1,4 +0,0 @@
1
- export declare function portal(el: HTMLElement, target?: HTMLElement | null): {
2
- update: (target?: HTMLElement | null) => void;
3
- destroy: () => void;
4
- };
package/actions/portal.js DELETED
@@ -1,13 +0,0 @@
1
- export function portal(el, target = document.body) {
2
- function update(target = document.body) {
3
- target?.appendChild(el);
4
- }
5
- function destroy() {
6
- el.parentElement?.removeChild(el);
7
- }
8
- update(target);
9
- return {
10
- update,
11
- destroy
12
- };
13
- }
@@ -1,20 +0,0 @@
1
- import { Store } from '@txstate-mws/svelte-store';
2
- import type { SettableSubject } from '@txstate-mws/svelte-store';
3
- export interface ElementSize {
4
- clientWidth?: number;
5
- clientHeight?: number;
6
- offsetWidth?: number;
7
- offsetHeight?: number;
8
- }
9
- interface ResizeConfig {
10
- debounce?: boolean | number;
11
- store?: SettableSubject<ElementSize>;
12
- }
13
- export declare class ResizeStore extends Store<ElementSize> {
14
- constructor(initialState?: ElementSize);
15
- }
16
- export declare function resize(el: HTMLElement, config?: ResizeConfig): void | {
17
- update(newConfig?: ResizeConfig): void;
18
- destroy(): void;
19
- };
20
- export {};
package/actions/resize.js DELETED
@@ -1,45 +0,0 @@
1
- import { equal } from 'txstate-utils';
2
- import { Store } from '@txstate-mws/svelte-store';
3
- import { debounced } from '../util/index.js';
4
- export class ResizeStore extends Store {
5
- constructor(initialState) {
6
- super(initialState ?? { clientWidth: 1024, clientHeight: 1024, offsetWidth: 1024, offsetHeight: 1024 });
7
- }
8
- }
9
- export function resize(el, config) {
10
- if (config?.debounce === true)
11
- config.debounce = 100;
12
- let lastSize = {};
13
- function watch() {
14
- const current = { clientWidth: el.clientWidth, clientHeight: el.clientHeight, offsetWidth: el.offsetWidth, offsetHeight: el.offsetHeight };
15
- if (!equal(lastSize, current)) {
16
- lastSize = current;
17
- if (config?.store)
18
- config.store.set(current);
19
- el.dispatchEvent(new CustomEvent('resize', { detail: current }));
20
- }
21
- }
22
- if (!ResizeObserver)
23
- return watch(); // support SSR
24
- let observer = new ResizeObserver(config?.debounce ? debounced(watch, config.debounce) : watch);
25
- observer.observe(el);
26
- watch();
27
- return {
28
- update(newConfig) {
29
- if (newConfig?.debounce === true)
30
- newConfig.debounce = 100;
31
- if (newConfig?.store !== config?.store) {
32
- newConfig?.store?.set(lastSize);
33
- }
34
- if (!!newConfig?.debounce !== !!config?.debounce) {
35
- observer.disconnect();
36
- observer = new ResizeObserver(newConfig?.debounce ? debounced(watch, newConfig.debounce) : watch);
37
- observer.observe(el);
38
- }
39
- config = newConfig;
40
- },
41
- destroy() {
42
- observer.disconnect();
43
- }
44
- };
45
- }
@@ -1,33 +0,0 @@
1
- import type { SettableSubject } from '@txstate-mws/svelte-store';
2
- export interface StickyArgs<T extends StickyStore = StickyStore> {
3
- target?: HTMLElement;
4
- store?: SettableSubject<T>;
5
- }
6
- export interface StickyStore {
7
- floating?: boolean;
8
- translateY?: number;
9
- }
10
- /**
11
- * A robust implementation of stickyness based on `transform: translateY()`. Only use when
12
- * `position: sticky` won't work for you. If `position: sticky` does work for you, use it,
13
- * it performs better.
14
- *
15
- * Reasons `position: sticky` might not work:
16
- * 1) it cannot work on a thead or tr, only td and th (and non-table elements)
17
- * 2) it gets confused if any parent elements have an `overflow` set, even `overflow: hidden`
18
- * 3) it always stays within the boundaries of the closest parent element that has a `position`
19
- * * use:sticky accepts a `target` element as a parameter, defining the bounding container
20
- *
21
- * Note that if the element you place this on has other transforms, this is going to overwrite them.
22
- *
23
- * The main drawback to this approach is that it cannot do its work before scroll on mobile, only after,
24
- * so on mobile you are going to see it visually lag a little during scrolling.
25
- *
26
- * You may also want to try use:stickyfixed. It is based on setting `position: fixed` when appropriate,
27
- * and therefore animates better than this. However, it has a lot more work to do and is more
28
- * fragile if your markup/CSS situation is very complex.
29
- */
30
- export declare function sticky(el: HTMLElement, config?: StickyArgs): {
31
- update: (newConfig?: StickyArgs) => void;
32
- destroy: () => void;
33
- };
package/actions/sticky.js DELETED
@@ -1,85 +0,0 @@
1
- import { bodyOffset, watchForPositionChange } from '../util/index.js';
2
- let scrolled = false;
3
- /**
4
- * A robust implementation of stickyness based on `transform: translateY()`. Only use when
5
- * `position: sticky` won't work for you. If `position: sticky` does work for you, use it,
6
- * it performs better.
7
- *
8
- * Reasons `position: sticky` might not work:
9
- * 1) it cannot work on a thead or tr, only td and th (and non-table elements)
10
- * 2) it gets confused if any parent elements have an `overflow` set, even `overflow: hidden`
11
- * 3) it always stays within the boundaries of the closest parent element that has a `position`
12
- * * use:sticky accepts a `target` element as a parameter, defining the bounding container
13
- *
14
- * Note that if the element you place this on has other transforms, this is going to overwrite them.
15
- *
16
- * The main drawback to this approach is that it cannot do its work before scroll on mobile, only after,
17
- * so on mobile you are going to see it visually lag a little during scrolling.
18
- *
19
- * You may also want to try use:stickyfixed. It is based on setting `position: fixed` when appropriate,
20
- * and therefore animates better than this. However, it has a lot more work to do and is more
21
- * fragile if your markup/CSS situation is very complex.
22
- */
23
- export function sticky(el, config) {
24
- let floating;
25
- let translateY;
26
- let scrollstart = Number.MAX_SAFE_INTEGER;
27
- let scrollend = Number.MAX_SAFE_INTEGER;
28
- function recalc(scroll) {
29
- const newTranslateY = Math.max(Math.min(scroll, scrollend) - scrollstart, 0);
30
- if (newTranslateY !== translateY) {
31
- el.style.transform = `translateY(${newTranslateY}px)`;
32
- translateY = newTranslateY;
33
- floating = scroll >= scrollstart;
34
- config?.store?.update(v => ({ ...v, translateY, floating }));
35
- }
36
- }
37
- function onscroll(e) {
38
- recalc(window.scrollY);
39
- }
40
- function onwheel(e) {
41
- e.preventDefault();
42
- const scroll = window.scrollY + e.deltaY;
43
- recalc(scroll);
44
- if (!scrolled) {
45
- window.scrollTo({ top: scroll });
46
- scrolled = true;
47
- setTimeout(() => {
48
- scrolled = false;
49
- }, 0);
50
- }
51
- }
52
- let lastOffsetParent = el.offsetParent;
53
- function onpositionchange(offset) {
54
- const offsetParent = el.offsetParent;
55
- if (!(offsetParent instanceof HTMLElement))
56
- return;
57
- if (lastOffsetParent !== offsetParent) {
58
- offset[0] = bodyOffset(offsetParent);
59
- update(offsetParent, onpositionchange);
60
- lastOffsetParent = offsetParent;
61
- }
62
- scrollstart = offset[0].top + el.offsetTop;
63
- const containeroffset = config?.target ? offset[1] : offset[0];
64
- scrollend = Math.max(scrollstart, containeroffset.top + (config?.target ?? offsetParent).clientHeight - el.offsetHeight);
65
- recalc(window.scrollY);
66
- }
67
- const { destroy, update } = watchForPositionChange([el.offsetParent, config?.target], onpositionchange);
68
- window.addEventListener('scroll', onscroll, { passive: true });
69
- window.addEventListener('wheel', onwheel, { passive: false });
70
- function stickyUpdate(newConfig) {
71
- if (newConfig?.store !== config?.store)
72
- newConfig?.store?.update(v => ({ ...v, translateY, floating }));
73
- config = newConfig;
74
- update([el.offsetParent, config?.target], onpositionchange);
75
- }
76
- function stickyDestroy() {
77
- window.removeEventListener('scroll', onscroll);
78
- window.removeEventListener('wheel', onwheel);
79
- destroy();
80
- }
81
- return {
82
- update: stickyUpdate,
83
- destroy: stickyDestroy
84
- };
85
- }
@@ -1,25 +0,0 @@
1
- import type { StickyArgs } from './sticky.js';
2
- /**
3
- * An implementation of stickyness based on `positon: fixed`. Only use when
4
- * `position: sticky` won't work for you. If `position: sticky` does work for you, use it,
5
- * it performs better.
6
- *
7
- * Reasons `position: sticky` might not work:
8
- * 1) it cannot work on a thead or tr, only td and th (and non-table elements)
9
- * 2) it gets confused if any parent elements have an `overflow` set, even `overflow: hidden`
10
- * 3) it always stays within the boundaries of the closest parent element that has a `position`
11
- * * use:sticky accepts a `target` element as a parameter, defining the bounding container
12
- *
13
- * Note that this implementation gets great animation performance at the cost of extra complexity.
14
- * It does a lot of work like cloning the element you place it on, placing the cloned element next
15
- * to yours in the DOM, setting widths to match between them, watching for mutations to keep them
16
- * in sync, etc. Some of this work may get foiled if you have a complex html/css/js situation.
17
- *
18
- * You may also want to try use:sticky. It is based on setting `transform: translateY()` as appropriate,
19
- * and is less fragile than this. However, it cannot do its work before the scroll on mobile, only after,
20
- * so it will visually lag a bit on mobile.
21
- */
22
- export declare function stickyfixed(el: HTMLElement, config?: StickyArgs): {
23
- update: (newConfig?: StickyArgs) => void;
24
- destroy: () => void;
25
- };