@vygruppen/spor-react 12.24.0 → 12.24.2

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": "@vygruppen/spor-react",
3
3
  "type": "module",
4
- "version": "12.24.0",
4
+ "version": "12.24.2",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -46,8 +46,8 @@
46
46
  "react-stately": "^3.31.1",
47
47
  "react-swipeable": "^7.0.1",
48
48
  "usehooks-ts": "^3.1.0",
49
- "@vygruppen/spor-icon-react": "4.5.1",
50
49
  "@vygruppen/spor-design-tokens": "4.3.2",
50
+ "@vygruppen/spor-icon-react": "4.5.2",
51
51
  "@vygruppen/spor-loader": "0.7.0"
52
52
  },
53
53
  "devDependencies": {
@@ -68,8 +68,8 @@
68
68
  "vitest": "^0.26.3",
69
69
  "vitest-axe": "^0.1.0",
70
70
  "vitest-canvas-mock": "^0.2.2",
71
- "@vygruppen/eslint-config": "2.1.0",
72
- "@vygruppen/tsconfig": "0.1.1"
71
+ "@vygruppen/tsconfig": "0.1.1",
72
+ "@vygruppen/eslint-config": "2.1.0"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "react": ">=18.0.0 <19.0.0",
@@ -113,7 +113,7 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
113
113
  id={id}
114
114
  >
115
115
  {label && !floatingLabel && (
116
- <Label asChild={labelAsChild}>
116
+ <Label asChild={labelAsChild} aria-hidden>
117
117
  {renderLabelWithIndicator(label, labelAsChild)}
118
118
  </Label>
119
119
  )}
@@ -124,6 +124,7 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
124
124
  <FloatingLabel
125
125
  data-float={shouldFloat ? true : undefined}
126
126
  asChild={labelAsChild}
127
+ aria-hidden
127
128
  >
128
129
  {renderLabelWithIndicator(label, labelAsChild)}
129
130
  </FloatingLabel>
@@ -98,7 +98,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
98
98
  shouldFloat={shouldFloat}
99
99
  position="relative"
100
100
  label={
101
- <Box id={labelId} aria-hidden>
101
+ <Box id={labelId}>
102
102
  <label ref={labelRef}>{label}</label>
103
103
  </Box>
104
104
  }
@@ -7,7 +7,6 @@ import {
7
7
  TableBodyProps as ChakraTableBodyProps,
8
8
  TableColumnHeaderProps as ChakraTableColumnHeaderProps,
9
9
  TableRootProps as ChakraTableProps,
10
- TableRowProps as ChakraTableRowProps,
11
10
  useSlotRecipe,
12
11
  } from "@chakra-ui/react";
13
12
  import {
@@ -20,16 +19,17 @@ import {
20
19
  forwardRef,
21
20
  PropsWithChildren,
22
21
  useContext,
23
- useMemo,
22
+ useLayoutEffect,
23
+ useRef,
24
24
  useState,
25
25
  } from "react";
26
26
 
27
27
  import { tableSlotRecipe } from "../theme/slot-recipes/table";
28
28
  import {
29
+ applyDomSort,
30
+ captureRowOrder,
29
31
  getColumnIndex,
30
32
  getNextSortState,
31
- getSortKey,
32
- sortRows,
33
33
  type SortState,
34
34
  } from "./sort-utils";
35
35
 
@@ -38,10 +38,10 @@ type TableVariantProps = RecipeVariantProps<typeof tableSlotRecipe>;
38
38
  const SortContext = createContext<{
39
39
  enabled: boolean;
40
40
  sortState: SortState;
41
- onSort: (key: string, columnIndex: number) => void;
41
+ onSort: (columnIndex: number) => void;
42
42
  }>({
43
43
  enabled: false,
44
- sortState: { key: null, direction: "asc", columnIndex: null },
44
+ sortState: { direction: "asc", columnIndex: null },
45
45
  onSort: () => {},
46
46
  });
47
47
 
@@ -83,14 +83,13 @@ export const Table = forwardRef<HTMLTableElement, TableProps>(
83
83
  ref,
84
84
  ) => {
85
85
  const [sortState, setSortState] = useState<SortState>({
86
- key: null,
87
86
  direction: "asc",
88
87
  columnIndex: null,
89
88
  });
90
89
 
91
- const handleSort = (key: string, columnIndex: number) => {
90
+ const handleSort = (columnIndex: number) => {
92
91
  if (!sortable) return;
93
- setSortState(getNextSortState(sortState, key, columnIndex));
92
+ setSortState(getNextSortState(sortState, columnIndex));
94
93
  };
95
94
 
96
95
  const recipe = useSlotRecipe({ key: "table" });
@@ -123,22 +122,29 @@ export const TableColumnHeader = forwardRef<
123
122
  TableColumnHeaderProps
124
123
  >(({ children, ...rest }, ref) => {
125
124
  const { enabled, sortState, onSort } = useTableSort();
126
- const key = getSortKey(children);
125
+ const [columnIndex, setColumnIndex] = useState<number | null>(null);
127
126
  const props = rest as Record<string, unknown>;
128
- const columnSortable = enabled && key != null && !("data-nosort" in props);
129
- const isActive = columnSortable && key === sortState.key;
127
+ const columnSortable = enabled && !("data-nosort" in props);
128
+ const isActive =
129
+ columnSortable &&
130
+ columnIndex != null &&
131
+ columnIndex === sortState.columnIndex;
130
132
 
131
133
  return (
132
- <ChakraTable.ColumnHeader ref={ref} {...rest}>
134
+ <ChakraTable.ColumnHeader
135
+ ref={(element: HTMLTableCellElement) => {
136
+ if (element) setColumnIndex(getColumnIndex(element));
137
+ if (typeof ref === "function") ref(element);
138
+ else if (ref) ref.current = element;
139
+ }}
140
+ {...rest}
141
+ >
133
142
  <HStack>
134
143
  {children}
135
- {columnSortable && (
144
+ {columnSortable && columnIndex != null && (
136
145
  <Button
137
146
  variant="ghost"
138
- onClick={(event) => {
139
- const th = event.currentTarget.closest("th");
140
- if (th) onSort(key, getColumnIndex(th));
141
- }}
147
+ onClick={() => onSort(columnIndex)}
142
148
  p="0px !important"
143
149
  size="xs"
144
150
  >
@@ -162,30 +168,40 @@ export const TableColumnHeader = forwardRef<
162
168
  });
163
169
  TableColumnHeader.displayName = "ColumnHeader";
164
170
 
165
- export type TableRowProps = ChakraTableRowProps;
166
-
167
- export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
168
- (props, ref) => <ChakraTable.Row ref={ref} {...props} />,
169
- );
170
- TableRow.displayName = "TableRow";
171
-
172
171
  export type TableBodyProps = ChakraTableBodyProps;
173
172
 
174
173
  export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
175
174
  ({ children, ...rest }, ref) => {
176
175
  const { sortState } = useTableSort();
176
+ const tbodyRef = useRef<HTMLTableSectionElement | null>(null);
177
+ const originalOrder = useRef<HTMLTableRowElement[]>([]);
178
+ const previousChildren = useRef(children);
177
179
 
178
- const sorted = useMemo(
179
- () =>
180
- sortState.columnIndex == null
181
- ? children
182
- : sortRows(children, sortState.columnIndex, sortState.direction),
183
- [children, sortState],
184
- );
180
+ useLayoutEffect(() => {
181
+ const tbody = tbodyRef.current;
182
+ if (!tbody) return;
183
+
184
+ if (
185
+ previousChildren.current !== children ||
186
+ originalOrder.current.length === 0
187
+ ) {
188
+ originalOrder.current = captureRowOrder(tbody);
189
+ previousChildren.current = children;
190
+ }
191
+
192
+ applyDomSort(tbody, sortState, originalOrder.current);
193
+ }, [sortState, children]);
185
194
 
186
195
  return (
187
- <ChakraTable.Body ref={ref} {...rest}>
188
- {sorted}
196
+ <ChakraTable.Body
197
+ ref={(element: HTMLTableSectionElement) => {
198
+ tbodyRef.current = element;
199
+ if (typeof ref === "function") ref(element);
200
+ else if (ref) ref.current = element;
201
+ }}
202
+ {...rest}
203
+ >
204
+ {children}
189
205
  </ChakraTable.Body>
190
206
  );
191
207
  },
@@ -6,6 +6,7 @@ export type {
6
6
  TableFooterProps,
7
7
  TableHeaderProps,
8
8
  TableRootProps,
9
+ TableRowProps,
9
10
  } from "@chakra-ui/react";
10
11
  export {
11
12
  TableCaption,
@@ -15,4 +16,5 @@ export {
15
16
  TableFooter,
16
17
  TableHeader,
17
18
  TableRoot,
19
+ TableRow,
18
20
  } from "@chakra-ui/react";
@@ -1,51 +1,48 @@
1
- import { Children, isValidElement, type ReactNode } from "react";
2
-
3
1
  export type SortDirection = "asc" | "desc";
4
2
  export type SortState = {
5
- key: string | null;
6
3
  direction: SortDirection;
7
4
  columnIndex: number | null;
8
5
  };
9
6
 
10
7
  export const getNextSortState = (
11
8
  current: SortState,
12
- key: string,
13
9
  columnIndex: number,
14
10
  ): SortState => {
15
- if (current.key !== key) return { key, columnIndex, direction: "asc" };
16
- if (current.direction === "asc")
17
- return { key, columnIndex, direction: "desc" };
18
- return { key: null, direction: "asc", columnIndex: null }; // Initial sort state
11
+ if (current.columnIndex !== columnIndex)
12
+ return { columnIndex, direction: "asc" };
13
+ if (current.direction === "asc") return { columnIndex, direction: "desc" };
14
+ return { direction: "asc", columnIndex: null };
19
15
  };
20
16
 
21
- export const getSortKey = (children: ReactNode) =>
22
- typeof children === "string" ? children.trim() : null;
23
-
24
17
  export const getColumnIndex = (element: HTMLElement) =>
25
18
  Array.prototype.indexOf.call(element.parentElement?.children, element);
26
19
 
27
- const getCellText = (row: React.ReactElement, columnIndex: number) => {
28
- const cell = Children.toArray(
29
- (row.props as { children?: ReactNode }).children,
30
- )[columnIndex];
31
- if (!isValidElement(cell)) return "";
32
- const props = cell.props as Record<string, unknown>;
33
- return (
34
- (typeof props["data-sort"] === "string" && props["data-sort"]) ||
35
- (typeof props.children === "string" && props.children.trim()) ||
36
- ""
37
- );
20
+ const getCellSortText = (row: HTMLTableRowElement, columnIndex: number) => {
21
+ const cell = row.cells[columnIndex];
22
+ if (!cell) return "";
23
+ return cell.dataset.sort || cell.textContent?.trim() || "";
38
24
  };
39
25
 
40
- export const sortRows = (
41
- children: ReactNode,
42
- columnIndex: number,
43
- direction: SortDirection,
44
- ) =>
45
- Children.toArray(children).toSorted((a, b) => {
46
- if (!isValidElement(a) || !isValidElement(b)) return 0;
47
- const cmp = getCellText(a, columnIndex).localeCompare(
48
- getCellText(b, columnIndex),
49
- );
50
- return direction === "asc" ? cmp : -cmp;
51
- });
26
+ export const applyDomSort = (
27
+ tbody: HTMLTableSectionElement,
28
+ sortState: SortState,
29
+ originalRows: HTMLTableRowElement[],
30
+ ) => {
31
+ if (sortState.columnIndex == null) {
32
+ for (const row of originalRows) tbody.append(row);
33
+ } else {
34
+ // eslint-disable-next-line unicorn/prefer-spread -- HTMLCollectionOf is not spreadable
35
+ const rows = Array.from(tbody.rows);
36
+ rows.sort((a, b) => {
37
+ const cmp = getCellSortText(a, sortState.columnIndex!).localeCompare(
38
+ getCellSortText(b, sortState.columnIndex!),
39
+ );
40
+ return sortState.direction === "asc" ? cmp : -cmp;
41
+ });
42
+ for (const row of rows) tbody.append(row);
43
+ }
44
+ };
45
+
46
+ export const captureRowOrder = (tbody: HTMLTableSectionElement) =>
47
+ // eslint-disable-next-line unicorn/prefer-spread -- HTMLCollectionOf is not spreadable
48
+ Array.from(tbody.rows);
@@ -6,7 +6,7 @@ export const attachedInputsRecipe = defineRecipe({
6
6
  gap: "0.1rem",
7
7
  width: "100%",
8
8
  "& select": {
9
- borderEndRadius: 0,
9
+ borderEndRadius: "0 !important",
10
10
  },
11
11
 
12
12
  "& > *": {
@@ -22,13 +22,13 @@ export const attachedInputsRecipe = defineRecipe({
22
22
  horizontal: {
23
23
  flexDirection: "row",
24
24
  "& > *:first-of-type:not(:last-of-type) [data-attachable]": {
25
- borderEndRadius: 0,
25
+ borderEndRadius: "0 !important",
26
26
  },
27
27
  "& > *:not(:first-of-type):not(:last-of-type) [data-attachable]": {
28
- borderRadius: 0,
28
+ borderRadius: "0 !important",
29
29
  },
30
30
  "& > *:not(:first-of-type):last-of-type [data-attachable]": {
31
- borderStartRadius: 0,
31
+ borderStartRadius: "0 !important",
32
32
  },
33
33
 
34
34
  "&[data-with-flip-button]": {
@@ -45,13 +45,13 @@ export const attachedInputsRecipe = defineRecipe({
45
45
  vertical: {
46
46
  flexDirection: "column",
47
47
  "& > *:first-of-type:not(:last-of-type) [data-attachable]": {
48
- borderBottomRadius: 0,
48
+ borderBottomRadius: "0 !important",
49
49
  },
50
50
  "& > *:not(:first-of-type):not(:last-of-type) [data-attachable]": {
51
- borderRadius: 0,
51
+ borderRadius: "0 !important",
52
52
  },
53
53
  "& > *:not(:first-of-type):last-of-type [data-attachable]": {
54
- borderTopRadius: 0,
54
+ borderTopRadius: "0 !important",
55
55
  },
56
56
  },
57
57
  },
@@ -11,6 +11,11 @@ export const nativeSelectSlotRecipe = defineSlotRecipe({
11
11
  width: "100%",
12
12
  height: "fit-content",
13
13
  position: "relative",
14
+ backgroundColor: "surface",
15
+ "& option, & optgroup": {
16
+ color: "text",
17
+ backgroundColor: "surface",
18
+ },
14
19
  },
15
20
  field: {
16
21
  ...inputRecipe.base,
@@ -1,17 +1,23 @@
1
1
  /** This file works as a proxy for all Chakra UI exports */
2
- export type { PortalProps, UseDisclosureProps } from "@chakra-ui/react";
2
+ export type {
3
+ PortalProps,
4
+ SystemConfig,
5
+ UseDisclosureProps,
6
+ } from "@chakra-ui/react";
3
7
  export {
4
8
  AspectRatio,
5
9
  ClientOnly,
6
10
  createIcon,
7
11
  createListCollection,
8
12
  defineRecipe,
13
+ defineSlotRecipe,
9
14
  defineStyle,
10
15
  For,
11
16
  FormatByte,
12
17
  FormatNumber,
13
18
  Icon,
14
19
  LocaleProvider,
20
+ mergeConfigs,
15
21
  Portal,
16
22
  Show,
17
23
  chakra as spor,