@udixio/ui-react 2.10.12 → 2.10.14

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 (179) hide show
  1. package/dist/index.cjs +3 -3
  2. package/dist/index.js +2696 -2710
  3. package/dist/lib/effects/ThemeProvider.d.ts.map +1 -1
  4. package/dist/theme.worker.js +6633 -0
  5. package/package.json +4 -1
  6. package/.eslintrc.mjs +0 -22
  7. package/.storybook/main.ts +0 -20
  8. package/.storybook/preview.ts +0 -1
  9. package/CHANGELOG.md +0 -1130
  10. package/dist/scrollDriven-AP2yWhzi.js +0 -121
  11. package/postcss.config.mjs +0 -5
  12. package/src/index.css +0 -4
  13. package/src/index.ts +0 -1
  14. package/src/lib/components/AnchorPositioner.tsx +0 -185
  15. package/src/lib/components/Button.tsx +0 -208
  16. package/src/lib/components/Card.tsx +0 -47
  17. package/src/lib/components/Carousel.tsx +0 -437
  18. package/src/lib/components/CarouselItem.tsx +0 -61
  19. package/src/lib/components/Checkbox.tsx +0 -120
  20. package/src/lib/components/Chip.tsx +0 -341
  21. package/src/lib/components/Chips.tsx +0 -331
  22. package/src/lib/components/ContextMenu.tsx +0 -109
  23. package/src/lib/components/DatePicker.tsx +0 -432
  24. package/src/lib/components/Divider.tsx +0 -20
  25. package/src/lib/components/Fab.tsx +0 -127
  26. package/src/lib/components/FabMenu.tsx +0 -239
  27. package/src/lib/components/IconButton.tsx +0 -146
  28. package/src/lib/components/Menu.tsx +0 -88
  29. package/src/lib/components/MenuGroup.tsx +0 -34
  30. package/src/lib/components/MenuHeadline.tsx +0 -9
  31. package/src/lib/components/MenuItem.tsx +0 -215
  32. package/src/lib/components/NavigationRail.tsx +0 -186
  33. package/src/lib/components/NavigationRailItem.tsx +0 -227
  34. package/src/lib/components/ProgressIndicator.tsx +0 -214
  35. package/src/lib/components/SideSheet.tsx +0 -135
  36. package/src/lib/components/Slider.tsx +0 -374
  37. package/src/lib/components/Snackbar.tsx +0 -77
  38. package/src/lib/components/Switch.tsx +0 -107
  39. package/src/lib/components/Tab.tsx +0 -123
  40. package/src/lib/components/TabGroup.tsx +0 -66
  41. package/src/lib/components/TabGroupContext.tsx +0 -16
  42. package/src/lib/components/TabPanel.tsx +0 -27
  43. package/src/lib/components/TabPanels.tsx +0 -76
  44. package/src/lib/components/Tabs.tsx +0 -105
  45. package/src/lib/components/TextField.tsx +0 -586
  46. package/src/lib/components/Tooltip.tsx +0 -217
  47. package/src/lib/components/index.ts +0 -34
  48. package/src/lib/config/config.interface.ts +0 -9
  49. package/src/lib/config/define-config.ts +0 -16
  50. package/src/lib/config/index.ts +0 -2
  51. package/src/lib/effects/AnimateOnScroll.ts +0 -391
  52. package/src/lib/effects/State.tsx +0 -90
  53. package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
  54. package/src/lib/effects/ThemeProvider.tsx +0 -172
  55. package/src/lib/effects/block-scroll.effect.tsx +0 -313
  56. package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
  57. package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
  58. package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
  59. package/src/lib/effects/custom-scroll/index.ts +0 -3
  60. package/src/lib/effects/index.ts +0 -7
  61. package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
  62. package/src/lib/effects/ripple/index.tsx +0 -1
  63. package/src/lib/effects/scrollDriven.ts +0 -239
  64. package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
  65. package/src/lib/effects/theme.worker.ts +0 -97
  66. package/src/lib/hooks/index.ts +0 -10
  67. package/src/lib/hooks/useTooltipTrigger.ts +0 -270
  68. package/src/lib/icon/icon.tsx +0 -125
  69. package/src/lib/icon/index.ts +0 -1
  70. package/src/lib/index.ts +0 -8
  71. package/src/lib/interfaces/button.interface.ts +0 -65
  72. package/src/lib/interfaces/card.interface.ts +0 -11
  73. package/src/lib/interfaces/carousel-item.interface.ts +0 -12
  74. package/src/lib/interfaces/carousel.interface.ts +0 -41
  75. package/src/lib/interfaces/checkbox.interface.ts +0 -39
  76. package/src/lib/interfaces/chip.interface.ts +0 -97
  77. package/src/lib/interfaces/chips.interface.ts +0 -37
  78. package/src/lib/interfaces/date-picker.interface.ts +0 -79
  79. package/src/lib/interfaces/divider.interface.ts +0 -7
  80. package/src/lib/interfaces/fab-menu.interface.ts +0 -12
  81. package/src/lib/interfaces/fab.interface.ts +0 -27
  82. package/src/lib/interfaces/icon-button.interface.ts +0 -38
  83. package/src/lib/interfaces/index.ts +0 -26
  84. package/src/lib/interfaces/menu-group.interface.ts +0 -13
  85. package/src/lib/interfaces/menu-item.interface.ts +0 -29
  86. package/src/lib/interfaces/menu.interface.ts +0 -19
  87. package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
  88. package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
  89. package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
  90. package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
  91. package/src/lib/interfaces/slider.interface.ts +0 -27
  92. package/src/lib/interfaces/snackbar.interface.ts +0 -13
  93. package/src/lib/interfaces/switch.interface.ts +0 -14
  94. package/src/lib/interfaces/tab-group.interface.ts +0 -13
  95. package/src/lib/interfaces/tab-panels.interface.ts +0 -21
  96. package/src/lib/interfaces/tab.interface.ts +0 -31
  97. package/src/lib/interfaces/tabs.interface.ts +0 -22
  98. package/src/lib/interfaces/text-field.interface.ts +0 -61
  99. package/src/lib/interfaces/tooltip.interface.ts +0 -61
  100. package/src/lib/styles/button.style.ts +0 -136
  101. package/src/lib/styles/card.style.ts +0 -29
  102. package/src/lib/styles/carousel-item.style.ts +0 -24
  103. package/src/lib/styles/carousel.style.ts +0 -22
  104. package/src/lib/styles/checkbox.style.ts +0 -64
  105. package/src/lib/styles/chip.style.ts +0 -62
  106. package/src/lib/styles/chips.style.ts +0 -20
  107. package/src/lib/styles/date-picker.style.ts +0 -43
  108. package/src/lib/styles/divider.style.ts +0 -31
  109. package/src/lib/styles/fab-menu.style.ts +0 -29
  110. package/src/lib/styles/fab.style.ts +0 -49
  111. package/src/lib/styles/icon-button.style.ts +0 -168
  112. package/src/lib/styles/index.ts +0 -25
  113. package/src/lib/styles/menu-group.style.ts +0 -34
  114. package/src/lib/styles/menu-headline.style.ts +0 -20
  115. package/src/lib/styles/menu-item.style.ts +0 -45
  116. package/src/lib/styles/menu.style.ts +0 -32
  117. package/src/lib/styles/navigation-rail-item.style.ts +0 -56
  118. package/src/lib/styles/navigation-rail.style.ts +0 -36
  119. package/src/lib/styles/progress-indicator.style.ts +0 -72
  120. package/src/lib/styles/side-sheet.style.ts +0 -45
  121. package/src/lib/styles/slider.style.ts +0 -41
  122. package/src/lib/styles/snackbar.style.ts +0 -26
  123. package/src/lib/styles/switch.style.ts +0 -67
  124. package/src/lib/styles/tab-panels.style.ts +0 -35
  125. package/src/lib/styles/tab.style.ts +0 -78
  126. package/src/lib/styles/tabs.style.ts +0 -22
  127. package/src/lib/styles/text-field.style.ts +0 -115
  128. package/src/lib/styles/tooltip.style.ts +0 -48
  129. package/src/lib/utils/component-helper.ts +0 -134
  130. package/src/lib/utils/component.ts +0 -34
  131. package/src/lib/utils/index.ts +0 -7
  132. package/src/lib/utils/string.ts +0 -9
  133. package/src/lib/utils/styles/classnames.ts +0 -49
  134. package/src/lib/utils/styles/get-classname.ts +0 -96
  135. package/src/lib/utils/styles/index.ts +0 -4
  136. package/src/lib/utils/styles/use-classnames.ts +0 -25
  137. package/src/stories/action/button.stories.tsx +0 -86
  138. package/src/stories/action/fab.stories.tsx +0 -54
  139. package/src/stories/action/icon-button.stories.tsx +0 -134
  140. package/src/stories/assets/accessibility.png +0 -0
  141. package/src/stories/assets/accessibility.svg +0 -5
  142. package/src/stories/assets/addon-library.png +0 -0
  143. package/src/stories/assets/assets.png +0 -0
  144. package/src/stories/assets/context.png +0 -0
  145. package/src/stories/assets/discord.svg +0 -15
  146. package/src/stories/assets/docs.png +0 -0
  147. package/src/stories/assets/figma-plugin.png +0 -0
  148. package/src/stories/assets/github.svg +0 -3
  149. package/src/stories/assets/share.png +0 -0
  150. package/src/stories/assets/styling.png +0 -0
  151. package/src/stories/assets/testing.png +0 -0
  152. package/src/stories/assets/theming.png +0 -0
  153. package/src/stories/assets/tutorials.svg +0 -12
  154. package/src/stories/assets/youtube.svg +0 -4
  155. package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
  156. package/src/stories/communication/SnackBar.stories.tsx +0 -32
  157. package/src/stories/communication/tool-tip.stories.tsx +0 -133
  158. package/src/stories/containment/card.stories.tsx +0 -42
  159. package/src/stories/containment/carousel.stories.tsx +0 -65
  160. package/src/stories/containment/divider.stories.tsx +0 -35
  161. package/src/stories/containment/slide-sheet.stories.tsx +0 -45
  162. package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
  163. package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
  164. package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
  165. package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
  166. package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
  167. package/src/stories/selection/slider.stories.tsx +0 -85
  168. package/src/stories/selection/switch.stories.tsx +0 -46
  169. package/src/stories/text-inputs/text-field.stories.tsx +0 -135
  170. package/src/tests/Button.spec.tsx +0 -67
  171. package/src/tests/useClassNames.spec.tsx +0 -82
  172. package/src/udixio.css +0 -120
  173. package/theme.config.ts +0 -7
  174. package/tsconfig.json +0 -16
  175. package/tsconfig.lib.json +0 -51
  176. package/tsconfig.spec.json +0 -37
  177. package/tsconfig.storybook.json +0 -38
  178. package/vite.config.ts +0 -82
  179. /package/dist/{scrollDriven-DWAu7CR0.cjs → scrollDriven.js} +0 -0
@@ -1,90 +0,0 @@
1
- import { RippleEffect } from './ripple';
2
- import {
3
- ClassNameComponent,
4
- classNames,
5
- createUseClassNames,
6
- ReactProps,
7
- } from '../utils';
8
- import { useEffect, useRef, useState } from 'react';
9
-
10
- export interface StateInterface {
11
- type: 'div';
12
- props: {
13
- colorName: string;
14
- stateClassName?:
15
- | string
16
- | 'state-ripple-group'
17
- | 'state-group'
18
- | 'state-layer';
19
- className?: string;
20
- style?: React.CSSProperties;
21
- children?: React.ReactNode;
22
- };
23
- states: { isClient: boolean };
24
- elements: ['stateLayer'];
25
- }
26
-
27
- export const State = ({
28
- style,
29
- colorName,
30
- stateClassName = 'state-ripple-group',
31
- children,
32
- className,
33
- }: ReactProps<StateInterface>) => {
34
- const ref = useRef<HTMLDivElement>(null);
35
- const groupStateRef = useRef<HTMLElement | null>(null);
36
-
37
- const [isClient, setIsClient] = useState(false);
38
- const styles = useStateStyle({
39
- isClient,
40
- stateClassName,
41
- className,
42
- colorName,
43
- });
44
-
45
- useEffect(() => {
46
- if (ref.current && stateClassName !== 'state-layer') {
47
- const groupName = !stateClassName.includes('[')
48
- ? 'group'
49
- : 'group/' + stateClassName.split('[')[1].split(']')[0];
50
-
51
- // On échappe le slash pour le sélecteur CSS
52
- const safeGroupName = groupName.replace(/\//g, '\\/');
53
-
54
- const furthestGroupState = ref.current.closest(
55
- `.${safeGroupName}:not(.${safeGroupName} .${safeGroupName})`,
56
- );
57
- groupStateRef.current = furthestGroupState as HTMLElement | null;
58
- }
59
- setIsClient(true);
60
- }, []);
61
-
62
- return (
63
- <div
64
- ref={ref}
65
- className={styles.stateLayer}
66
- style={{
67
- ['--state-color' as any]: `var(--default-color, var(--color-${colorName}))`,
68
- ...style,
69
- }}
70
- >
71
- {isClient && <RippleEffect triggerRef={groupStateRef} />}
72
- {children}
73
- </div>
74
- );
75
- };
76
- // ... existing code ...
77
- const cardConfig: ClassNameComponent<StateInterface> = ({
78
- isClient,
79
- stateClassName,
80
- }) => ({
81
- stateLayer: classNames([
82
- 'w-full top-0 left-0 h-full absolute pointer-events-none overflow-hidden',
83
- stateClassName,
84
- ]),
85
- });
86
-
87
- export const useStateStyle = createUseClassNames<StateInterface>(
88
- 'stateLayer',
89
- cardConfig,
90
- );
@@ -1,62 +0,0 @@
1
- import {
2
- type ReactNode,
3
- type RefObject,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from 'react';
8
- import { createPortal } from 'react-dom';
9
-
10
- type SyncedFixedWrapperProps = {
11
- targetRef: RefObject<any>;
12
- children: ReactNode;
13
- };
14
-
15
- export const SyncedFixedWrapper = ({
16
- targetRef,
17
- children,
18
- }: SyncedFixedWrapperProps) => {
19
- const [style, setStyle] = useState<React.CSSProperties | null>(null);
20
- const resizeObserver = useRef<ResizeObserver | null>(null);
21
-
22
- const updatePosition = () => {
23
- const target = targetRef.current;
24
- if (!target) return;
25
-
26
- const rect = target.getBoundingClientRect();
27
-
28
- setStyle({
29
- position: 'fixed',
30
- top: rect.top,
31
- left: rect.left,
32
- width: rect.width,
33
- height: rect.height,
34
- pointerEvents: 'none', // si le wrapper ne doit pas capter les events
35
- zIndex: 999, // personnalise si besoin
36
- });
37
- };
38
-
39
- useEffect(() => {
40
- const target = targetRef.current;
41
- if (!target) return;
42
-
43
- updatePosition();
44
-
45
- window.addEventListener('scroll', updatePosition, true);
46
- window.addEventListener('resize', updatePosition);
47
-
48
- // Observe resize of the target element
49
- resizeObserver.current = new ResizeObserver(updatePosition);
50
- resizeObserver.current.observe(target);
51
-
52
- return () => {
53
- window.removeEventListener('scroll', updatePosition, true);
54
- window.removeEventListener('resize', updatePosition);
55
- resizeObserver.current?.disconnect();
56
- };
57
- }, [targetRef]);
58
-
59
- if (!style) return null;
60
-
61
- return createPortal(<div style={style}>{children}</div>, document.body);
62
- };
@@ -1,172 +0,0 @@
1
- import {
2
- type API,
3
- type ConfigInterface,
4
- ContextOptions,
5
- FontPlugin,
6
- loader,
7
- serializeThemeContext,
8
- } from '@udixio/theme';
9
- import { useEffect, useRef, useState } from 'react';
10
- import { TailwindPlugin } from '@udixio/tailwind';
11
- import type {
12
- WorkerInboundMessage,
13
- WorkerOutboundMessage,
14
- } from './theme.worker';
15
- import ThemeWorker from './theme.worker?worker&inline';
16
-
17
- function isValidHexColor(hexColorString: string) {
18
- const regex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
19
- return regex.test(hexColorString);
20
- }
21
-
22
- export const ThemeProvider = ({
23
- config,
24
- throttleDelay = 100,
25
- onLoad,
26
- loadTheme = false,
27
- }: {
28
- config: Readonly<ConfigInterface>;
29
- onLoad?: (api: API) => void;
30
- throttleDelay?: number;
31
- loadTheme?: boolean;
32
- }) => {
33
- const [themeApi, setThemeApi] = useState<API | null>(null);
34
- const [outputCss, setOutputCss] = useState<string | null>(null);
35
-
36
- const workerRef = useRef<Worker | null>(null);
37
- const generationRef = useRef(0);
38
- const lastAppliedIdRef = useRef(0);
39
- const themeApiRef = useRef<API | null>(null);
40
- const firstLoadDoneRef = useRef(false);
41
- const onLoadRef = useRef(onLoad);
42
- useEffect(() => {
43
- onLoadRef.current = onLoad;
44
- }, [onLoad]);
45
-
46
- // Initialisation de l'API et du Worker
47
- useEffect(() => {
48
- let cancelled = false;
49
-
50
- (async () => {
51
- const api = await loader(config, loadTheme);
52
- if (cancelled) return;
53
-
54
- themeApiRef.current = api;
55
- setThemeApi(api);
56
-
57
- const worker = new ThemeWorker() as Worker;
58
- workerRef.current = worker;
59
-
60
- worker.onmessage = (e: MessageEvent<WorkerOutboundMessage>) => {
61
- if (e.data.id > lastAppliedIdRef.current) {
62
- lastAppliedIdRef.current = e.data.id;
63
- firstLoadDoneRef.current = true;
64
- setOutputCss(e.data.css);
65
- onLoadRef.current?.(themeApiRef.current!);
66
- }
67
- };
68
- })();
69
-
70
- return () => {
71
- cancelled = true;
72
- workerRef.current?.terminate();
73
- workerRef.current = null;
74
- };
75
- }, []);
76
-
77
- // Throttle avec exécution en tête (leading) et en fin (trailing)
78
- const timeoutRef = useRef<NodeJS.Timeout | null>(null);
79
- const lastExecTimeRef = useRef<number>(0);
80
- const lastArgsRef = useRef<Partial<ContextOptions> | null>(null);
81
-
82
- useEffect(() => {
83
- if (!themeApi) return;
84
-
85
- const ctx: Partial<ContextOptions> = {
86
- ...config,
87
- sourceColor: config.sourceColor,
88
- };
89
-
90
- const now = Date.now();
91
- const timeSinceLast = now - lastExecTimeRef.current;
92
-
93
- const invoke = async (args: Partial<ContextOptions>) => {
94
- await applyThemeChange(args);
95
- };
96
-
97
- if (lastExecTimeRef.current === 0 || timeSinceLast >= throttleDelay) {
98
- if (timeoutRef.current) {
99
- clearTimeout(timeoutRef.current);
100
- timeoutRef.current = null;
101
- }
102
- lastArgsRef.current = null;
103
- lastExecTimeRef.current = now;
104
- void invoke(ctx);
105
- } else {
106
- lastArgsRef.current = ctx;
107
- if (!timeoutRef.current) {
108
- const remaining = Math.max(0, throttleDelay - timeSinceLast);
109
- timeoutRef.current = setTimeout(async () => {
110
- timeoutRef.current = null;
111
- const args = lastArgsRef.current;
112
- lastArgsRef.current = null;
113
- if (args) {
114
- lastExecTimeRef.current = Date.now();
115
- await invoke(args);
116
- }
117
- }, remaining);
118
- }
119
- }
120
-
121
- return () => {};
122
- }, [config, throttleDelay, themeApi]);
123
-
124
- const applyThemeChange = async (ctx: Partial<ContextOptions>) => {
125
- if (typeof ctx.sourceColor === 'string' && !isValidHexColor(ctx.sourceColor)) {
126
- throw new Error('Invalid hex color');
127
- }
128
-
129
- const api = themeApiRef.current;
130
- if (!api) return;
131
-
132
- // Toujours évaluer sur le main thread (rapide)
133
- api.context.update(ctx);
134
- api.palettes.sync((ctx as any).palettes);
135
-
136
- const worker = workerRef.current;
137
-
138
- // Fallback synchrone : premier rendu ou Worker pas encore prêt
139
- if (!worker || !firstLoadDoneRef.current) {
140
- await api.load();
141
- const css = api.plugins.getPlugin(TailwindPlugin).getInstance().outputCss;
142
- setOutputCss(css);
143
- firstLoadDoneRef.current = true;
144
- onLoad?.(api);
145
- return;
146
- }
147
-
148
- // Offload au Worker
149
- const id = ++generationRef.current;
150
- worker.postMessage({
151
- id,
152
- snapshot: serializeThemeContext(api),
153
- tailwindOptions: api.plugins.getPlugin(TailwindPlugin).options,
154
- fontOptions: api.plugins.getPlugin(FontPlugin).options,
155
- } satisfies WorkerInboundMessage);
156
- };
157
-
158
- useEffect(() => {
159
- return () => {
160
- if (timeoutRef.current) {
161
- clearTimeout(timeoutRef.current);
162
- timeoutRef.current = null;
163
- }
164
- };
165
- }, []);
166
-
167
- if (!outputCss) {
168
- return null;
169
- }
170
-
171
- return <style dangerouslySetInnerHTML={{ __html: outputCss }} />;
172
- };
@@ -1,313 +0,0 @@
1
- import React, { useEffect, useRef } from 'react';
2
-
3
- type ScrollIntent =
4
- | {
5
- type: 'intent';
6
- source: 'wheel' | 'touch' | 'keyboard';
7
- deltaX: number;
8
- deltaY: number;
9
- originalEvent: Event;
10
- }
11
- | {
12
- type: 'scrollbar';
13
- scrollTop: number;
14
- scrollLeft: number;
15
- maxScrollTop: number;
16
- maxScrollLeft: number;
17
- };
18
-
19
- type BlockScrollProps = {
20
- onScroll?: (evt: ScrollIntent) => void; // log des intentions + du scroll via scrollbar
21
- touch?: boolean;
22
- el: HTMLElement;
23
- };
24
-
25
- /**
26
- * @deprecated Potentially blocks scroll events unintentionally (wheel/touch/keyboard)
27
- * and may interfere with internal scrollable areas (modals, lists, overflow containers).
28
- * This API will be removed soon. Avoid using it and migrate to native behaviors
29
- * or to local scroll handling at the component level.
30
- */
31
- export const BlockScroll: React.FC<BlockScrollProps> = ({
32
- onScroll,
33
- el,
34
- touch = true,
35
- }) => {
36
- const lastTouch = useRef<{ x: number; y: number } | null>(null);
37
- const lastScrollTop = useRef<number>(0);
38
- const lastScrollLeft = useRef<number>(0);
39
-
40
- useEffect(() => {
41
- if (!el) return;
42
-
43
- // Initialize last known scroll positions to block scrollbar-based scrolling
44
- lastScrollTop.current = el.scrollTop;
45
- lastScrollLeft.current = el.scrollLeft;
46
-
47
- const emitIntent = (payload: Extract<ScrollIntent, { type: 'intent' }>) => {
48
- // Log the desired deltaY for every scroll attempt (wheel/touch/keyboard)
49
- onScroll?.(payload);
50
- };
51
-
52
- const findScrollableParent = (
53
- node: HTMLElement | null,
54
- ): HTMLElement | null => {
55
- let elNode: HTMLElement | null = node;
56
- while (
57
- elNode &&
58
- elNode !== document.body &&
59
- elNode !== document.documentElement
60
- ) {
61
- const style = window.getComputedStyle(elNode);
62
- const overflowY = style.overflowY || style.overflow;
63
- const isScrollableY =
64
- (overflowY === 'auto' || overflowY === 'scroll') &&
65
- elNode.scrollHeight > elNode.clientHeight;
66
- if (isScrollableY) return elNode;
67
- elNode = elNode.parentElement;
68
- }
69
- return null;
70
- };
71
-
72
- const onWheel = (e: WheelEvent) => {
73
- // Auto-detect closest scrollable ancestor and allow native scroll when it can handle the intent
74
- const target = e.target as HTMLElement | null;
75
-
76
- const scrollableParent = findScrollableParent(target);
77
-
78
- if (scrollableParent && scrollableParent !== el) {
79
- const canScrollDown =
80
- scrollableParent.scrollTop <
81
- scrollableParent.scrollHeight - scrollableParent.clientHeight;
82
- const canScrollUp = scrollableParent.scrollTop > 0;
83
-
84
- // Wheel: positive deltaY => scroll down, negative => scroll up
85
- if ((e.deltaY > 0 && canScrollDown) || (e.deltaY < 0 && canScrollUp)) {
86
- // Let the native scrolling happen inside the scrollable element
87
- return;
88
- }
89
- }
90
-
91
- // Otherwise, block native scroll and emit intent for global smooth scroll
92
- e.preventDefault();
93
- emitIntent({
94
- type: 'intent',
95
- source: 'wheel',
96
- deltaX: e.deltaX,
97
- deltaY: e.deltaY,
98
- originalEvent: e,
99
- });
100
- };
101
-
102
- const onTouchStart = (e: TouchEvent) => {
103
- if (!touch) return;
104
- const t = e.touches[0];
105
- if (t) lastTouch.current = { x: t.clientX, y: t.clientY };
106
- };
107
-
108
- const onTouchMove = (e: TouchEvent) => {
109
- if (!touch) return;
110
- const t = e.touches[0];
111
- if (!t || !lastTouch.current) return;
112
-
113
- const dx = lastTouch.current.x - t.clientX;
114
- const dy = lastTouch.current.y - t.clientY;
115
-
116
- // Auto-detect closest scrollable ancestor for touch and allow native scroll when possible
117
- const target = e.target as HTMLElement | null;
118
-
119
- const scrollableParent = findScrollableParent(target);
120
-
121
- if (scrollableParent && scrollableParent !== el) {
122
- const canScrollDown =
123
- scrollableParent.scrollTop <
124
- scrollableParent.scrollHeight - scrollableParent.clientHeight;
125
- const canScrollUp = scrollableParent.scrollTop > 0;
126
-
127
- // Touch: dy > 0 means user moves finger up -> intent to scroll down
128
- if ((dy > 0 && canScrollDown) || (dy < 0 && canScrollUp)) {
129
- // Update last touch and allow native scroll inside the element
130
- lastTouch.current = { x: t.clientX, y: t.clientY };
131
- return;
132
- }
133
- }
134
-
135
- // Otherwise block and emit intent for global smooth scroll
136
- e.preventDefault();
137
- lastTouch.current = { x: t.clientX, y: t.clientY };
138
- emitIntent({
139
- type: 'intent',
140
- source: 'touch',
141
- deltaX: dx,
142
- deltaY: dy,
143
- originalEvent: e,
144
- });
145
- };
146
-
147
- const onTouchEnd = () => {
148
- if (!touch) return;
149
- lastTouch.current = null;
150
- };
151
-
152
- const interactiveRoles = new Set([
153
- 'textbox',
154
- 'listbox',
155
- 'menu',
156
- 'menubar',
157
- 'grid',
158
- 'tree',
159
- 'tablist',
160
- 'toolbar',
161
- 'radiogroup',
162
- 'combobox',
163
- 'spinbutton',
164
- 'slider',
165
- ]);
166
-
167
- const isEditableOrInteractive = (node: HTMLElement | null) => {
168
- if (!node) return false;
169
- const tag = node.tagName.toLowerCase();
170
- if (tag === 'input' || tag === 'textarea' || tag === 'select')
171
- return true;
172
- if ((node as HTMLInputElement).isContentEditable) return true;
173
- if (tag === 'button') return true;
174
- if (tag === 'a' && (node as HTMLAnchorElement).href) return true;
175
- const role = node.getAttribute('role');
176
- if (role && interactiveRoles.has(role)) return true;
177
- return false;
178
- };
179
-
180
- const hasInteractiveAncestor = (el: HTMLElement | null) => {
181
- let n: HTMLElement | null = el;
182
- while (n && n !== document.body && n !== document.documentElement) {
183
- if (isEditableOrInteractive(n)) return true;
184
- n = n.parentElement;
185
- }
186
- return false;
187
- };
188
-
189
- const canAncestorScroll = (start: HTMLElement | null, dy: number) => {
190
- let n: HTMLElement | null = start;
191
- while (n && n !== el) {
192
- const style = window.getComputedStyle(n);
193
- const overflowY = style.overflowY || style.overflow;
194
- const canScrollY =
195
- (overflowY === 'auto' || overflowY === 'scroll') &&
196
- n.scrollHeight > n.clientHeight;
197
- if (canScrollY) {
198
- const canDown = n.scrollTop < n.scrollHeight - n.clientHeight;
199
- const canUp = n.scrollTop > 0;
200
- if ((dy > 0 && canDown) || (dy < 0 && canUp)) return true;
201
- }
202
- n = n.parentElement;
203
- }
204
- return false;
205
- };
206
-
207
- const onKeyDown = (e: KeyboardEvent) => {
208
- if (e.defaultPrevented) return;
209
-
210
- // Garder les comportements natifs pour les éléments interactifs
211
- const target = e.target as HTMLElement | null;
212
- if (isEditableOrInteractive(target) || hasInteractiveAncestor(target)) {
213
- return; // ne pas empêcher
214
- }
215
-
216
- const line = 40;
217
- const page = el.clientHeight * 0.9;
218
- let dy = 0;
219
-
220
- switch (e.key) {
221
- case 'ArrowDown':
222
- dy = line;
223
- break;
224
- case 'ArrowUp':
225
- dy = -line;
226
- break;
227
- case 'PageDown':
228
- dy = page;
229
- break;
230
- case 'PageUp':
231
- dy = -page;
232
- break;
233
- case 'Home':
234
- dy = Number.NEGATIVE_INFINITY;
235
- break;
236
- case 'End':
237
- dy = Number.POSITIVE_INFINITY;
238
- break;
239
- case ' ': {
240
- // Espace: laisser passer sur boutons/inputs/etc. déjà filtrés ci-dessus
241
- dy = e.shiftKey ? -page : page;
242
- break;
243
- }
244
- default:
245
- return; // ne pas gérer, laisser natif
246
- }
247
-
248
- // Si un ancêtre (≠ el) peut scroller dans ce sens, laisser le natif
249
- if (canAncestorScroll(target, dy)) return;
250
-
251
- // Ne gérer que si focus est sur body/html ou dans el
252
- const ae = document.activeElement as HTMLElement | null;
253
- const focusInsideEl =
254
- ae &&
255
- (ae === document.body ||
256
- ae === document.documentElement ||
257
- el.contains(ae));
258
- if (!focusInsideEl) return;
259
-
260
- // OK on prend en charge: empêcher le natif et émettre l’intention
261
- e.preventDefault();
262
- emitIntent({
263
- type: 'intent',
264
- source: 'keyboard',
265
- deltaX: 0,
266
- deltaY: dy,
267
- originalEvent: e,
268
- });
269
- };
270
-
271
- // const onScrollEvent = (e) => {
272
- // const currentScrollTop = e.target.scrollTop;
273
- // const currentScrollLeft = e.target.scrollLeft;
274
- //
275
- // // Check if scroll position changed from last known position
276
- // if (
277
- // currentScrollTop !== lastScrollTop.current ||
278
- // currentScrollLeft !== lastScrollLeft.current
279
- // ) {
280
- // console.log('onScrollllllllllll', e, document);
281
- // onScroll?.({
282
- // type: 'scrollbar',
283
- // scrollTop: currentScrollTop,
284
- // scrollLeft: currentScrollLeft,
285
- // maxScrollTop: e.target.scrollHeight - e.target.clientHeight,
286
- // maxScrollLeft: e.target.scrollWidth - e.target.clientWidth,
287
- // });
288
- // }
289
- //
290
- // // Update last known scroll positions
291
- // lastScrollTop.current = currentScrollTop;
292
- // lastScrollLeft.current = currentScrollLeft;
293
- //
294
- // document.querySelector('html')?.scrollTo({ top: 0 });
295
- // };
296
-
297
- el.addEventListener('wheel', onWheel, { passive: false });
298
- el.addEventListener('touchstart', onTouchStart, { passive: true });
299
- el.addEventListener('touchmove', onTouchMove, { passive: false });
300
- el.addEventListener('touchend', onTouchEnd, { passive: true });
301
- el.addEventListener('keydown', onKeyDown);
302
- // el.addEventListener('scroll', onScrollEvent, { passive: true });
303
-
304
- return () => {
305
- el.removeEventListener('wheel', onWheel as EventListener);
306
- el.removeEventListener('touchstart', onTouchStart as EventListener);
307
- el.removeEventListener('touchmove', onTouchMove as EventListener);
308
- el.removeEventListener('touchend', onTouchEnd as EventListener);
309
- el.removeEventListener('keydown', onKeyDown as EventListener);
310
- // el.removeEventListener('scroll', onScrollEvent as EventListener);
311
- };
312
- }, [onScroll]);
313
- };