@johly/bits-ui 2.18.8 → 2.18.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.
@@ -139,7 +139,7 @@
139
139
  <MenuSubContent
140
140
  sideOffset={8}
141
141
  align={getSubmenuAlign(meta)}
142
- collisionAvoidance={{ side: "shift", align: "shift" }}
142
+ collisionAvoidance={{ side: "flip", align: "shift" }}
143
143
  >
144
144
  {#if column && meta?.kind === "column" && meta.editor === "text"}
145
145
  <FilterTextEditor column={column as Column<unknown, "text">} {actions} />
@@ -16,6 +16,11 @@ function isSameDay(a, b) {
16
16
  a.getMonth() === b.getMonth() &&
17
17
  a.getDate() === b.getDate());
18
18
  }
19
+ function setEquals(a, b) {
20
+ const aSet = new Set(a);
21
+ const bSet = new Set(b);
22
+ return aSet.size === bSet.size && [...aSet].every((value) => bSet.has(value));
23
+ }
19
24
  export function optionFilterFn(inputData, filterValue) {
20
25
  if (!inputData)
21
26
  return false;
@@ -50,6 +55,10 @@ export function multiOptionFilterFn(inputData, filterValue) {
50
55
  return intersection(values, filterValues).length === filterValues.length;
51
56
  case "exclude if all":
52
57
  return intersection(values, filterValues).length !== filterValues.length;
58
+ case "include exactly":
59
+ return setEquals(values, filterValues);
60
+ case "exclude exactly":
61
+ return !setEquals(values, filterValues);
53
62
  }
54
63
  }
55
64
  export function dateFilterFn(inputData, filterValue) {
@@ -80,9 +89,11 @@ export function dateFilterFn(inputData, filterValue) {
80
89
  case "is on or before":
81
90
  return isSameDay(value, d1) || value.getTime() < startOfDay(d1).getTime();
82
91
  case "is between":
83
- return value.getTime() >= startOfDay(d1).getTime() && value.getTime() <= endOfDay(d2).getTime();
92
+ return (value.getTime() >= startOfDay(d1).getTime() &&
93
+ value.getTime() <= endOfDay(d2).getTime());
84
94
  case "is not between":
85
- return value.getTime() < startOfDay(d1).getTime() || value.getTime() > endOfDay(d2).getTime();
95
+ return (value.getTime() < startOfDay(d1).getTime() ||
96
+ value.getTime() > endOfDay(d2).getTime());
86
97
  }
87
98
  }
88
99
  export function textFilterFn(inputData, filterValue) {
@@ -19,8 +19,10 @@ const en = {
19
19
  "filters.multiOption.includeAnyOf": "includes any of",
20
20
  "filters.multiOption.excludeAllOf": "excludes all of",
21
21
  "filters.multiOption.includeAllOf": "includes all of",
22
+ "filters.multiOption.includeExactly": "includes exactly",
22
23
  "filters.multiOption.excludeIfAnyOf": "excludes if any of",
23
24
  "filters.multiOption.excludeIfAll": "excludes if all of",
25
+ "filters.multiOption.excludeExactly": "excludes exactly",
24
26
  "filters.date.is": "is",
25
27
  "filters.date.isNot": "is not",
26
28
  "filters.date.isBefore": "is before",
@@ -143,7 +143,12 @@ function getOperatorRecord(column) {
143
143
  export function createOperatorNodes({ filter, column, actions, locale, }) {
144
144
  const operators = getOperatorRecord(column);
145
145
  const currentOperator = operators[filter.operator];
146
- const relatedOperators = Object.values(operators).filter((operator) => currentOperator && operator.target === currentOperator.target);
146
+ const currentTarget = currentOperator?.target === "any"
147
+ ? filter.values.length > 1
148
+ ? "multiple"
149
+ : "single"
150
+ : currentOperator?.target;
151
+ const relatedOperators = Object.values(operators).filter((operator) => currentTarget && (operator.target === currentTarget || operator.target === "any"));
147
152
  return [
148
153
  {
149
154
  kind: "radio-group",
@@ -185,7 +190,8 @@ export function createColumnNodes({ columns, filters, actions, locale, strategy,
185
190
  };
186
191
  }
187
192
  if (isNumberColumn(column)) {
188
- const unit = column.meta?.number?.unit;
193
+ const unit = column.meta
194
+ ?.number?.unit;
189
195
  return {
190
196
  kind: "submenu",
191
197
  id: column.id,
@@ -1,5 +1,5 @@
1
1
  import type { ColumnDataType, FilterOperatorTarget, FilterOperators, FilterTypeOperatorDetails, FilterValues } from "./types.js";
2
- export declare const DEFAULT_OPERATORS: Record<ColumnDataType, Record<FilterOperatorTarget, FilterOperators[ColumnDataType]>>;
2
+ export declare const DEFAULT_OPERATORS: Record<ColumnDataType, Record<Exclude<FilterOperatorTarget, "any">, FilterOperators[ColumnDataType]>>;
3
3
  export declare const optionFilterOperators: {
4
4
  readonly is: {
5
5
  readonly key: "filters.option.is";
@@ -80,19 +80,35 @@ export declare const multiOptionFilterOperators: {
80
80
  readonly value: "include all of";
81
81
  readonly target: "multiple";
82
82
  readonly pluralOf: "include";
83
- readonly relativeOf: ["include any of", "exclude if all", "exclude if any of"];
83
+ readonly relativeOf: ["include any of", "exclude if all", "include exactly", "exclude if any of"];
84
84
  readonly isNegated: false;
85
85
  readonly negation: "exclude if any of";
86
86
  };
87
+ readonly "include exactly": {
88
+ readonly key: "filters.multiOption.includeExactly";
89
+ readonly value: "include exactly";
90
+ readonly target: "any";
91
+ readonly relativeOf: ["include", "include any of", "include all of", "exclude exactly"];
92
+ readonly isNegated: false;
93
+ readonly negation: "exclude exactly";
94
+ };
87
95
  readonly "exclude if any of": {
88
96
  readonly key: "filters.multiOption.excludeIfAnyOf";
89
97
  readonly value: "exclude if any of";
90
98
  readonly target: "multiple";
91
99
  readonly pluralOf: "exclude";
92
- readonly relativeOf: ["include any of", "exclude if all", "include all of"];
100
+ readonly relativeOf: ["include any of", "exclude if all", "include all of", "exclude exactly"];
93
101
  readonly isNegated: true;
94
102
  readonly negationOf: "include all of";
95
103
  };
104
+ readonly "exclude exactly": {
105
+ readonly key: "filters.multiOption.excludeExactly";
106
+ readonly value: "exclude exactly";
107
+ readonly target: "any";
108
+ readonly relativeOf: ["exclude", "exclude if any of", "exclude if all", "include exactly"];
109
+ readonly isNegated: true;
110
+ readonly negationOf: "include exactly";
111
+ };
96
112
  };
97
113
  export declare const dateFilterOperators: {
98
114
  readonly is: {
@@ -108,19 +108,35 @@ export const multiOptionFilterOperators = {
108
108
  value: "include all of",
109
109
  target: "multiple",
110
110
  pluralOf: "include",
111
- relativeOf: ["include any of", "exclude if all", "exclude if any of"],
111
+ relativeOf: ["include any of", "exclude if all", "include exactly", "exclude if any of"],
112
112
  isNegated: false,
113
113
  negation: "exclude if any of",
114
114
  },
115
+ "include exactly": {
116
+ key: "filters.multiOption.includeExactly",
117
+ value: "include exactly",
118
+ target: "any",
119
+ relativeOf: ["include", "include any of", "include all of", "exclude exactly"],
120
+ isNegated: false,
121
+ negation: "exclude exactly",
122
+ },
115
123
  "exclude if any of": {
116
124
  key: "filters.multiOption.excludeIfAnyOf",
117
125
  value: "exclude if any of",
118
126
  target: "multiple",
119
127
  pluralOf: "exclude",
120
- relativeOf: ["include any of", "exclude if all", "include all of"],
128
+ relativeOf: ["include any of", "exclude if all", "include all of", "exclude exactly"],
121
129
  isNegated: true,
122
130
  negationOf: "include all of",
123
131
  },
132
+ "exclude exactly": {
133
+ key: "filters.multiOption.excludeExactly",
134
+ value: "exclude exactly",
135
+ target: "any",
136
+ relativeOf: ["exclude", "exclude if any of", "exclude if all", "include exactly"],
137
+ isNegated: true,
138
+ negationOf: "include exactly",
139
+ },
124
140
  };
125
141
  export const dateFilterOperators = {
126
142
  is: {
@@ -289,7 +305,13 @@ export const numberFilterOperators = {
289
305
  value: "is less than or equal to",
290
306
  target: "single",
291
307
  singularOf: "is between",
292
- relativeOf: ["is", "is not", "is greater than", "is less than", "is greater than or equal to"],
308
+ relativeOf: [
309
+ "is",
310
+ "is not",
311
+ "is greater than",
312
+ "is less than",
313
+ "is greater than or equal to",
314
+ ],
293
315
  isNegated: false,
294
316
  negation: "is greater than or equal to",
295
317
  },
@@ -344,7 +366,9 @@ export const filterTypeOperatorDetails = {
344
366
  };
345
367
  export function determineNewOperator(type, oldVals, nextVals, currentOperator) {
346
368
  const a = Array.isArray(oldVals) && Array.isArray(oldVals[0]) ? oldVals[0].length : oldVals.length;
347
- const b = Array.isArray(nextVals) && Array.isArray(nextVals[0]) ? nextVals[0].length : nextVals.length;
369
+ const b = Array.isArray(nextVals) && Array.isArray(nextVals[0])
370
+ ? nextVals[0].length
371
+ : nextVals.length;
348
372
  if (a === b || (a >= 2 && b >= 2) || (a <= 1 && b <= 1))
349
373
  return currentOperator;
350
374
  const opDetails = filterTypeOperatorDetails[type][currentOperator];
@@ -106,7 +106,7 @@ export type BigIntFilterOperator = NumberFilterOperator;
106
106
  export type DateFilterOperator = "is" | "is not" | "is before" | "is on or after" | "is after" | "is on or before" | "is between" | "is not between";
107
107
  export type BooleanFilterOperator = "is" | "is not";
108
108
  export type OptionFilterOperator = "is" | "is not" | "is any of" | "is none of";
109
- export type MultiOptionFilterOperator = "include" | "exclude" | "include any of" | "include all of" | "exclude if any of" | "exclude if all";
109
+ export type MultiOptionFilterOperator = "include" | "exclude" | "include any of" | "include all of" | "include exactly" | "exclude if any of" | "exclude if all" | "exclude exactly";
110
110
  export type FilterOperators = {
111
111
  text: TextFilterOperator;
112
112
  number: NumberFilterOperator;
@@ -126,7 +126,7 @@ export type FiltersState = FilterModel[];
126
126
  export type FilterDetails<T extends ColumnDataType> = {
127
127
  [key in FilterOperators[T]]: FilterOperatorDetails<key, T>;
128
128
  };
129
- export type FilterOperatorTarget = "single" | "multiple";
129
+ export type FilterOperatorTarget = "single" | "multiple" | "any";
130
130
  export type FilterOperatorDetailsBase<OperatorValue, T extends ColumnDataType> = {
131
131
  key: string;
132
132
  value: OperatorValue;
@@ -141,6 +141,10 @@ export type FilterOperatorDetailsBase<OperatorValue, T extends ColumnDataType> =
141
141
  export type FilterOperatorDetails<OperatorValue, T extends ColumnDataType> = FilterOperatorDetailsBase<OperatorValue, T> & ({
142
142
  singularOf?: never;
143
143
  pluralOf?: never;
144
+ } | {
145
+ target: "any";
146
+ singularOf?: never;
147
+ pluralOf?: never;
144
148
  } | {
145
149
  target: "single";
146
150
  singularOf: FilterOperators[T];
@@ -128,6 +128,13 @@
128
128
  .requestAnimationFrame(measureListAlign);
129
129
  }
130
130
 
131
+ function measureListAlignAndQueueRemeasure() {
132
+ if (align !== "list") return;
133
+ clearMeasureFrame();
134
+ measureListAlign();
135
+ queueMeasureListAlign();
136
+ }
137
+
131
138
  $effect(() => {
132
139
  if (align !== "list") {
133
140
  listAlignOffset = 0;
@@ -137,7 +144,7 @@
137
144
  listAlignOffset = 0;
138
145
  return;
139
146
  }
140
- afterTick(queueMeasureListAlign);
147
+ afterTick(measureListAlignAndQueueRemeasure);
141
148
  });
142
149
 
143
150
  $effect(() => {
@@ -205,7 +212,7 @@
205
212
 
206
213
  function handlePlaced() {
207
214
  onPlaced();
208
- queueMeasureListAlign();
215
+ measureListAlignAndQueueRemeasure();
209
216
  }
210
217
 
211
218
  function handleOnFocusOutside(e: FocusEvent) {
@@ -12,6 +12,72 @@ const OPPOSITE_SIDE = {
12
12
  bottom: "top",
13
13
  left: "right",
14
14
  };
15
+ function getPaddingForSide(padding, side) {
16
+ if (typeof padding === "number")
17
+ return padding;
18
+ return padding?.[side] ?? 0;
19
+ }
20
+ function parsePixelValue(value) {
21
+ const parsed = Number.parseFloat(value);
22
+ return Number.isFinite(parsed) ? parsed : 0;
23
+ }
24
+ function replacePlacementSide(placement, side) {
25
+ const [, align] = placement.split("-");
26
+ return (side + (align ? `-${align}` : ""));
27
+ }
28
+ function getFloatingContentNode(floating) {
29
+ return floating.firstElementChild instanceof HTMLElement
30
+ ? floating.firstElementChild
31
+ : floating instanceof HTMLElement
32
+ ? floating
33
+ : null;
34
+ }
35
+ function getIntendedFloatingWidth(floating, measuredWidth) {
36
+ const contentNode = getFloatingContentNode(floating);
37
+ if (!contentNode)
38
+ return measuredWidth;
39
+ const style = contentNode.ownerDocument.defaultView?.getComputedStyle(contentNode);
40
+ const rowWidth = style ? parsePixelValue(style.getPropertyValue("--row-width")) : 0;
41
+ const isSubmenu = contentNode.hasAttribute("data-menu-sub-content") ||
42
+ contentNode.hasAttribute("data-dropdown-menu-sub-content") ||
43
+ contentNode.hasAttribute("data-context-menu-sub-content") ||
44
+ contentNode.hasAttribute("data-menubar-sub-content");
45
+ return Math.max(measuredWidth, rowWidth, isSubmenu ? 250 : 0);
46
+ }
47
+ function preferOppositeSideWithRoom(opts) {
48
+ return {
49
+ name: "preferOppositeSideWithRoom",
50
+ async fn(state) {
51
+ const side = getSideFromPlacement(state.placement);
52
+ if (side !== "right" && side !== "left")
53
+ return {};
54
+ const referenceNode = state.elements.reference;
55
+ if (!("getBoundingClientRect" in referenceNode))
56
+ return {};
57
+ const referenceRect = referenceNode.getBoundingClientRect();
58
+ const floatingNode = state.elements.floating;
59
+ const win = getWindow(floatingNode);
60
+ const padding = opts.collisionPadding();
61
+ const sideOffset = opts.sideOffset();
62
+ const desiredWidth = getIntendedFloatingWidth(floatingNode, state.rects.floating.width);
63
+ const leftRoom = referenceRect.left - getPaddingForSide(padding, "left") - sideOffset;
64
+ const rightRoom = win.innerWidth -
65
+ referenceRect.right -
66
+ getPaddingForSide(padding, "right") -
67
+ sideOffset;
68
+ const currentRoom = side === "right" ? rightRoom : leftRoom;
69
+ const oppositeSide = OPPOSITE_SIDE[side];
70
+ const oppositeRoom = oppositeSide === "right" ? rightRoom : leftRoom;
71
+ if (currentRoom >= desiredWidth || oppositeRoom < desiredWidth)
72
+ return {};
73
+ return {
74
+ reset: {
75
+ placement: replacePlacementSide(state.placement, oppositeSide),
76
+ },
77
+ };
78
+ },
79
+ };
80
+ }
15
81
  const FloatingRootContext = new Context("Floating.Root");
16
82
  const FloatingContentContext = new Context("Floating.Content");
17
83
  const FloatingTooltipRootContext = new Context("Floating.Root");
@@ -96,6 +162,12 @@ export class FloatingContentState {
96
162
  mainAxis: this.opts.sideOffset.current + this.#arrowHeight,
97
163
  alignmentAxis: this.opts.alignOffset.current,
98
164
  }),
165
+ this.opts.avoidCollisions.current &&
166
+ this.#collisionAvoidance.side === "flip" &&
167
+ preferOppositeSideWithRoom({
168
+ sideOffset: () => this.opts.sideOffset.current + this.#arrowHeight,
169
+ collisionPadding: () => this.opts.collisionPadding.current,
170
+ }),
99
171
  this.opts.avoidCollisions.current &&
100
172
  (this.#collisionAvoidance.side === "shift" ||
101
173
  this.#collisionAvoidance.align === "shift") &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johly/bits-ui",
3
- "version": "2.18.8",
3
+ "version": "2.18.10",
4
4
  "license": "MIT",
5
5
  "repository": "github:johanohly/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",