@agilant/toga-blox 1.0.319-beta.82 → 1.0.319-beta.83

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.
@@ -10,14 +10,21 @@ const FILTER_MODE_LABELS = {
10
10
  includes: "Includes",
11
11
  excludes: "Excludes",
12
12
  };
13
+ // Keep in sync with .headerFilterMenu width in toga.module.css
14
+ const HEADER_FILTER_MENU_WIDTH = 329;
13
15
  export const HeaderFilterSearch = ({ columnId, filterValue, filterMode: filterModeProp, filterIndex, onFilterChange, onClear, skin, }) => {
14
16
  const [isOpen, setIsOpen] = useState(false);
15
17
  const [portalStyles, setPortalStyles] = useState({});
16
18
  const [isModeOpen, setIsModeOpen] = useState(false);
17
19
  const [filterMode, setFilterMode] = useState("includes");
18
20
  const [inputValue, setInputValue] = useState(filterValue);
21
+ const [isFocused, setIsFocused] = useState(false);
19
22
  const [activeTags, setActiveTags] = useState(filterValue ? [filterValue] : []);
20
23
  const [modeDropdownPos, setModeDropdownPos] = useState(null);
24
+ // When centering would clip the menu off the left edge of the scroll
25
+ // container (e.g. the first column), left-align it so it extends right.
26
+ const [alignLeft, setAlignLeft] = useState(false);
27
+ const triggerRef = useRef(null);
21
28
  const inputRef = useRef(null);
22
29
  const modeButtonRef = useRef(null);
23
30
  const isActive = activeTags.length > 0;
@@ -65,6 +72,22 @@ export const HeaderFilterSearch = ({ columnId, filterValue, filterMode: filterMo
65
72
  setActiveTags([filterValue]);
66
73
  }
67
74
  }, [filterValue]);
75
+ const handleTriggerClick = (e) => {
76
+ e.stopPropagation();
77
+ // Only recompute alignment when opening.
78
+ if (!isOpen) {
79
+ const btnRect = triggerRef.current?.getBoundingClientRect();
80
+ const scroller = triggerRef.current?.closest('[class*="scrollWrapper"]');
81
+ const containerLeft = scroller?.getBoundingClientRect().left ?? 0;
82
+ if (btnRect) {
83
+ const centeredLeft = btnRect.left +
84
+ btnRect.width / 2 -
85
+ HEADER_FILTER_MENU_WIDTH / 2;
86
+ setAlignLeft(centeredLeft < containerLeft);
87
+ }
88
+ }
89
+ setIsOpen((prev) => !prev);
90
+ };
68
91
  const handleModeButtonClick = () => {
69
92
  if (isModeOpen) {
70
93
  setIsModeOpen(false);
@@ -109,14 +132,13 @@ export const HeaderFilterSearch = ({ columnId, filterValue, filterMode: filterMo
109
132
  else
110
133
  onFilterChange(next.join(","), filterMode);
111
134
  };
112
- return (_jsxs("div", { className: styles.headerFilter, children: [_jsx("button", { onClick: (e) => {
113
- e.stopPropagation();
114
- setIsOpen((prev) => !prev);
115
- }, className: styles.headerFilterButton, "data-active": isActive, "data-open": isOpen, children: _jsx("span", { className: styles.headerFilterButtonIcon, "data-active": isActive, children: getFontAwesomeIcon("search", isActive ? "solid" : "regular") }) }), isOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.headerFilterBackdrop, onClick: () => {
135
+ return (_jsxs("div", { className: styles.headerFilter, children: [_jsx("button", { ref: triggerRef, onClick: handleTriggerClick, className: styles.headerFilterButton, "data-active": isActive, "data-open": isOpen, children: _jsx("span", { className: styles.headerFilterButtonIcon, "data-active": isActive, children: getFontAwesomeIcon("search", isActive ? "solid" : "regular") }) }), isOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.headerFilterBackdrop, onClick: () => {
116
136
  setIsOpen(false);
117
137
  setIsModeOpen(false);
118
138
  setModeDropdownPos(null);
119
- } }), _jsxs("div", { ref: popoverRef, className: styles.headerFilterMenu, "data-mode-open": isModeOpen, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: styles.headerFilterInputRow, children: [_jsx("div", { className: styles.headerFilterModeWrapper, children: _jsxs("button", { ref: modeButtonRef, className: styles.headerFilterModeButton, onClick: handleModeButtonClick, children: [isModeOpen && (_jsx("span", { children: getFontAwesomeIcon("check", "regular") })), FILTER_MODE_LABELS[filterMode], !isModeOpen && (_jsx("span", { className: styles.headerFilterModeButtonIcon, children: getFontAwesomeIcon("chevronDown", "regular") }))] }) }), _jsx("label", { htmlFor: "search", className: "sr-only" }), _jsx("input", { ref: inputRef, id: "search", className: styles.headerFilterInput, placeholder: "Search", value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyDown: handleKeyDown }), inputValue && (_jsx("button", { className: styles.headerFilterIconButton, onClick: () => setInputValue(""), children: getFontAwesomeIcon("xmark", "regular") })), _jsx("button", { className: styles.headerFilterSearchButton, onClick: handleConfirm, children: getFontAwesomeIcon("search", "regular") })] }), activeTags.length > 0 && (_jsx("div", { className: styles.headerFilterTags, children: activeTags.map((tag) => (_jsxs("span", { className: styles.headerFilterTag, children: [tag, _jsx("button", { className: styles.headerFilterTagRemove, onClick: () => removeTag(tag), children: getFontAwesomeIcon("xmark", "regular") })] }, tag))) }))] })] })), isModeOpen &&
139
+ } }), _jsxs("div", { ref: popoverRef, className: styles.headerFilterMenu, "data-mode-open": isModeOpen, "data-align": alignLeft ? "left" : "center", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: styles.headerFilterInputRow, children: [_jsx("div", { className: styles.headerFilterModeWrapper, children: _jsxs("button", { ref: modeButtonRef, className: styles.headerFilterModeButton, onClick: handleModeButtonClick, children: [isModeOpen && (_jsx("span", { className: styles.headerFilterModeButtonIcon, children: getFontAwesomeIcon("check", "regular") })), FILTER_MODE_LABELS[filterMode], !isModeOpen && (_jsx("span", { className: styles.headerFilterModeButtonIcon, children: getFontAwesomeIcon("chevronDown", "regular") }))] }) }), activeTags.length === 0 &&
140
+ !inputValue &&
141
+ !isFocused && (_jsx("span", { className: styles.headerFilterSearchIcon, children: getFontAwesomeIcon("search", "regular") })), _jsx("label", { htmlFor: "search", className: "sr-only" }), _jsx("input", { ref: inputRef, id: "search", className: styles.headerFilterInput, placeholder: "Search", value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyDown: handleKeyDown, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false) }), inputValue && (_jsx("button", { className: styles.headerFilterIconButton, onClick: () => setInputValue(""), children: getFontAwesomeIcon("xmark", "regular") })), inputValue && (_jsx("button", { className: styles.headerFilterSearchButton, onClick: handleConfirm, children: getFontAwesomeIcon("search", "regular") }))] }), activeTags.length > 0 && (_jsx("div", { className: styles.headerFilterTags, children: activeTags.map((tag) => (_jsxs("span", { className: styles.headerFilterTag, children: [tag, _jsx("button", { className: styles.headerFilterTagRemove, onClick: () => removeTag(tag), children: getFontAwesomeIcon("xmark", "regular") })] }, tag))) }))] })] })), isModeOpen &&
120
142
  modeDropdownPos &&
121
143
  createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: styles.headerFilterBackdrop, onClick: () => {
122
144
  setIsModeOpen(false);
@@ -946,6 +946,13 @@
946
946
  font-size: 14px;
947
947
  }
948
948
 
949
+ /* First column: centering clips the menu off the scroll container's left
950
+ edge, so anchor it to the button's left edge and let it extend right. */
951
+ .headerFilterMenu[data-align="left"] {
952
+ left: -100px;
953
+ transform: none;
954
+ }
955
+
949
956
  /* Square off the bottom-left corner so the mode dropdown visually connects */
950
957
  .headerFilterMenu[data-mode-open="true"] {
951
958
  border-radius: 4px 4px 4px 0px;
@@ -973,6 +980,7 @@
973
980
  color: var(--filterDropdown-text-default);
974
981
  white-space: nowrap;
975
982
  width: 103px;
983
+ font-weight: 400;
976
984
  }
977
985
 
978
986
  .headerFilterModeButtonIcon {
@@ -1001,7 +1009,7 @@
1001
1009
  gap: 6px;
1002
1010
  width: 103px;
1003
1011
  padding: 7px 12px;
1004
- color: #374151;
1012
+ color: var(--filterDropdown-itemText-color-default);
1005
1013
  text-align: right;
1006
1014
  font-family: var(--filterDropdown-fontFamily-default);
1007
1015
  font-size: 14px;
@@ -1023,20 +1031,45 @@
1023
1031
  flex-shrink: 0;
1024
1032
  }
1025
1033
 
1034
+ /* Search icon (leading, inside input row) */
1035
+ .headerFilterSearchIcon {
1036
+ display: flex;
1037
+ align-items: center;
1038
+ justify-content: center;
1039
+ padding-left: 8px;
1040
+ color: var(--headerFilterInput-icon-default);
1041
+ flex-shrink: 0;
1042
+ }
1043
+
1044
+ /* Hide the leading search icon while the input is focused */
1045
+ .headerFilterInputRow:focus-within .headerFilterSearchIcon {
1046
+ display: none;
1047
+ }
1048
+
1026
1049
  /* Text input */
1027
1050
  .headerFilterInput {
1028
1051
  flex: 1;
1029
1052
  height: 100%;
1030
1053
  padding: 0 8px;
1031
- color: #111827;
1054
+ color: var(--headerFilterInput-text-default);
1055
+ font-weight: var(--headerFilterInput-font-weight);
1032
1056
  background: transparent;
1033
1057
  border: none;
1034
1058
  outline: none;
1035
1059
  min-width: 0;
1060
+
1061
+ &::placeholder {
1062
+ font-weight: 400;
1063
+ color: #9ca3af;
1064
+ }
1036
1065
  }
1037
1066
 
1038
1067
  .headerFilterInput::placeholder {
1039
- color: #9ca3af;
1068
+ color: var(--headerFilterInput-placeholder-text-default);
1069
+ }
1070
+ .headerFilterSearchIcon {
1071
+ color: var(--filterDropdown-text-default);
1072
+ padding-left: 10px;
1040
1073
  }
1041
1074
 
1042
1075
  /* Icon buttons (clear X) */
@@ -1046,13 +1079,13 @@
1046
1079
  justify-content: center;
1047
1080
  padding: 0 6px;
1048
1081
  height: 100%;
1049
- color: #6b7280;
1082
+ color: var(--headerFilterInput-icon-btn-text-default);
1050
1083
  flex-shrink: 0;
1051
1084
  font-size: 20px;
1052
1085
  }
1053
1086
 
1054
1087
  .headerFilterIconButton:hover {
1055
- color: #111827;
1088
+ color: var(--headerFilterInput-icon-btn-text-hover);
1056
1089
  }
1057
1090
 
1058
1091
  /* Blue search confirm button */
@@ -1077,7 +1110,6 @@
1077
1110
  gap: 4px;
1078
1111
  padding: 6px 8px;
1079
1112
  border-top: 1px solid #e5e7eb;
1080
- background-color: #f9fafb;
1081
1113
  }
1082
1114
 
1083
1115
  .headerFilterTag {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agilant/toga-blox",
3
3
  "private": false,
4
- "version": "1.0.319-beta.82",
4
+ "version": "1.0.319-beta.83",
5
5
  "description": "Toga-Blox is a comprehensive and reusable React component library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",