tycho-components 0.19.9 → 0.19.10

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.
@@ -1,11 +1,43 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { MenuItem, Select } from '@mui/material';
3
3
  import classNames from 'classnames';
4
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
5
  import { Trans, useTranslation } from 'react-i18next';
5
- import { IconButton } from 'tycho-storybook';
6
+ import { Icon, IconButton } from 'tycho-storybook';
6
7
  import './styles.scss';
8
+ const DRAG_THRESHOLD_PX = 6;
9
+ /** 0-based index: center when page 4+ is active (after the 3rd page). */
10
+ const CENTER_SCROLL_FROM_PAGE_INDEX = 3;
7
11
  export default function AppPagination({ totalElements, pagination, setPagination, hideItensPage, numItens = [10, 25, 50], size = 'medium', }) {
8
12
  const { t } = useTranslation('common');
13
+ const scrollContainerRef = useRef(null);
14
+ const activePageButtonRef = useRef(null);
15
+ const centerAfterClickRef = useRef(false);
16
+ const [stripScrollEdges, setStripScrollEdges] = useState({
17
+ overflow: false,
18
+ canScrollLeft: false,
19
+ canScrollRight: false,
20
+ });
21
+ const { isDraggingScroll, suppressPageClickRef, pagesTotalPointerHandlers } = useDragToScrollRow(scrollContainerRef, DRAG_THRESHOLD_PX);
22
+ const updateStripScrollEdgeState = useCallback(() => {
23
+ const el = scrollContainerRef.current;
24
+ if (!el)
25
+ return;
26
+ const maxScroll = el.scrollWidth - el.clientWidth;
27
+ const overflow = maxScroll > 1;
28
+ setStripScrollEdges({
29
+ overflow,
30
+ canScrollLeft: overflow && el.scrollLeft > 1,
31
+ canScrollRight: overflow && el.scrollLeft < maxScroll - 1,
32
+ });
33
+ }, []);
34
+ const scrollPageStrip = useCallback((direction) => {
35
+ const el = scrollContainerRef.current;
36
+ if (!el)
37
+ return;
38
+ const step = Math.max(72, Math.floor(el.clientWidth * 0.8));
39
+ el.scrollBy({ left: direction * step, behavior: 'smooth' });
40
+ }, []);
9
41
  const numPages = Math.ceil(totalElements / pagination.pageSize);
10
42
  const currentPage = pagination.pageIndex + 1;
11
43
  const getFirstElementNumber = () => pagination.pageIndex * pagination.pageSize + 1;
@@ -16,17 +48,86 @@ export default function AppPagination({ totalElements, pagination, setPagination
16
48
  const setPageIndex = (p) => {
17
49
  setPagination({ ...pagination, pageIndex: p });
18
50
  };
51
+ const scrollActivePageIntoCenter = useCallback(() => {
52
+ activePageButtonRef.current?.scrollIntoView({
53
+ inline: 'center',
54
+ block: 'nearest',
55
+ behavior: 'smooth',
56
+ });
57
+ }, []);
58
+ useLayoutEffect(() => {
59
+ if (centerAfterClickRef.current) {
60
+ centerAfterClickRef.current = false;
61
+ scrollActivePageIntoCenter();
62
+ }
63
+ else if (pagination.pageIndex >= CENTER_SCROLL_FROM_PAGE_INDEX) {
64
+ scrollActivePageIntoCenter();
65
+ }
66
+ requestAnimationFrame(() => updateStripScrollEdgeState());
67
+ }, [
68
+ numPages,
69
+ pagination.pageIndex,
70
+ pagination.pageSize,
71
+ scrollActivePageIntoCenter,
72
+ updateStripScrollEdgeState,
73
+ ]);
74
+ useEffect(() => {
75
+ const el = scrollContainerRef.current;
76
+ if (!el)
77
+ return;
78
+ const onScroll = () => updateStripScrollEdgeState();
79
+ el.addEventListener('scroll', onScroll, { passive: true });
80
+ const ro = new ResizeObserver(() => updateStripScrollEdgeState());
81
+ ro.observe(el);
82
+ return () => {
83
+ el.removeEventListener('scroll', onScroll);
84
+ ro.disconnect();
85
+ };
86
+ }, [updateStripScrollEdgeState]);
87
+ useEffect(() => {
88
+ const el = scrollContainerRef.current;
89
+ if (!el)
90
+ return;
91
+ const onWheel = (e) => {
92
+ if (el.scrollWidth <= el.clientWidth + 1)
93
+ return;
94
+ const dominantX = Math.abs(e.deltaX) > Math.abs(e.deltaY);
95
+ const scrollDelta = dominantX ? e.deltaX : e.deltaY;
96
+ if (scrollDelta === 0)
97
+ return;
98
+ const maxScroll = el.scrollWidth - el.clientWidth;
99
+ const nextLeft = el.scrollLeft + scrollDelta;
100
+ const clamped = Math.max(0, Math.min(maxScroll, nextLeft));
101
+ const atStart = el.scrollLeft <= 0;
102
+ const atEnd = el.scrollLeft >= maxScroll - 1;
103
+ if ((scrollDelta < 0 && atStart) || (scrollDelta > 0 && atEnd)) {
104
+ return;
105
+ }
106
+ el.scrollLeft = clamped;
107
+ e.preventDefault();
108
+ };
109
+ el.addEventListener('wheel', onWheel, { passive: false });
110
+ return () => el.removeEventListener('wheel', onWheel);
111
+ }, []);
19
112
  return (_jsxs("div", { className: classNames('app-table-pagination', {
20
113
  small: size === 'small',
21
114
  }), children: [_jsxs("div", { className: "itens-container", children: [!hideItensPage && (_jsxs("div", { className: "itens-page", children: [_jsx("span", { children: t('table.label.rows-page') }), _jsx(Select, { value: pagination.pageSize, onChange: (e) => setPageSize(e.target.value), MenuProps: menuProps, size: size === 'small' ? 'small' : 'medium', children: numItens.map((size) => (_jsx(MenuItem, { value: size, children: size }, size))) })] })), _jsx("div", { className: "itens-total", children: _jsx("span", { children: _jsx(Trans, { t: t, i18nKey: "table.label.items", values: {
22
115
  first: getFirstElementNumber(),
23
116
  last: getLastElementNumber(),
24
117
  total: totalElements,
25
- } }) }) })] }), _jsxs("div", { className: "pages-navigation", children: [_jsx(IconButton, { size: size, mode: "tonal", onClick: () => setPageIndex(pagination.pageIndex - 1), disabled: pagination.pageIndex === 0, name: "chevron_backward" }), _jsx("div", { className: "pages-total", children: Array.from({ length: numPages }, (_, index) => {
118
+ } }) }) })] }), _jsxs("div", { className: "pages-navigation", children: [_jsx(IconButton, { size: size, mode: "tonal", onClick: () => setPageIndex(pagination.pageIndex - 1), disabled: pagination.pageIndex === 0, name: "chevron_backward" }), stripScrollEdges.overflow && (_jsx("button", { type: "button", className: "page-strip-nudge", disabled: !stripScrollEdges.canScrollLeft, onClick: () => scrollPageStrip(-1), "aria-label": t('table.pagination.scroll-strip-left'), title: t('table.pagination.scroll-strip-left'), children: _jsx(Icon, { name: "keyboard_double_arrow_left", size: "x-small" }) })), _jsx("div", { ref: scrollContainerRef, className: classNames('pages-total', {
119
+ 'is-dragging': isDraggingScroll,
120
+ }), ...pagesTotalPointerHandlers, children: Array.from({ length: numPages }, (_, index) => {
26
121
  const pageNum = index + 1;
27
122
  const isActive = pageNum === currentPage;
28
- return (_jsx("button", { type: "button", className: classNames('page-number', { active: isActive }), onClick: () => setPageIndex(index), "aria-current": isActive ? 'page' : undefined, children: pageNum }, pageNum));
29
- }) }), _jsx(IconButton, { size: size, mode: "tonal", onClick: () => setPageIndex(pagination.pageIndex + 1), disabled: pagination.pageIndex >= numPages - 1, name: "chevron_forward" })] })] }));
123
+ return (_jsx("button", { ref: isActive ? activePageButtonRef : undefined, type: "button", className: classNames('page-number', { active: isActive }), onClick: () => {
124
+ if (suppressPageClickRef.current) {
125
+ return;
126
+ }
127
+ centerAfterClickRef.current = true;
128
+ setPageIndex(index);
129
+ }, "aria-current": isActive ? 'page' : undefined, children: pageNum }, pageNum));
130
+ }) }), stripScrollEdges.overflow && (_jsx("button", { type: "button", className: "page-strip-nudge", disabled: !stripScrollEdges.canScrollRight, onClick: () => scrollPageStrip(1), "aria-label": t('table.pagination.scroll-strip-right'), title: t('table.pagination.scroll-strip-right'), children: _jsx(Icon, { name: "keyboard_double_arrow_right", size: "x-small" }) })), _jsx(IconButton, { size: size, mode: "tonal", onClick: () => setPageIndex(pagination.pageIndex + 1), disabled: pagination.pageIndex >= numPages - 1, name: "chevron_forward" })] })] }));
30
131
  }
31
132
  const menuProps = {
32
133
  PaperProps: {
@@ -59,3 +160,86 @@ const menuProps = {
59
160
  ],
60
161
  },
61
162
  };
163
+ const initialDragRow = {
164
+ pointerId: null,
165
+ startX: 0,
166
+ startScrollLeft: 0,
167
+ dragging: false,
168
+ };
169
+ /**
170
+ * Horizontal drag-to-scroll for an overflow-x container: hides reliance on
171
+ * the scrollbar (paired with CSS) while keeping click-vs-drag distinct.
172
+ */
173
+ function useDragToScrollRow(containerRef, dragThresholdPx) {
174
+ const suppressPageClickRef = useRef(false);
175
+ const dragRef = useRef({ ...initialDragRow });
176
+ const [isDraggingScroll, setIsDraggingScroll] = useState(false);
177
+ const pagesTotalPointerHandlers = useMemo(() => {
178
+ const endPointer = (e) => {
179
+ const d = dragRef.current;
180
+ if (d.pointerId !== e.pointerId)
181
+ return;
182
+ const el = containerRef.current;
183
+ if (d.dragging) {
184
+ suppressPageClickRef.current = true;
185
+ window.setTimeout(() => {
186
+ suppressPageClickRef.current = false;
187
+ }, 0);
188
+ }
189
+ dragRef.current = { ...initialDragRow };
190
+ setIsDraggingScroll(false);
191
+ if (el) {
192
+ try {
193
+ el.releasePointerCapture(e.pointerId);
194
+ }
195
+ catch {
196
+ /* capture may already be released */
197
+ }
198
+ }
199
+ };
200
+ return {
201
+ onPointerDown(e) {
202
+ if (e.button !== 0)
203
+ return;
204
+ const target = e.target;
205
+ if (target?.closest('.page-number')) {
206
+ return;
207
+ }
208
+ const el = containerRef.current;
209
+ if (!el)
210
+ return;
211
+ dragRef.current = {
212
+ pointerId: e.pointerId,
213
+ startX: e.clientX,
214
+ startScrollLeft: el.scrollLeft,
215
+ dragging: false,
216
+ };
217
+ el.setPointerCapture(e.pointerId);
218
+ },
219
+ onPointerMove(e) {
220
+ const d = dragRef.current;
221
+ if (d.pointerId !== e.pointerId)
222
+ return;
223
+ const el = containerRef.current;
224
+ if (!el)
225
+ return;
226
+ const dx = e.clientX - d.startX;
227
+ if (!d.dragging && Math.abs(dx) > dragThresholdPx) {
228
+ d.dragging = true;
229
+ setIsDraggingScroll(true);
230
+ }
231
+ if (d.dragging) {
232
+ el.scrollLeft = d.startScrollLeft - dx;
233
+ e.preventDefault();
234
+ }
235
+ },
236
+ onPointerUp: endPointer,
237
+ onPointerCancel: endPointer,
238
+ };
239
+ }, [containerRef, dragThresholdPx]);
240
+ return {
241
+ isDraggingScroll,
242
+ suppressPageClickRef,
243
+ pagesTotalPointerHandlers,
244
+ };
245
+ }
@@ -27,19 +27,26 @@
27
27
  align-items: center;
28
28
  gap: 4px;
29
29
  flex-wrap: nowrap;
30
- max-width: 200px;
30
+ max-width: 196px;
31
31
  overflow-x: auto;
32
32
  overflow-y: hidden;
33
- padding-bottom: 2px;
34
- scrollbar-width: thin;
33
+ cursor: grab;
34
+ touch-action: pan-x;
35
+ overscroll-behavior-x: contain;
36
+ scrollbar-width: none;
37
+ -ms-overflow-style: none;
38
+ user-select: none;
35
39
 
36
40
  &::-webkit-scrollbar {
37
- height: 6px;
41
+ display: none;
38
42
  }
39
43
 
40
- &::-webkit-scrollbar-thumb {
41
- background-color: var(--border-subtle-2);
42
- border-radius: 3px;
44
+ &.is-dragging {
45
+ cursor: grabbing;
46
+
47
+ .page-number {
48
+ cursor: grabbing;
49
+ }
43
50
  }
44
51
 
45
52
  .page-number {
@@ -83,6 +90,37 @@
83
90
  margin-left: auto;
84
91
  }
85
92
 
93
+ .page-strip-nudge {
94
+ flex-shrink: 0;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ width: 22px;
99
+ min-width: 22px;
100
+ height: 40px;
101
+ padding: 0;
102
+ margin: 0;
103
+ border: none;
104
+ border-radius: 0;
105
+ background-color: var(--button-secondary-default);
106
+ color: var(--text-on-color-dark);
107
+ cursor: pointer;
108
+
109
+ &:hover:not(:disabled) {
110
+ filter: brightness(1.05);
111
+ }
112
+
113
+ &:disabled {
114
+ opacity: 0.4;
115
+ cursor: default;
116
+ }
117
+
118
+ &:focus-visible {
119
+ outline: 2px solid var(--border-focus);
120
+ outline-offset: 2px;
121
+ }
122
+ }
123
+
86
124
  .MuiInputBase-root {
87
125
  border-radius: 0px;
88
126
  font-family: 'Work Sans' !important;
@@ -127,6 +165,10 @@
127
165
  }
128
166
  }
129
167
 
168
+ .page-strip-nudge {
169
+ height: 32px;
170
+ }
171
+
130
172
  .MuiInputBase-root {
131
173
  > .MuiSelect-select {
132
174
  height: 32px;
@@ -45,7 +45,7 @@ const init = ({ selector, tree, dagre = false, stylesheet = defaultStylesheet, w
45
45
  // @ts-ignore (no types)
46
46
  cy.nodeHtmlLabel([
47
47
  {
48
- query: 'node',
48
+ query: 'node.token',
49
49
  halign: 'center',
50
50
  valign: 'center',
51
51
  halignBox: 'center',
@@ -89,6 +89,8 @@ export declare const commonResources: {
89
89
  'internal.server.error': string;
90
90
  'table.label.rows-page': string;
91
91
  'table.label.items': string;
92
+ 'table.pagination.scroll-strip-left': string;
93
+ 'table.pagination.scroll-strip-right': string;
92
94
  'tooltip.copy': string;
93
95
  'tooltip.copied': string;
94
96
  'document.status.deleted': string;
@@ -410,6 +412,8 @@ export declare const commonResources: {
410
412
  'internal.server.error': string;
411
413
  'table.label.rows-page': string;
412
414
  'table.label.items': string;
415
+ 'table.pagination.scroll-strip-left': string;
416
+ 'table.pagination.scroll-strip-right': string;
413
417
  'tooltip.copy': string;
414
418
  'tooltip.copied': string;
415
419
  'document.status.deleted': string;
@@ -727,6 +731,8 @@ export declare const commonResources: {
727
731
  'internal.server.error': string;
728
732
  'table.label.rows-page': string;
729
733
  'table.label.items': string;
734
+ 'table.pagination.scroll-strip-left': string;
735
+ 'table.pagination.scroll-strip-right': string;
730
736
  'tooltip.copy': string;
731
737
  'tooltip.copied': string;
732
738
  'document.status.deleted': string;
@@ -43,6 +43,8 @@ export declare const CommonTexts: {
43
43
  'internal.server.error': string;
44
44
  'table.label.rows-page': string;
45
45
  'table.label.items': string;
46
+ 'table.pagination.scroll-strip-left': string;
47
+ 'table.pagination.scroll-strip-right': string;
46
48
  'tooltip.copy': string;
47
49
  'tooltip.copied': string;
48
50
  'document.status.deleted': string;
@@ -111,6 +113,8 @@ export declare const CommonTexts: {
111
113
  'internal.server.error': string;
112
114
  'table.label.rows-page': string;
113
115
  'table.label.items': string;
116
+ 'table.pagination.scroll-strip-left': string;
117
+ 'table.pagination.scroll-strip-right': string;
114
118
  'tooltip.copy': string;
115
119
  'tooltip.copied': string;
116
120
  'document.status.deleted': string;
@@ -179,6 +183,8 @@ export declare const CommonTexts: {
179
183
  'internal.server.error': string;
180
184
  'table.label.rows-page': string;
181
185
  'table.label.items': string;
186
+ 'table.pagination.scroll-strip-left': string;
187
+ 'table.pagination.scroll-strip-right': string;
182
188
  'tooltip.copy': string;
183
189
  'tooltip.copied': string;
184
190
  'document.status.deleted': string;
@@ -43,6 +43,8 @@ export const CommonTexts = {
43
43
  'internal.server.error': 'An unexpected error occurred. Contact the administrator.',
44
44
  'table.label.rows-page': 'Items per page',
45
45
  'table.label.items': '{{first}} - {{last}} of {{total}} items',
46
+ 'table.pagination.scroll-strip-left': 'Scroll page numbers left',
47
+ 'table.pagination.scroll-strip-right': 'Scroll page numbers right',
46
48
  'tooltip.copy': 'Copy',
47
49
  'tooltip.copied': 'Copied!',
48
50
  'document.status.deleted': 'Deleted',
@@ -111,6 +113,8 @@ export const CommonTexts = {
111
113
  'internal.server.error': 'Ocorreu um erro inesperado. Entre em contato com o administrador.',
112
114
  'table.label.rows-page': 'Itens por página',
113
115
  'table.label.items': '{{first}} - {{last}} de {{total}} itens',
116
+ 'table.pagination.scroll-strip-left': 'Rolar números das páginas à esquerda',
117
+ 'table.pagination.scroll-strip-right': 'Rolar números das páginas à direita',
114
118
  'tooltip.copy': 'Copiar',
115
119
  'tooltip.copied': 'Copiado!',
116
120
  'document.status.deleted': 'Excluído',
@@ -179,6 +183,8 @@ export const CommonTexts = {
179
183
  'internal.server.error': "Si è verificato un errore inaspettato. Contatta l'amministratore.",
180
184
  'table.label.rows-page': 'Elementi per pagina',
181
185
  'table.label.items': '{{first}} - {{last}} di {{total}} elementi',
186
+ 'table.pagination.scroll-strip-left': 'Scorri i numeri di pagina a sinistra',
187
+ 'table.pagination.scroll-strip-right': 'Scorri i numeri di pagina a destra',
182
188
  'tooltip.copy': 'Copia',
183
189
  'tooltip.copied': 'Copiato!',
184
190
  'document.status.deleted': 'Eliminato',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.19.9",
4
+ "version": "0.19.10",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -49,7 +49,7 @@
49
49
  "react-hook-form": "^7.45.2",
50
50
  "react-i18next": "^13.0.2",
51
51
  "react-router-dom": "^6.14.2",
52
- "tycho-storybook": "0.7.1",
52
+ "tycho-storybook": "0.7.2",
53
53
  "yup": "^1.2.0"
54
54
  },
55
55
  "devDependencies": {