innoboxrr-react-datatable 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innoboxrr-react-datatable",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Gemelo React de innoboxrr-vue-datatable: el mismo contrato de modelo, la misma tabla.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -37,11 +37,12 @@
37
37
  "node": ">=20"
38
38
  },
39
39
  "dependencies": {
40
+ "@floating-ui/dom": "^1.8.0",
40
41
  "@iconify/react": "^6.0.0",
41
42
  "axios": "^1.7.0"
42
43
  },
43
44
  "peerDependencies": {
44
- "innoboxrr-form-core": "^2.1.0",
45
+ "innoboxrr-form-core": "^2.3.0",
45
46
  "react": "^19.0.0",
46
47
  "react-dom": "^19.0.0",
47
48
  "react-router-dom": "^7.0.0"
@@ -2,13 +2,12 @@ import DisabledLinkComponent from './DisabledLinkComponent.jsx'
2
2
  import IconLinkComponent from './IconLinkComponent.jsx'
3
3
  import IconRouteComponent from './IconRouteComponent.jsx'
4
4
 
5
+ // Un popover se cierra solo: no hace falta preguntarle nada a nadie.
6
+ // La llamada es opcional porque hidePopover no existe en un navegador anterior
7
+ // a la Popover API —ni en jsdom—, y un menu que no cierra es mejor que una
8
+ // excepcion.
5
9
  const closeDropdown = (event) => {
6
- const dropdown = event.target.closest('.uk-dropdown')
7
-
8
- // UIkit lo aporta la aplicación anfitriona.
9
- if (dropdown) {
10
- globalThis.UIkit?.dropdown(dropdown)?.hide(false)
11
- }
10
+ event.target.closest('[popover]')?.hidePopover?.()
12
11
  }
13
12
 
14
13
  /**
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from 'react'
2
2
  import ActionListComponent from './ActionListComponent.jsx'
3
+ import DatatableIcon from './DatatableIcon.jsx'
3
4
  import NavDropdownComponent from './NavDropdownComponent.jsx'
4
5
 
5
6
  const cellValue = (head, row) => (
@@ -96,8 +97,9 @@ export default function DataTableComponent({
96
97
  type="button"
97
98
  aria-label={`Acciones del registro ${row.id}`}
98
99
  className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-2 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
100
+ popoverTarget={`dropdown_${row.id}`}
99
101
  onClick={() => onActionButtonClicked?.(row.actions)}>
100
- <i className="fas fa-cogs"></i>
102
+ <DatatableIcon icon="actions" />
101
103
  </button>
102
104
 
103
105
  <NavDropdownComponent id={`dropdown_${row.id}`} pos="left">
@@ -13,7 +13,8 @@ export default function DisabledLinkComponent({ icon = '', text }) {
13
13
  href="#"
14
14
  aria-disabled="true"
15
15
  className="disabled-link block px-4 py-2 dark:hover:text-white dark:text-slate-400"
16
- uk-tooltip="title: This action is not authorized; pos:right"
16
+ data-tooltip="This action is not authorized"
17
+ data-tooltip-pos="right"
17
18
  onClick={(event) => event.preventDefault()}>
18
19
  {showIcon ? <DatatableIcon icon={icon} /> : null}
19
20
  <span>{text}</span>
@@ -1,35 +1,98 @@
1
- import { useEffect, useRef } from 'react'
1
+ import { useCallback, useEffect, useRef } from 'react'
2
+ import { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift } from '@floating-ui/dom'
3
+ import { classFor } from 'innoboxrr-form-core'
2
4
 
3
5
  /**
4
6
  * Gemelo de NavDropdownComponent.vue.
5
7
  *
6
- * UIkit lee el atributo `uk-dropdown` del DOM. React lo escribe igual, pero
7
- * UIkit sólo lo procesa al montarse el nodo, así que se le avisa — con guarda,
8
- * porque UIkit lo aporta la aplicación anfitriona y en una prueba no está.
8
+ * Antes lo movía `uk-dropdown`, y como UIkit sólo procesa el atributo al
9
+ * montarse el nodo, había que avisarle con `UIkit.update()` — con guarda,
10
+ * porque UIkit lo aportaba la aplicación anfitriona sin que nadie lo
11
+ * declarara, y en una prueba no estaba.
12
+ *
13
+ * La mitad de lo que hacía ya lo hace el navegador. La **Popover API** —el
14
+ * atributo `popover` y el `popoverTarget` del botón— aporta de fábrica la capa
15
+ * superior, el cierre al pulsar fuera y el cierre con Escape. Lo único que
16
+ * falta es colocarlo, y de eso se ocupa Floating UI.
17
+ *
18
+ * El botón que lo abre sólo necesita `popoverTarget` con este mismo id:
19
+ *
20
+ * <button popoverTarget={`dropdown_${row.id}`}>…</button>
21
+ * <NavDropdownComponent id={`dropdown_${row.id}`} pos="left" />
22
+ *
23
+ * @param {{ id: string, pos?: string, offset?: number, children?: any }} props
9
24
  */
10
25
  export default function NavDropdownComponent({
11
26
  id,
27
+ // Se conservan los nombres que usaba UIkit para no romper a quien ya los
28
+ // pasa; `bottom-left` y compañía se traducen a los de Floating UI.
12
29
  pos = 'bottom-left',
13
- mode = 'click',
14
- offset = 0,
15
- // Es el nombre de una animacion de UIkit, no una clase nuestra.
16
- animation = 'uk-animation-slide-top-small',
17
- duration = 500,
30
+ offset = 4,
18
31
  children,
19
32
  }) {
20
- const host = useRef(null)
33
+ const panel = useRef(null)
34
+ const stopFollowing = useRef(null)
35
+
36
+ const placement = (() => {
37
+ const [side, align] = pos.split('-')
38
+
39
+ if (! align) {
40
+ return side
41
+ }
42
+
43
+ // UIkit dice `bottom-left` para «debajo, alineado a la izquierda»;
44
+ // Floating UI lo llama `bottom-start`.
45
+ return `${side}-${align === 'left' ? 'start' : 'end'}`
46
+ })()
47
+
48
+ const onBeforeToggle = useCallback((event) => {
49
+ if (event.newState !== 'open') {
50
+ stopFollowing.current?.()
51
+ stopFollowing.current = null
52
+
53
+ return
54
+ }
55
+
56
+ // El botón es quien apunta a este panel, así que se encuentra por el
57
+ // atributo y no hace falta que nadie lo pase como prop.
58
+ const trigger = document.querySelector(`[popovertarget="${id}"]`)
59
+
60
+ if (! trigger || ! panel.current) {
61
+ return
62
+ }
63
+
64
+ // autoUpdate recoloca al hacer scroll o redimensionar: un menú abierto
65
+ // que se queda flotando donde estaba es peor que uno mal colocado.
66
+ stopFollowing.current = autoUpdate(trigger, panel.current, () => {
67
+ computePosition(trigger, panel.current, {
68
+ placement,
69
+ middleware: [
70
+ offsetMiddleware(offset),
71
+ // Si no cabe abajo, se va arriba; si se sale por un lado,
72
+ // se desplaza para caber.
73
+ flip(),
74
+ shift({ padding: 8 }),
75
+ ],
76
+ }).then(({ x, y }) => {
77
+ Object.assign(panel.current.style, { left: `${x}px`, top: `${y}px` })
78
+ })
79
+ })
80
+ }, [id, offset, placement])
21
81
 
22
82
  useEffect(() => {
23
- globalThis.UIkit?.update?.(host.current)
24
- }, [])
83
+ const node = panel.current
84
+
85
+ node?.addEventListener('beforetoggle', onBeforeToggle)
86
+
87
+ return () => {
88
+ node?.removeEventListener('beforetoggle', onBeforeToggle)
89
+ stopFollowing.current?.()
90
+ }
91
+ }, [onBeforeToggle])
25
92
 
26
93
  return (
27
- <div
28
- ref={host}
29
- id={id}
30
- uk-dropdown={`pos: ${pos}; mode: ${mode}; offset: ${offset}; animation: ${animation}; duration: ${duration};`}
31
- className="fe-p-0 z-10 hidden text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-slate-800 p-2">
32
- <ul className="fe-menu">{children}</ul>
94
+ <div ref={panel} id={id} popover="" className={classFor('menu')}>
95
+ <ul className="fe-menu-list">{children}</ul>
33
96
  </div>
34
97
  )
35
98
  }