fui-material 2.8.3 → 2.8.4

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,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import React, { forwardRef, useRef, useState, useEffect, useMemo, useCallback, useLayoutEffect, Fragment as Fragment$1, isValidElement, cloneElement, createContext, useContext, useDeferredValue } from "react";
2
+ import React, { forwardRef, useRef, useState, useEffect, createContext, isValidElement, Children, Fragment as Fragment$1, cloneElement, useContext, useMemo, useLayoutEffect, useDeferredValue, useCallback, useId } from "react";
3
3
  import ReactDOM, { createPortal } from "react-dom";
4
4
  import JSZip from "jszip";
5
5
  import initializeModule from "allorion-exporting-html-to-docx";
@@ -712,26 +712,39 @@ const FPaper = ({
712
712
  }
713
713
  );
714
714
  };
715
- const bordered = "_bordered_1clfu_18";
716
- const left$1 = "_left_1clfu_40";
717
- const center = "_center_1clfu_43";
718
- const right = "_right_1clfu_46";
715
+ const TableControlsContext = createContext({ query: "", sort: null });
716
+ const left$1 = "_left_blx02_68";
717
+ const center = "_center_blx02_71";
718
+ const right = "_right_blx02_74";
719
+ const justify = "_justify_blx02_77";
720
+ const more = "_more_blx02_141";
719
721
  const styles$t = {
720
- "f-table-component": "_f-table-component_1clfu_1",
721
- "f-table-component__table": "_f-table-component__table_1clfu_7",
722
- bordered,
723
- "bordered-half": "_bordered-half_1clfu_22",
724
- "f-table-component__table_header": "_f-table-component__table_header_1clfu_25",
725
- "is-sticky": "_is-sticky_1clfu_29",
726
- "f-table-component__table_header-cell": "_f-table-component__table_header-cell_1clfu_37",
722
+ "f-table-component": "_f-table-component_blx02_1",
723
+ "f-table-component__table": "_f-table-component__table_blx02_12",
724
+ "f-table-component__table_header": "_f-table-component__table_header_blx02_21",
725
+ "is-sticky": "_is-sticky_blx02_25",
726
+ "f-table-component__table_header-cell": "_f-table-component__table_header-cell_blx02_36",
727
+ "f-table-component__table_body-cell": "_f-table-component__table_body-cell_blx02_36",
728
+ "f-table-component__table_body": "_f-table-component__table_body_blx02_36",
729
+ "f-table-component__table_row": "_f-table-component__table_row_blx02_53",
730
+ "f-table-component__table_row_no-hover": "_f-table-component__table_row_no-hover_blx02_53",
731
+ "f-table-component__table_footer": "_f-table-component__table_footer_blx02_59",
727
732
  left: left$1,
728
733
  center,
729
734
  right,
730
- "f-table-component__table_body": "_f-table-component__table_body_1clfu_65",
731
- "f-table-component__table_row": "_f-table-component__table_row_1clfu_68",
732
- "f-table-component__table_row_no-hover": "_f-table-component__table_row_no-hover_1clfu_68",
733
- "f-table-component__table_body-cell": "_f-table-component__table_body-cell_1clfu_81",
734
- "f-table-component__table_footer": "_f-table-component__table_footer_1clfu_97"
735
+ justify,
736
+ "cell-text": "_cell-text_blx02_81",
737
+ "cell-preview": "_cell-preview_blx02_105",
738
+ "cell-reveal": "_cell-reveal_blx02_116",
739
+ more,
740
+ "row-menu": "_row-menu_blx02_170",
741
+ "table-tools": "_table-tools_blx02_257",
742
+ "table-search": "_table-search_blx02_263",
743
+ "table-sort": "_table-sort_blx02_292",
744
+ "table-empty": "_table-empty_blx02_319",
745
+ "cell-tooltip-anchor": "_cell-tooltip-anchor_blx02_325",
746
+ "cell-tooltip": "_cell-tooltip_blx02_325",
747
+ "cell-tooltip-in": "_cell-tooltip-in_blx02_1"
735
748
  };
736
749
  const FTableHead = ({
737
750
  st,
@@ -752,99 +765,208 @@ const FTableHead = ({
752
765
  }
753
766
  );
754
767
  };
768
+ const TruncateContext = createContext(void 0);
769
+ function tableChildren(children, prefix = "") {
770
+ return Children.toArray(children).flatMap((child) => {
771
+ if (!isValidElement(child)) return [child];
772
+ const key = `${prefix}${child.key}`;
773
+ return child.type === Fragment$1 ? tableChildren(child.props.children, `${key}/`) : [cloneElement(child, { key })];
774
+ });
775
+ }
776
+ function cellText(children) {
777
+ return Children.toArray(children).map((child) => {
778
+ if (typeof child === "string" || typeof child === "number") return String(child);
779
+ if (!isValidElement(child) || child.props.hidden || child.props["aria-hidden"] === true || child.props["aria-hidden"] === "true") return "";
780
+ if (typeof child.type !== "string" && child.type !== Fragment$1) return "";
781
+ if (["button", "input", "select", "textarea", "svg"].includes(String(child.type))) return "";
782
+ return cellText(child.props.children);
783
+ }).join("");
784
+ }
785
+ const normalize = (text) => text.normalize("NFKC").toLocaleLowerCase("ru").replace(/\s+/g, " ").trim();
786
+ const collator = new Intl.Collator("ru", { numeric: true, sensitivity: "base" });
787
+ function indexTableRows(children) {
788
+ const rows = tableChildren(children);
789
+ const sections = [{ heading: [], groups: [] }];
790
+ let section = sections[0];
791
+ let group;
792
+ let spanEnd = 0;
793
+ let columnCount = 1;
794
+ let hasRowSpans = false;
795
+ rows.forEach((row, index) => {
796
+ const cells = isValidElement(row) ? tableChildren(row.props.children).filter(isValidElement) : [];
797
+ columnCount = Math.max(columnCount, cells.reduce((sum, cell) => sum + Math.max(1, cell.props.colSpan ?? cell.props.col ?? 1), 0));
798
+ const heading = index >= spanEnd && cells.length > 0 && cells.every((cell) => !cell.props.sortKey && (cell.props.colSpan ?? cell.props.col ?? 1) > 1);
799
+ if (heading) {
800
+ section = { heading: [row], groups: [] };
801
+ sections.push(section);
802
+ return;
803
+ }
804
+ if (!group || index >= spanEnd) {
805
+ group = { rows: [], text: "", values: /* @__PURE__ */ new Map() };
806
+ section.groups.push(group);
807
+ }
808
+ group.rows.push(row);
809
+ cells.forEach((cell) => {
810
+ const props = cell.props;
811
+ const span = props.rowSpan ?? props.row ?? 1;
812
+ if (span === 0 || span > 1) {
813
+ hasRowSpans = true;
814
+ spanEnd = Math.max(spanEnd, span === 0 ? rows.length : index + span);
815
+ }
816
+ const text = props.searchValue ?? cellText(props.children);
817
+ group.text += ` ${normalize(text)}`;
818
+ if (props.sortKey && !group.values.has(props.sortKey)) {
819
+ group.values.set(props.sortKey, props.sortValue ?? text);
820
+ }
821
+ });
822
+ });
823
+ return { rows, sections, columnCount, hasRowSpans };
824
+ }
825
+ function selectTableRows(index, query, sort) {
826
+ const needle = normalize(query);
827
+ if (!needle && !sort) return index.rows;
828
+ return index.sections.flatMap((section) => {
829
+ const groups = section.groups.filter((group) => !needle || group.text.includes(needle));
830
+ if (sort) groups.sort((a, b) => {
831
+ const left2 = a.values.get(sort.key);
832
+ const right2 = b.values.get(sort.key);
833
+ const empty = (value) => value === void 0 || String(value).trim() === "" || typeof value === "number" && !Number.isFinite(value);
834
+ if (empty(left2) || empty(right2)) return Number(empty(left2)) - Number(empty(right2));
835
+ const comparison = typeof left2 === "number" && typeof right2 === "number" ? left2 - right2 : collator.compare(String(left2), String(right2));
836
+ return sort.direction === "ascending" ? comparison : -comparison;
837
+ });
838
+ return groups.length ? [...section.heading, ...groups.flatMap((group) => group.rows)] : needle ? [] : section.heading;
839
+ });
840
+ }
841
+ function rowAt(offsets, position) {
842
+ let low = 0;
843
+ let high = offsets.length - 1;
844
+ while (low < high) {
845
+ const middle = Math.floor((low + high) / 2);
846
+ if (offsets[middle + 1] <= position) low = middle + 1;
847
+ else high = middle;
848
+ }
849
+ return Math.min(low, Math.max(0, offsets.length - 2));
850
+ }
851
+ function virtualRowRange(offsets, top, viewportHeight, minimumRows) {
852
+ const count = Math.max(0, offsets.length - 1);
853
+ if (!count) return { start: 0, end: 0 };
854
+ const first = rowAt(offsets, Math.max(0, top));
855
+ const last = rowAt(offsets, Math.max(0, top) + Math.max(0, viewportHeight));
856
+ const start = Math.max(0, Math.min(first - 3, count - Math.floor(minimumRows)));
857
+ const end = Math.min(count, Math.max(last + 4, start + Math.floor(minimumRows)));
858
+ return { start, end };
859
+ }
755
860
  const FTableBody = ({
756
861
  st,
757
862
  children,
758
863
  textAlignment = "left",
759
- // По умолчанию выравнивание слева
760
864
  tableWrapperRef,
761
- visibleRowCount = void 0,
865
+ visibleRowCount,
866
+ truncateAt,
762
867
  ...props
763
868
  }) => {
764
869
  const tableBodyRef = useRef(null);
765
- const flatChildren = useMemo(() => React.Children.toArray(children), [children]);
766
- const totalRows = flatChildren.length;
767
- const [startIdx, setStartIdx] = useState(0);
768
- const [rowHeights, setRowHeights] = useState(Array(flatChildren.length).fill(0));
769
- const virtualizationEnabled = typeof visibleRowCount === "number" && visibleRowCount > 0;
870
+ const { query, sort } = useContext(TableControlsContext);
871
+ const index = useMemo(() => indexTableRows(children), [children]);
872
+ const rows = useMemo(() => selectTableRows(index, query, sort), [index, query, sort]);
873
+ const [viewport, setViewport] = useState({ top: 0, height: 0 });
874
+ const [heights, setHeights] = useState(() => /* @__PURE__ */ new Map());
875
+ const virtual = Boolean(tableWrapperRef) && visibleRowCount !== void 0 && Number.isFinite(visibleRowCount) && visibleRowCount >= 1 && !index.hasRowSpans;
876
+ const keyOf = (row, index2) => React.isValidElement(row) ? String(row.key ?? index2) : String(index2);
877
+ const offsets = useMemo(() => {
878
+ const result = [0];
879
+ if (virtual) rows.forEach((row, index2) => result.push(result[index2] + (heights.get(keyOf(row, index2)) ?? 32)));
880
+ return result;
881
+ }, [rows, heights, virtual]);
882
+ const { start, end } = virtual ? virtualRowRange(offsets, viewport.top, viewport.height, visibleRowCount) : { start: 0, end: rows.length };
883
+ const visibleRows = useMemo(() => rows.slice(start, end), [rows, start, end]);
884
+ useLayoutEffect(() => {
885
+ setViewport((previous) => ({ ...previous, top: 0 }));
886
+ if (tableWrapperRef == null ? void 0 : tableWrapperRef.current) tableWrapperRef.current.scrollTop = 0;
887
+ }, [query, sort, tableWrapperRef]);
770
888
  useEffect(() => {
771
- if (rowHeights.length < flatChildren.length) {
772
- setRowHeights((prev) => [...prev, ...Array(flatChildren.length - prev.length).fill(0)]);
773
- }
774
- }, [flatChildren.length, rowHeights.length]);
775
- const updateStartIdx = useCallback(() => {
776
- if (!virtualizationEnabled) return;
777
- if (!(tableWrapperRef == null ? void 0 : tableWrapperRef.current) || !tableBodyRef.current) return;
778
- const wrapper = tableWrapperRef.current;
779
- const scrollTop = wrapper.scrollTop;
780
- let acc = 0;
781
- let idx = 0;
782
- for (let i = 0; i < rowHeights.length; i++) {
783
- if (acc + rowHeights[i] > scrollTop) {
784
- idx = i;
785
- break;
786
- }
787
- acc += rowHeights[i];
788
- }
789
- setStartIdx(idx);
790
- }, [rowHeights, tableWrapperRef, virtualizationEnabled]);
791
- const handleScroll = useCallback(() => {
792
- if (!virtualizationEnabled) return;
793
- requestAnimationFrame(updateStartIdx);
794
- }, [updateStartIdx, virtualizationEnabled]);
889
+ const keys = new Set(index.rows.map(keyOf));
890
+ setHeights((previous) => {
891
+ const next = new Map([...previous].filter(([key]) => keys.has(key)));
892
+ return next.size === previous.size ? previous : next;
893
+ });
894
+ }, [index]);
795
895
  useEffect(() => {
796
- var _a;
797
- if (!virtualizationEnabled) return;
798
- const handleResize = () => requestAnimationFrame(updateStartIdx);
799
- window.addEventListener("resize", handleResize);
800
- (_a = tableWrapperRef == null ? void 0 : tableWrapperRef.current) == null ? void 0 : _a.addEventListener("scroll", handleScroll);
801
- handleResize();
802
- return () => {
803
- var _a2;
804
- window.removeEventListener("resize", handleResize);
805
- (_a2 = tableWrapperRef == null ? void 0 : tableWrapperRef.current) == null ? void 0 : _a2.removeEventListener("scroll", handleScroll);
896
+ const wrapper = tableWrapperRef == null ? void 0 : tableWrapperRef.current;
897
+ if (!virtual || !wrapper) return;
898
+ let frame = 0;
899
+ let width = wrapper.clientWidth;
900
+ const measureScroll = () => {
901
+ cancelAnimationFrame(frame);
902
+ frame = requestAnimationFrame(() => {
903
+ const body = tableBodyRef.current;
904
+ if (body) {
905
+ const top = Math.max(0, wrapper.getBoundingClientRect().top + wrapper.clientTop - body.getBoundingClientRect().top);
906
+ const height = wrapper.clientHeight;
907
+ setViewport((previous) => previous.top === top && previous.height === height ? previous : { top, height });
908
+ }
909
+ });
806
910
  };
807
- }, [handleScroll, updateStartIdx, virtualizationEnabled]);
808
- useLayoutEffect(() => {
809
- if (!tableBodyRef.current) return;
810
- const rows = tableBodyRef.current.querySelectorAll("tr[data-row-index]");
811
- const newHeights = [...rowHeights];
812
- let changed = false;
813
- rows.forEach((row, idx) => {
814
- const realIdx = virtualizationEnabled ? startIdx + idx : idx;
815
- const h = row.getBoundingClientRect().height;
816
- if (h > 0 && newHeights[realIdx] !== h) {
817
- newHeights[realIdx] = h;
818
- changed = true;
911
+ wrapper.addEventListener("scroll", measureScroll);
912
+ const resize = new ResizeObserver(() => {
913
+ var _a;
914
+ if (wrapper.clientWidth !== width) {
915
+ width = wrapper.clientWidth;
916
+ const renderedRows = ((_a = tableBodyRef.current) == null ? void 0 : _a.querySelectorAll(":scope > tr[data-table-row-key]")) ?? [];
917
+ setHeights(new Map(Array.from(renderedRows, (row) => [
918
+ row.getAttribute("data-table-row-key"),
919
+ row.getBoundingClientRect().height
920
+ ])));
819
921
  }
922
+ measureScroll();
820
923
  });
821
- if (changed) setRowHeights(newHeights);
822
- }, [flatChildren, rowHeights, startIdx, virtualizationEnabled]);
823
- let visibleItems;
824
- let topOffset = 0;
825
- let bottomOffset = 0;
826
- if (virtualizationEnabled) {
827
- let getEstimatedHeight = function(index, heights) {
828
- for (let i = index - 1; i >= 0; i--) {
829
- if (heights[i] > 0) return heights[i];
830
- }
831
- for (let i = index + 1; i < heights.length; i++) {
832
- if (heights[i] > 0) return heights[i];
833
- }
834
- return 0;
924
+ resize.observe(wrapper);
925
+ measureScroll();
926
+ return () => {
927
+ cancelAnimationFrame(frame);
928
+ wrapper.removeEventListener("scroll", measureScroll);
929
+ resize.disconnect();
835
930
  };
836
- const endIdx = Math.min(startIdx + visibleRowCount, totalRows);
837
- visibleItems = flatChildren.slice(startIdx, endIdx);
838
- topOffset = rowHeights.slice(0, startIdx).reduce((a, b, i) => a + (b > 0 ? b : getEstimatedHeight(i, rowHeights)), 0);
839
- bottomOffset = rowHeights.slice(endIdx).reduce((a, b, i) => a + (b > 0 ? b : getEstimatedHeight(endIdx + i, rowHeights)), 0);
840
- } else {
841
- visibleItems = flatChildren;
842
- }
843
- return /* @__PURE__ */ jsxs("tbody", { ref: tableBodyRef, style: st, ...props, className: `${styles$t["f-table-component__table_body"]} ${props.className || ""} ${styles$t[textAlignment]}`, children: [
844
- virtualizationEnabled && rowHeights.some((h) => h > 0) && /* @__PURE__ */ jsx("tr", { style: { height: topOffset } }),
845
- visibleItems.map((child, idx) => React.isValidElement(child) ? React.cloneElement(child, { "data-row-index": virtualizationEnabled ? startIdx + idx : idx, key: virtualizationEnabled ? startIdx + idx : idx }) : child),
846
- virtualizationEnabled && rowHeights.some((h) => h > 0) && /* @__PURE__ */ jsx("tr", { style: { height: bottomOffset } })
847
- ] });
931
+ }, [tableWrapperRef, virtual, rows]);
932
+ useLayoutEffect(() => {
933
+ if (!virtual || !tableBodyRef.current) return;
934
+ const observer = new ResizeObserver((entries) => {
935
+ setHeights((previous) => {
936
+ const next = new Map(previous);
937
+ let changed = false;
938
+ entries.forEach(({ target }) => {
939
+ const key = target.getAttribute("data-table-row-key");
940
+ const height = target.getBoundingClientRect().height;
941
+ if (height > 0 && previous.get(key) !== height) {
942
+ next.set(key, height);
943
+ changed = true;
944
+ }
945
+ });
946
+ return changed ? next : previous;
947
+ });
948
+ });
949
+ tableBodyRef.current.querySelectorAll(":scope > tr[data-table-row-key]").forEach((row) => observer.observe(row));
950
+ return () => observer.disconnect();
951
+ }, [virtual, visibleRows]);
952
+ return /* @__PURE__ */ jsx(TruncateContext.Provider, { value: truncateAt, children: /* @__PURE__ */ jsxs(
953
+ "tbody",
954
+ {
955
+ ref: tableBodyRef,
956
+ style: st,
957
+ ...props,
958
+ className: `${styles$t["f-table-component__table_body"]} ${props.className || ""} ${styles$t[textAlignment]}`,
959
+ children: [
960
+ virtual && start > 0 && /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx("td", { colSpan: index.columnCount, style: { height: offsets[start], padding: 0, border: 0 } }) }),
961
+ visibleRows.map((child, index2) => React.isValidElement(child) ? React.cloneElement(child, {
962
+ "data-row-index": start + index2,
963
+ "data-table-row-key": keyOf(child, start + index2)
964
+ }) : child),
965
+ query.trim() && rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: index.columnCount, className: styles$t["table-empty"], children: /* @__PURE__ */ jsx("span", { role: "status", children: "Ничего не найдено" }) }) }),
966
+ virtual && end < rows.length && /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx("td", { colSpan: index.columnCount, style: { height: offsets[rows.length] - offsets[end], padding: 0, border: 0 } }) })
967
+ ]
968
+ }
969
+ ) });
848
970
  };
849
971
  const FTableBody$1 = React.memo(FTableBody);
850
972
  const FTable = ({
@@ -858,10 +980,18 @@ const FTable = ({
858
980
  // По умолчанию заголовок не "липкий"
859
981
  layout = "auto",
860
982
  // Добавляем новый пропс с дефолтным значением 'auto'
983
+ searchable = false,
861
984
  ...props
862
985
  }) => {
863
986
  const tableWrapperRef = useRef(null);
864
987
  const [isStickyState, setIsStickyState] = useState(false);
988
+ const [search, setSearch] = useState("");
989
+ const query = useDeferredValue(searchable ? search : "");
990
+ const [sort, setSort] = useState(null);
991
+ const toggleSort = useCallback((key) => {
992
+ setSort((current) => (current == null ? void 0 : current.key) !== key ? { key, direction: "ascending" } : current.direction === "ascending" ? { key, direction: "descending" } : null);
993
+ }, []);
994
+ const controls = useMemo(() => ({ query, sort, toggleSort }), [query, sort, toggleSort]);
865
995
  useEffect(() => {
866
996
  const handleScroll = () => {
867
997
  if (tableWrapperRef.current && isSticky) {
@@ -879,45 +1009,66 @@ const FTable = ({
879
1009
  }
880
1010
  };
881
1011
  }, []);
882
- return /* @__PURE__ */ jsx(
883
- "div",
884
- {
885
- ref: tableWrapperRef,
886
- className: styles$t["f-table-component"],
887
- style: { overflowX, overflowY },
888
- children: /* @__PURE__ */ jsx(
889
- "table",
1012
+ return /* @__PURE__ */ jsxs(TableControlsContext.Provider, { value: controls, children: [
1013
+ searchable && /* @__PURE__ */ jsx("div", { className: styles$t["table-tools"], children: /* @__PURE__ */ jsxs("label", { className: styles$t["table-search"], children: [
1014
+ /* @__PURE__ */ jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: [
1015
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
1016
+ /* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })
1017
+ ] }),
1018
+ /* @__PURE__ */ jsx(
1019
+ "input",
890
1020
  {
891
- style: { ...st, tableLayout: layout },
892
- ...props,
893
- className: `table ${styles$t["f-table-component__table"]} ${styles$t["bordered"]} ${styles$t["bordered-half"]} ${props.className || ""}`,
894
- children: React.Children.map(children, (child) => {
895
- if (React.isValidElement(child) && child.type === FTableHead) {
896
- const headProps = child.props;
897
- return React.cloneElement(
898
- child,
899
- {
900
- ...headProps,
901
- isSticky: isStickyState
902
- }
903
- );
904
- }
905
- if (React.isValidElement(child) && child.type === FTableBody$1) {
906
- const headProps = child.props;
907
- return React.cloneElement(
908
- child,
909
- {
910
- ...headProps,
911
- tableWrapperRef
912
- }
913
- );
914
- }
915
- return child;
916
- })
1021
+ type: "search",
1022
+ "aria-label": "Поиск по таблице",
1023
+ placeholder: "Поиск",
1024
+ value: search,
1025
+ onChange: (event) => setSearch(event.target.value),
1026
+ onKeyDown: (event) => {
1027
+ if (event.key === "Escape") setSearch("");
1028
+ }
917
1029
  }
918
1030
  )
919
- }
920
- );
1031
+ ] }) }),
1032
+ /* @__PURE__ */ jsx(
1033
+ "div",
1034
+ {
1035
+ ref: tableWrapperRef,
1036
+ className: styles$t["f-table-component"],
1037
+ style: { overflowX, overflowY },
1038
+ children: /* @__PURE__ */ jsx(
1039
+ "table",
1040
+ {
1041
+ style: { ...st, tableLayout: layout },
1042
+ ...props,
1043
+ className: `table ${styles$t["f-table-component__table"]} ${props.className || ""}`,
1044
+ children: React.Children.map(children, (child) => {
1045
+ if (React.isValidElement(child) && child.type === FTableHead) {
1046
+ const headProps = child.props;
1047
+ return React.cloneElement(
1048
+ child,
1049
+ {
1050
+ ...headProps,
1051
+ isSticky: isStickyState
1052
+ }
1053
+ );
1054
+ }
1055
+ if (React.isValidElement(child) && child.type === FTableBody$1) {
1056
+ const headProps = child.props;
1057
+ return React.cloneElement(
1058
+ child,
1059
+ {
1060
+ ...headProps,
1061
+ tableWrapperRef
1062
+ }
1063
+ );
1064
+ }
1065
+ return child;
1066
+ })
1067
+ }
1068
+ )
1069
+ }
1070
+ )
1071
+ ] });
921
1072
  };
922
1073
  const FTableRow = ({
923
1074
  st,
@@ -956,8 +1107,12 @@ const FTableHeaderCell = ({
956
1107
  // По умолчанию выравнивания нет
957
1108
  width = "auto",
958
1109
  // По умолчанию ширина автоматическая
1110
+ sortKey,
959
1111
  ...props
960
1112
  }) => {
1113
+ const { sort, toggleSort } = useContext(TableControlsContext);
1114
+ const sortable = Boolean(sortKey && toggleSort);
1115
+ const direction = (sort == null ? void 0 : sort.key) === sortKey ? sort == null ? void 0 : sort.direction : void 0;
961
1116
  const style = {
962
1117
  textAlign: textAlignment,
963
1118
  width,
@@ -969,12 +1124,101 @@ const FTableHeaderCell = ({
969
1124
  rowSpan: row && row > 1 ? row : void 0,
970
1125
  colSpan: col && col > 1 ? col : void 0,
971
1126
  ...props,
1127
+ "aria-sort": sortable ? direction ?? "none" : props["aria-sort"],
972
1128
  className: `${styles$t["f-table-component__table_header-cell"]} ${props.className || ""}`,
973
1129
  style,
974
- children
1130
+ children: sortable ? /* @__PURE__ */ jsxs(
1131
+ "button",
1132
+ {
1133
+ type: "button",
1134
+ className: styles$t["table-sort"],
1135
+ onClick: () => toggleSort == null ? void 0 : toggleSort(sortKey),
1136
+ title: direction === "ascending" ? "По убыванию" : direction === "descending" ? "Исходный порядок" : "По возрастанию",
1137
+ children: [
1138
+ children,
1139
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: direction === "ascending" ? "↑" : direction === "descending" ? "↓" : "↕" })
1140
+ ]
1141
+ }
1142
+ ) : children
975
1143
  }
976
1144
  );
977
1145
  };
1146
+ function FTableTextTooltip({ text, enabled, children }) {
1147
+ const id = useId();
1148
+ const anchor = useRef(null);
1149
+ const panel2 = useRef(null);
1150
+ const timer = useRef();
1151
+ const [open, setOpen] = useState(false);
1152
+ const [position, setPosition] = useState({ top: 0, left: 0 });
1153
+ const visible2 = enabled && open;
1154
+ const show = () => {
1155
+ clearTimeout(timer.current);
1156
+ setOpen(true);
1157
+ };
1158
+ const hide2 = () => {
1159
+ clearTimeout(timer.current);
1160
+ timer.current = setTimeout(() => setOpen(false), 150);
1161
+ };
1162
+ useEffect(() => () => clearTimeout(timer.current), []);
1163
+ useEffect(() => {
1164
+ if (!enabled) setOpen(false);
1165
+ }, [enabled]);
1166
+ useLayoutEffect(() => {
1167
+ if (!visible2) return;
1168
+ const update = () => {
1169
+ if (!anchor.current || !panel2.current) return;
1170
+ const rect = anchor.current.getBoundingClientRect();
1171
+ const tip = panel2.current.getBoundingClientRect();
1172
+ const top = rect.bottom + 8 + tip.height <= window.innerHeight - 10 ? rect.bottom + 8 : rect.top - tip.height - 8;
1173
+ setPosition({
1174
+ top: Math.max(10, Math.min(top, window.innerHeight - tip.height - 10)),
1175
+ left: Math.max(10, Math.min(rect.left, window.innerWidth - tip.width - 10))
1176
+ });
1177
+ };
1178
+ const escape = (event) => {
1179
+ if (event.key === "Escape") {
1180
+ clearTimeout(timer.current);
1181
+ setOpen(false);
1182
+ }
1183
+ };
1184
+ update();
1185
+ window.addEventListener("resize", update);
1186
+ window.addEventListener("scroll", update, true);
1187
+ document.addEventListener("keydown", escape);
1188
+ return () => {
1189
+ window.removeEventListener("resize", update);
1190
+ window.removeEventListener("scroll", update, true);
1191
+ document.removeEventListener("keydown", escape);
1192
+ };
1193
+ }, [visible2, text]);
1194
+ return /* @__PURE__ */ jsxs(
1195
+ "span",
1196
+ {
1197
+ ref: anchor,
1198
+ className: styles$t["cell-tooltip-anchor"],
1199
+ onMouseEnter: show,
1200
+ onMouseLeave: hide2,
1201
+ onFocus: show,
1202
+ onBlur: hide2,
1203
+ children: [
1204
+ cloneElement(children, { "aria-describedby": visible2 ? id : void 0 }),
1205
+ visible2 && createPortal(/* @__PURE__ */ jsx(
1206
+ "div",
1207
+ {
1208
+ ref: panel2,
1209
+ id,
1210
+ role: "tooltip",
1211
+ className: styles$t["cell-tooltip"],
1212
+ style: position,
1213
+ onMouseEnter: show,
1214
+ onMouseLeave: hide2,
1215
+ children: text
1216
+ }
1217
+ ), document.body)
1218
+ ]
1219
+ }
1220
+ );
1221
+ }
978
1222
  const FTableDataCell = ({
979
1223
  st,
980
1224
  row,
@@ -984,8 +1228,20 @@ const FTableDataCell = ({
984
1228
  // По умолчанию выравнивания нет
985
1229
  height = "auto",
986
1230
  // По умолчанию высота автоматическая
1231
+ truncate,
1232
+ sortKey: _sortKey,
1233
+ sortValue: _sortValue,
1234
+ searchValue: _searchValue,
987
1235
  ...props
988
1236
  }) => {
1237
+ const truncateAt = useContext(TruncateContext);
1238
+ const [expanded, setExpanded] = useState(false);
1239
+ const textId = useId();
1240
+ const limit = truncateAt !== void 0 && Number.isFinite(truncateAt) && truncateAt >= 1 ? Math.floor(truncateAt) : void 0;
1241
+ const text = typeof children === "string" || typeof children === "number" ? String(children) : null;
1242
+ const characters = text === null ? [] : Array.from(text);
1243
+ const threshold = limit ?? 80;
1244
+ const collapsible = truncate !== false && (truncate === true || limit !== void 0) && characters.length > threshold;
989
1245
  const style = {
990
1246
  textAlign: textAlignment,
991
1247
  height,
@@ -999,7 +1255,24 @@ const FTableDataCell = ({
999
1255
  ...props,
1000
1256
  className: `${styles$t["f-table-component__table_body-cell"]} ${props.className || ""}`,
1001
1257
  style,
1002
- children
1258
+ children: collapsible ? /* @__PURE__ */ jsx(FTableTextTooltip, { text, enabled: !expanded, children: /* @__PURE__ */ jsxs(
1259
+ "button",
1260
+ {
1261
+ type: "button",
1262
+ className: styles$t["cell-text"],
1263
+ "aria-expanded": expanded,
1264
+ "aria-controls": textId,
1265
+ "data-expanded": expanded,
1266
+ onClick: () => setExpanded((value) => !value),
1267
+ children: [
1268
+ /* @__PURE__ */ jsxs("span", { className: styles$t["cell-preview"], "aria-hidden": "true", children: [
1269
+ characters.slice(0, threshold).join(""),
1270
+ "…"
1271
+ ] }),
1272
+ /* @__PURE__ */ jsx("span", { className: styles$t["cell-reveal"], children: /* @__PURE__ */ jsx("span", { id: textId, children: text }) })
1273
+ ]
1274
+ }
1275
+ ) }) : children
1003
1276
  }
1004
1277
  );
1005
1278
  };
@@ -4032,6 +4305,87 @@ const FSegmentedControl = ({
4032
4305
  ] }) });
4033
4306
  };
4034
4307
  FSegmentedControl.displayName = "FSegmentedControl";
4308
+ function FTableActions({ children, className, onClick, ...props }) {
4309
+ const id = useId();
4310
+ const menu = useRef(null);
4311
+ const trigger = useRef(null);
4312
+ const [open, setOpen] = useState(false);
4313
+ useEffect(() => {
4314
+ const panel2 = menu.current;
4315
+ const toggle = () => {
4316
+ var _a, _b;
4317
+ const visible2 = panel2.matches(":popover-open");
4318
+ setOpen(visible2);
4319
+ if (visible2) (_a = panel2.querySelector('button:not(:disabled), a[href], input, [tabindex="0"]')) == null ? void 0 : _a.focus();
4320
+ else if (panel2.contains(document.activeElement)) (_b = trigger.current) == null ? void 0 : _b.focus();
4321
+ };
4322
+ panel2.addEventListener("toggle", toggle);
4323
+ return () => panel2.removeEventListener("toggle", toggle);
4324
+ }, []);
4325
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4326
+ /* @__PURE__ */ jsx(
4327
+ "button",
4328
+ {
4329
+ ref: trigger,
4330
+ type: "button",
4331
+ "aria-label": "Действия",
4332
+ ...props,
4333
+ className: `${styles$t.more} ${className || ""}`,
4334
+ "aria-expanded": open,
4335
+ "aria-controls": id,
4336
+ onClick: (event) => {
4337
+ event.stopPropagation();
4338
+ onClick == null ? void 0 : onClick(event);
4339
+ if (event.defaultPrevented || !menu.current) return;
4340
+ const rect = event.currentTarget.getBoundingClientRect();
4341
+ const panel2 = menu.current;
4342
+ panel2.style.maxHeight = `${Math.max(0, (rect.bottom > window.innerHeight / 2 ? rect.top : window.innerHeight - rect.bottom) - 12)}px`;
4343
+ panel2.style.left = `${Math.max(8, Math.min(rect.right - 178, window.innerWidth - 186))}px`;
4344
+ panel2.style.top = `${Math.max(8, rect.bottom + 4)}px`;
4345
+ panel2.style.bottom = "auto";
4346
+ if (rect.bottom > window.innerHeight / 2) {
4347
+ panel2.style.top = "auto";
4348
+ panel2.style.bottom = `${Math.max(8, window.innerHeight - rect.top + 4)}px`;
4349
+ }
4350
+ if (open) panel2.hidePopover();
4351
+ else panel2.showPopover();
4352
+ },
4353
+ children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [
4354
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1.6" }),
4355
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.6" }),
4356
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1.6" })
4357
+ ] })
4358
+ }
4359
+ ),
4360
+ /* @__PURE__ */ jsx(
4361
+ "div",
4362
+ {
4363
+ ref: menu,
4364
+ id,
4365
+ ...{ popover: "auto" },
4366
+ className: styles$t["row-menu"],
4367
+ onKeyDown: (event) => {
4368
+ var _a, _b;
4369
+ if (event.key === "Escape") {
4370
+ event.preventDefault();
4371
+ event.stopPropagation();
4372
+ (_a = trigger.current) == null ? void 0 : _a.focus();
4373
+ (_b = menu.current) == null ? void 0 : _b.hidePopover();
4374
+ }
4375
+ },
4376
+ onClick: (event) => {
4377
+ var _a, _b;
4378
+ event.stopPropagation();
4379
+ if (!event.defaultPrevented && event.target.closest("button:not(:disabled), a[href]")) {
4380
+ (_a = trigger.current) == null ? void 0 : _a.focus();
4381
+ (_b = menu.current) == null ? void 0 : _b.hidePopover();
4382
+ }
4383
+ },
4384
+ children
4385
+ }
4386
+ )
4387
+ ] });
4388
+ }
4035
4389
  const FPlusIcon = React.forwardRef(({
4036
4390
  color = "primary",
4037
4391
  size = 30,
@@ -8954,6 +9308,7 @@ export {
8954
9308
  FStack,
8955
9309
  FTab,
8956
9310
  FTable,
9311
+ FTableActions,
8957
9312
  FTableBody$1 as FTableBody,
8958
9313
  FTableDataCell,
8959
9314
  FTableFooter,