kelt-ui-kit-react 2.0.9 → 2.1.1

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,7 +1,7 @@
1
1
  {
2
2
  "name": "kelt-ui-kit-react",
3
3
  "type": "module",
4
- "version": "2.0.9",
4
+ "version": "2.1.1",
5
5
  "private": false,
6
6
  "repository": {
7
7
  "type": "git",
@@ -61,4 +61,4 @@
61
61
  "typescript": "^4.9.5",
62
62
  "vite-plugin-checker": "^0.8.0"
63
63
  }
64
- }
64
+ }
@@ -4,17 +4,11 @@ import { Icon } from "../../icon/Icon";
4
4
  import { IconSizeEnum } from "../../icon/iconSize.enum";
5
5
  import { MenuInterface } from "../../menus/menu/menu.interface";
6
6
  import { Menus } from "../../menus/Menus";
7
- import { OverlayPanel } from "../../overlayPanel/OverlayPanel";
7
+ import { OverlayPanel, PositionOverlay } from "../../overlayPanel/OverlayPanel";
8
8
  import { CardInterface } from "../card.interface";
9
9
  import { CardActionInterface } from "../cardAction.interface";
10
10
  import "./cardAction.css";
11
- export type PositionOverlay =
12
- | "left"
13
- | "right"
14
- | "topLeft"
15
- | "topRight"
16
- | "bottomLeft"
17
- | "bottomRight";
11
+ export type { PositionOverlay };
18
12
  export type CardActionProps = {
19
13
  actions: CardActionInterface[];
20
14
  item?: CardInterface;
@@ -22,6 +22,7 @@ export type {
22
22
  DataTableDateRangeFilterValue, DataTableFilterState, DataTableSortDirection,
23
23
  DataTableSortState, FilterValue,
24
24
  } from "./dataTable.utils";
25
+
25
26
  export type DataTableProps<T> = {
26
27
  id: string;
27
28
  name?: string;
@@ -91,9 +92,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
91
92
  const tableState = useDataTableState(id, groupedColumns, sourceData, onFiltersChange, onSortChange);
92
93
  const { flatColumns: columns, sortedData: data } = tableState;
93
94
  const actionsColumnWidth = getActionsWidth(actions, explicitActionsWidth);
94
- const filterRowRef = useRef<HTMLTableRowElement>(null);
95
95
  const groupRowRef = useRef<HTMLTableRowElement>(null);
96
- const [filterHeight, setFilterHeight] = useState(0);
97
96
  const [groupHeight, setGroupHeight] = useState(0);
98
97
  const hasGroups = groupedColumns.some((column) => column.children?.length);
99
98
  const stickyOffsets = useMemo(() => {
@@ -110,7 +109,6 @@ export function DataTable<T>(props: DataTableProps<T>) {
110
109
 
111
110
  useLayoutEffect(() => {
112
111
  const measure = () => {
113
- setFilterHeight(filterRowRef.current?.getBoundingClientRect().height ?? 0);
114
112
  setGroupHeight(groupRowRef.current?.getBoundingClientRect().height ?? 0);
115
113
  };
116
114
  measure();
@@ -119,10 +117,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
119
117
  return () => window.removeEventListener("resize", measure);
120
118
  }
121
119
  const observer = new ResizeObserver(measure);
122
- if (filterRowRef.current) observer.observe(filterRowRef.current);
123
120
  if (groupRowRef.current) observer.observe(groupRowRef.current);
124
121
  return () => observer.disconnect();
125
- }, [tableState.visibleFilters.size, hasGroups, groupedColumns]);
122
+ }, [hasGroups, groupedColumns]);
126
123
 
127
124
  const isMobile = useIsMobile();
128
125
  const showMobileCardView = mobileCardView && isMobile;
@@ -529,8 +526,8 @@ export function DataTable<T>(props: DataTableProps<T>) {
529
526
  isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
530
527
  onToggleFilter={tableState.toggleFilter} onToggleSort={tableState.toggleSort}
531
528
  sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
529
+ filterControl={tableState.renderFilter(column)}
532
530
  /></tr></thead></table>
533
- {tableState.renderFilter(column)}
534
531
  </div>
535
532
  ))}
536
533
  </div>
@@ -641,26 +638,17 @@ export function DataTable<T>(props: DataTableProps<T>) {
641
638
  {actions.length > 0 && <col style={{ width: actionsColumnWidth }} />}
642
639
  </colgroup>
643
640
  <thead>
644
- {tableState.visibleFilters.size > 0 && <tr ref={filterRowRef}>
645
- {onSelectionChange && <th />}
646
- {columns.map((column) => <th className="filter-row-cell" key={column.id}
647
- style={{ position: "sticky", top: 0, zIndex: 14, background: "#f9fbfc", minWidth: column.minWidth,
648
- ...(column.sticky ? { left: stickyOffsets.get(column.id) } : {}) }}>
649
- {tableState.renderFilter(column)}
650
- </th>)}
651
- {actions.length > 0 && <th />}
652
- </tr>}
653
641
  {hasGroups && <tr ref={groupRowRef} className="data-table-group-row">
654
642
  {onSelectionChange && <th />}
655
643
  {groupedColumns.map((column) => <th key={column.id} colSpan={flattenColumns([column]).length}
656
- style={{ top: filterHeight }}>{column.children?.length ? column.label : ""}</th>)}
644
+ style={{ top: 0 }}>{column.children?.length ? column.label : ""}</th>)}
657
645
  {actions.length > 0 && <th />}
658
646
  </tr>}
659
647
  <tr>
660
648
  {onSelectionChange && (
661
649
  <th
662
650
  className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
663
- style={{ width: "40px", textAlign: "center", top: filterHeight + groupHeight,
651
+ style={{ width: "40px", textAlign: "center", top: groupHeight,
664
652
  ...(stickyOffsets.size > 0 ? { left: 0, zIndex: 12 } : {}) }}
665
653
  >
666
654
  <Input
@@ -681,11 +669,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
681
669
  idx={idx}
682
670
  onColumnClick={onColumnClick}
683
671
  stickyLeft={stickyOffsets.get(column.id)}
684
- stickyTop={filterHeight + groupHeight}
672
+ stickyTop={groupHeight}
685
673
  isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
686
674
  onToggleFilter={tableState.toggleFilter}
687
675
  onToggleSort={tableState.toggleSort}
688
676
  sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
677
+ filterControl={tableState.renderFilter(column)}
689
678
  />
690
679
  ))}
691
680
  {actions.length > 0 && (
@@ -135,6 +135,102 @@
135
135
  color: var(--tertiary-color);
136
136
  min-width: 100px;
137
137
  padding: .5rem;
138
+
139
+ .data-table-column-filter,
140
+ .data-table-column-sort {
141
+ visibility: hidden;
142
+
143
+ &.active {
144
+ visibility: visible;
145
+ }
146
+ }
147
+
148
+ .data-table-column-filter-active,
149
+ .data-table-column-sort-active {
150
+ color: var(--tertiary-color);
151
+ }
152
+
153
+ &:hover,
154
+ &:focus-within {
155
+ .data-table-column-filter,
156
+ .data-table-column-sort {
157
+ visibility: visible;
158
+ }
159
+ }
160
+ }
161
+
162
+ .data-table-filter-row {
163
+ th {
164
+ padding-top: 6px;
165
+ padding-bottom: 6px;
166
+ overflow: visible;
167
+ }
168
+
169
+ .data-table-filter-select {
170
+ width: 100%;
171
+ max-width: 100%;
172
+
173
+ button {
174
+ width: 100%;
175
+ max-width: 100%;
176
+ overflow: hidden;
177
+ padding: 4px 6px;
178
+ border: 1px solid #dedede;
179
+ }
180
+
181
+ .select-container-value {
182
+ display: block;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ font-size: 0.7rem;
188
+ }
189
+ }
190
+
191
+ .data-table-filter-input-wrapper {
192
+ width: 100%;
193
+ max-width: 100%;
194
+ overflow: visible;
195
+ box-sizing: border-box;
196
+
197
+ .input-clear-icon,
198
+ .input-clear-btn,
199
+ .clear-btn {
200
+ right: 5px;
201
+
202
+ .bi {
203
+ font-size: 0.8rem;
204
+ }
205
+ }
206
+
207
+ .input-container {
208
+ min-width: 0;
209
+ width: 100%;
210
+ }
211
+
212
+ .select-container,
213
+ .select-container-button {
214
+ max-width: 100%;
215
+ min-width: 0;
216
+ }
217
+ }
218
+
219
+ .data-table-filter-input {
220
+ width: 100%;
221
+ max-width: 100%;
222
+ min-width: 0;
223
+ box-sizing: border-box;
224
+ overflow: hidden;
225
+ text-overflow: ellipsis;
226
+ white-space: nowrap;
227
+ border: 1px solid #c9cfdb;
228
+ border-radius: 6px;
229
+ padding: 4px 6px;
230
+ font-size: 12px;
231
+ background-color: #fff;
232
+ color: #333;
233
+ }
138
234
  }
139
235
  }
140
236
 
@@ -214,7 +310,20 @@
214
310
  .data-table-column-heading {
215
311
  display: flex;
216
312
  align-items: center;
217
- gap: 8px;
313
+ gap: 4px;
314
+
315
+ > .flex-1 {
316
+ flex: 1;
317
+ }
318
+ }
319
+ .data-table-filter-popover {
320
+ z-index: 1200;
321
+ padding: 8px;
322
+ box-sizing: border-box;
323
+ border: 1px solid #dce3e7;
324
+ border-radius: 7px;
325
+ background: #fff;
326
+ box-shadow: 0 6px 18px rgba(20, 40, 50, 0.16);
218
327
  }
219
328
  .data-table-group-row th {
220
329
  height: 40px;
@@ -1,3 +1,5 @@
1
+ import { ReactNode, useLayoutEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
1
3
  import { dataTableColumnsInterface } from "../dataTable.interface";
2
4
  import { DataTableColumnFilter } from "./dataTableColumnFilter/DataTableColumnFilter";
3
5
  import { DataTableColumnSort } from "./dataTableColumnSort/DataTableColumnSort";
@@ -13,7 +15,62 @@ type DataTableColumnProps = {
13
15
  sortDirection?: DataTableSortDirection | null;
14
16
  stickyLeft?: number;
15
17
  stickyTop?: number;
18
+ filterControl?: ReactNode;
16
19
  };
20
+
21
+ function FilterPopover({
22
+ anchor,
23
+ label,
24
+ children,
25
+ }: {
26
+ anchor: HTMLElement;
27
+ label: string;
28
+ children: ReactNode;
29
+ }) {
30
+ const [style, setStyle] = useState<React.CSSProperties>();
31
+
32
+ useLayoutEffect(() => {
33
+ const updatePosition = () => {
34
+ const rect = anchor.getBoundingClientRect();
35
+ if (!rect.width || !rect.height) {
36
+ setStyle(undefined);
37
+ return;
38
+ }
39
+ const width = Math.min(Math.max(rect.width, 220), window.innerWidth - 24);
40
+ setStyle({
41
+ position: "fixed",
42
+ top: Math.min(rect.bottom + 4, window.innerHeight - 80),
43
+ left: Math.max(12, Math.min(rect.left, window.innerWidth - width - 12)),
44
+ width,
45
+ });
46
+ };
47
+
48
+ updatePosition();
49
+ window.addEventListener("resize", updatePosition);
50
+ window.addEventListener("scroll", updatePosition, true);
51
+ return () => {
52
+ window.removeEventListener("resize", updatePosition);
53
+ window.removeEventListener("scroll", updatePosition, true);
54
+ };
55
+ }, [anchor]);
56
+
57
+ return style && typeof document !== "undefined"
58
+ ? createPortal(
59
+ <div
60
+ className="data-table-filter-popover"
61
+ style={style}
62
+ role="group"
63
+ aria-label={`Filtrer ${label}`}
64
+ onMouseDown={(event) => event.stopPropagation()}
65
+ onClick={(event) => event.stopPropagation()}
66
+ >
67
+ {children}
68
+ </div>,
69
+ document.body,
70
+ )
71
+ : null;
72
+ }
73
+
17
74
  export const DataTableColumn = ({
18
75
  column,
19
76
  idx,
@@ -24,9 +81,12 @@ export const DataTableColumn = ({
24
81
  sortDirection,
25
82
  stickyLeft,
26
83
  stickyTop = 0,
84
+ filterControl,
27
85
  }: DataTableColumnProps) => {
86
+ const headerRef = useRef<HTMLTableCellElement>(null);
28
87
  return (
29
88
  <th
89
+ ref={headerRef}
30
90
  key={`colonne-${column.id}-${idx}`}
31
91
  style={{
32
92
  minWidth: column.minWidth,
@@ -39,10 +99,18 @@ export const DataTableColumn = ({
39
99
  onClick={() => onColumnClick?.(column)}
40
100
  >
41
101
  <div className="data-table-column-heading">
42
- <span>{column.label}</span>
43
- {column.sortable && <DataTableColumnSort column={column} sortDirection={sortDirection} onToggleSort={onToggleSort} />}
102
+ <span className="flex-1">{column.label}</span>
44
103
  {column.filterable && <DataTableColumnFilter column={column} isFilterVisible={isFilterVisible} onToggleVisibility={onToggleFilter} />}
104
+ {column.sortable && <DataTableColumnSort column={column} sortDirection={sortDirection} onToggleSort={onToggleSort} />}
45
105
  </div>
106
+ {isFilterVisible && filterControl && headerRef.current && (
107
+ <FilterPopover
108
+ anchor={headerRef.current}
109
+ label={column.label}
110
+ >
111
+ {filterControl}
112
+ </FilterPopover>
113
+ )}
46
114
  </th>
47
115
  );
48
116
  };
@@ -67,7 +67,9 @@ export const DataTableRow = <T,>({
67
67
  column.onChange?.(next, row);
68
68
  commit(next);
69
69
  }}
70
- style={{ minWidth: column.minWidth, maxWidth: column.maxWidth, width: column.width }} />
70
+ style={{ minWidth: column.minWidth, maxWidth: column.maxWidth, width: column.width,
71
+ // le bouton du Select est un bloc : textAlign ne l'aligne pas
72
+ ...(align === "right" ? { marginLeft: "auto" } : align === "center" ? { marginInline: "auto" } : {}) }} />
71
73
  ) : (
72
74
  <input ref={inputRef} type={column.type === "image" ? "text" : column.type}
73
75
  value={enterCommit ? draft : stringValue}
@@ -60,6 +60,11 @@ export function useDataTableState<T>(
60
60
  }
61
61
 
62
62
  function renderFilter(column: dataTableColumnsInterface<T>) {
63
+ const control = renderFilterControl(column);
64
+ return control && <div className="data-table-filter-input-wrapper">{control}</div>;
65
+ }
66
+
67
+ function renderFilterControl(column: dataTableColumnsInterface<T>) {
63
68
  const key = getFilterKey(column);
64
69
  if (!column.filterable || !visibleFilters.has(key)) return null;
65
70
  const value = filters[key];
@@ -72,7 +77,7 @@ export function useDataTableState<T>(
72
77
  }
73
78
  if (column.filterType === TypeInputEnum.CHECKBOX || column.filterType === TypeInputEnum.RADIO) {
74
79
  return <Select id={inputId} options={column.filterDataEnum ?? []} multiple
75
- usePortal showOptions={focusKey === key}
80
+ className="data-table-filter-select" classNameOverlay="data-table-filter-select-overlay" usePortal showOptions={focusKey === key}
76
81
  value={String(value ?? "").split(",").filter(Boolean)}
77
82
  onChangeMultiple={(values) => updateFilter(key, values.join(","))}
78
83
  placeholder={`Filtrer ${column.label}`} />;
@@ -81,6 +86,7 @@ export function useDataTableState<T>(
81
86
  type={column.filterType === TypeInputEnum.DATE ? TypeInputEnum.DATE : TypeInputEnum.TEXT}
82
87
  value={String(value ?? "")} autoFocus={focusKey === key}
83
88
  disabledDebounce showClearButton placeholder={`Filtrer ${column.label}`}
89
+ className="data-table-filter-input"
84
90
  onClick={(event) => event.stopPropagation()}
85
91
  onChange={(event) => updateFilter(key, event.target.value)} />;
86
92
  }
@@ -20,6 +20,7 @@ export type PositionOverlay =
20
20
  | "right"
21
21
  | "topLeft"
22
22
  | "topRight"
23
+ | "bottom"
23
24
  | "bottomLeft"
24
25
  | "bottomRight";
25
26
 
@@ -107,6 +108,10 @@ export const OverlayPanel = forwardRef(
107
108
  top: rect.bottom,
108
109
  left: rect.left,
109
110
  }),
111
+ bottom: () => ({
112
+ top: rect.bottom,
113
+ left: rect.left + rect.width / 2 - boxRect.width / 2,
114
+ }),
110
115
  left: () => ({
111
116
  top: rect.top + rect.height / 2 - boxRect.height / 2,
112
117
  left: rect.left - boxRect.width,
@@ -122,7 +127,7 @@ export const OverlayPanel = forwardRef(
122
127
  ({ top, left } = positions[position]());
123
128
  // Manque d'espace en bas → afficher au-dessus de l'élément
124
129
  if (
125
- (position === "bottomLeft" || position === "bottomRight") &&
130
+ (position === "bottom" || position === "bottomLeft" || position === "bottomRight") &&
126
131
  top + boxRect.height > innerHeight &&
127
132
  rect.top - boxRect.height >= 0
128
133
  ) {
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Icon } from "../icon/Icon";
3
3
 
4
- import { OverlayPanel } from "../overlayPanel/OverlayPanel";
4
+ import { OverlayPanel, PositionOverlay } from "../overlayPanel/OverlayPanel";
5
5
  import "./select.css";
6
6
  import { SelectOptionInterface } from "./selectOption.interface";
7
7
 
@@ -26,6 +26,7 @@ export interface SelectProps {
26
26
  onChange?: (value: string) => void;
27
27
  placeholder?: string;
28
28
  classNameOverlay?: string;
29
+ positionOverlay?: PositionOverlay;
29
30
  }
30
31
 
31
32
  export const Select = ({
@@ -46,6 +47,7 @@ export const Select = ({
46
47
  value,
47
48
  placeholder,
48
49
  classNameOverlay,
50
+ positionOverlay = "bottomLeft",
49
51
  }: SelectProps): JSX.Element => {
50
52
  const [showOverlay, setShowOverlay] = useState(false);
51
53
  const [overlayRef, setOverlayRef] = useState<HTMLElement | null>(null);
@@ -143,7 +145,7 @@ export const Select = ({
143
145
  }, [showOptions, disabled]);
144
146
 
145
147
  return (
146
- <div id={id} className={`select-container ${className ? className : ""}`}>
148
+ <div id={id} className={`select-container ${hasSelection && placeholder ? "has-selection" : ""} ${className ? className : ""}`}>
147
149
  {label && (
148
150
  <label htmlFor={`select-${id}`} className="select-container-label">
149
151
  {label}
@@ -175,7 +177,7 @@ export const Select = ({
175
177
  {showOverlay && overlayRef && (
176
178
  <OverlayPanel
177
179
  show={showOverlay}
178
- position="bottomLeft"
180
+ position={positionOverlay}
179
181
  className={classNameOverlay ?? ""}
180
182
  closeOverlay={() => setShowOverlay(false)}
181
183
  referenceElement={overlayRef}
@@ -1,15 +1,6 @@
1
1
  .select-container {
2
2
  position: relative;
3
3
 
4
- &::after,
5
- &::before {
6
- --size: 0.3rem;
7
- content: "";
8
- position: absolute;
9
- right: 1rem;
10
- pointer-events: none;
11
- }
12
-
13
4
  .clear-btn {
14
5
  position: absolute;
15
6
  top: 50%;
@@ -18,22 +9,13 @@
18
9
  align-items: center;
19
10
  z-index: 2;
20
11
  justify-content: center;
21
- right: 10px;
12
+ /* à gauche de la flèche (dessinée à right: 1rem, largeur 0.6rem) */
13
+ right: 2rem;
22
14
  cursor: pointer;
23
15
  }
24
16
 
25
- &::before {
26
- border-left: var(--size) solid transparent;
27
- border-right: var(--size) solid transparent;
28
- border-bottom: var(--size) solid black;
29
- top: 40%;
30
- }
31
-
32
- &::after {
33
- border-left: var(--size) solid transparent;
34
- border-right: var(--size) solid transparent;
35
- border-top: var(--size) solid black;
36
- top: 55%;
17
+ &.has-selection .select-container-button {
18
+ padding-right: 3.5rem;
37
19
  }
38
20
 
39
21
  .select-container-label {
@@ -45,6 +27,7 @@
45
27
 
46
28
  .select-container-button {
47
29
  appearance: none;
30
+ position: relative;
48
31
  width: 100%;
49
32
  font-size: 0.8rem;
50
33
  padding: 0.5rem 1rem;
@@ -60,6 +43,29 @@
60
43
  justify-content: space-between;
61
44
  padding-right: 2rem;
62
45
 
46
+ &::before,
47
+ &::after {
48
+ --size: 0.3rem;
49
+ content: "";
50
+ position: absolute;
51
+ right: 1rem;
52
+ pointer-events: none;
53
+ }
54
+
55
+ &::before {
56
+ border-left: var(--size) solid transparent;
57
+ border-right: var(--size) solid transparent;
58
+ border-bottom: var(--size) solid black;
59
+ top: calc(50% - var(--size));
60
+ }
61
+
62
+ &::after {
63
+ border-left: var(--size) solid transparent;
64
+ border-right: var(--size) solid transparent;
65
+ border-top: var(--size) solid black;
66
+ top: calc(50% + 1px);
67
+ }
68
+
63
69
  .select-container-value {
64
70
  flex: 1;
65
71
  min-width: 0;