@sproutsocial/seeds-react-tree 0.3.1 → 0.4.0

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/dist/index.js CHANGED
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ MultiTreeSearchableSelect: () => MultiTreeSearchableSelect,
34
+ SingleTreeSearchableSelect: () => SingleTreeSearchableSelect,
33
35
  Tree: () => Tree,
34
36
  TreeCombobox: () => TreeCombobox,
35
37
  TreeItem: () => TreeItem
@@ -143,7 +145,8 @@ var TreeRoot = import_styled_components.default.ul`
143
145
  margin: 0;
144
146
  padding: 0;
145
147
  font-family: ${({ theme }) => theme.fontFamily};
146
- ${({ theme }) => theme.typography[300]}
148
+ font-size: ${({ theme }) => theme.typography[200].fontSize};
149
+ line-height: ${({ theme }) => theme.typography[200].lineHeight};
147
150
  color: ${({ theme }) => theme.colors.text.body};
148
151
  `;
149
152
  var TreeItemRow = import_styled_components.default.div`
@@ -151,7 +154,7 @@ var TreeItemRow = import_styled_components.default.div`
151
154
  align-items: center;
152
155
  gap: ${({ theme }) => theme.space[300]};
153
156
  padding: ${({ theme }) => theme.space[300]};
154
- padding-left: ${({ theme, $level }) => `calc(${theme.space[300]} + ${$level - 1} * ${theme.space[500]})`};
157
+ padding-left: ${({ theme, $level }) => `calc(${theme.space[300]} + ${$level - 1} * ${theme.space[400]})`};
155
158
  border-radius: ${({ theme }) => theme.radii[400]};
156
159
  cursor: pointer;
157
160
  user-select: none;
@@ -721,7 +724,13 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
721
724
  var Root = import_styled_components2.default.div`
722
725
  display: flex;
723
726
  flex-direction: column;
724
- gap: ${({ theme }) => theme.space[300]};
727
+ flex: 1;
728
+ min-height: 0;
729
+ `;
730
+ var SearchSection = import_styled_components2.default.div`
731
+ padding: ${({ theme }) => theme.space[300]};
732
+ border-bottom: 1px solid ${({ theme }) => theme.colors.container.border.base};
733
+ flex-shrink: 0;
725
734
  `;
726
735
  var InputGroup = import_styled_components2.default.div`
727
736
  display: flex;
@@ -729,8 +738,8 @@ var InputGroup = import_styled_components2.default.div`
729
738
  gap: ${({ theme }) => theme.space[200]};
730
739
  padding: ${({ theme }) => theme.space[200]} ${({ theme }) => theme.space[300]};
731
740
  border-radius: ${({ theme }) => theme.radii[500]};
732
- border: 1px solid ${({ theme }) => theme.colors.form.border.base};
733
- background: ${({ theme }) => theme.colors.form.background.base};
741
+ border: 1px solid ${({ theme }) => theme.colors.container.border.base};
742
+ background: ${({ theme }) => theme.colors.container.background.base};
734
743
  color: ${({ theme }) => theme.colors.icon.base};
735
744
 
736
745
  &:focus-within {
@@ -743,18 +752,26 @@ var ComboboxInput = import_styled_components2.default.input`
743
752
  background: transparent;
744
753
  outline: none;
745
754
  font-family: ${({ theme }) => theme.fontFamily};
746
- ${({ theme }) => theme.typography[300]}
755
+ font-size: ${({ theme }) => theme.typography[200].fontSize};
756
+ line-height: ${({ theme }) => theme.typography[200].lineHeight};
747
757
  color: ${({ theme }) => theme.colors.text.body};
748
758
 
749
759
  &::placeholder {
750
760
  color: ${({ theme }) => theme.colors.text.subtext};
751
761
  }
752
762
  `;
763
+ var ListSection = import_styled_components2.default.div`
764
+ padding: ${({ theme }) => theme.space[200]};
765
+ overflow: auto;
766
+ flex: 1;
767
+ min-height: 0;
768
+ `;
753
769
  var EmptyState = import_styled_components2.default.div`
754
- padding: ${({ theme }) => theme.space[400]};
770
+ padding: ${({ theme }) => `${theme.space[300]} ${theme.space[350]}`};
755
771
  text-align: center;
756
772
  color: ${({ theme }) => theme.colors.text.subtext};
757
- ${({ theme }) => theme.typography[300]}
773
+ font-size: ${({ theme }) => theme.typography[200].fontSize};
774
+ line-height: ${({ theme }) => theme.typography[200].lineHeight};
758
775
  `;
759
776
  var NAV_KEYS = /* @__PURE__ */ new Set([
760
777
  "ArrowDown",
@@ -857,9 +874,17 @@ function TreeCombobox(props) {
857
874
  setFocusedId(result.nextFocusedId);
858
875
  }
859
876
  };
877
+ React3.useLayoutEffect(() => {
878
+ if (!focusedId) return;
879
+ const itemEl = findTreeItemEl(focusedId);
880
+ const row = itemEl?.querySelector("[data-treeitem-row]") ?? itemEl;
881
+ if (row && typeof row.scrollIntoView === "function") {
882
+ row.scrollIntoView({ block: "nearest", inline: "nearest" });
883
+ }
884
+ }, [focusedId]);
860
885
  const showEmpty = visibleItems.length === 0;
861
886
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Root, { className, children: [
862
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(InputGroup, { children: [
887
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SearchSection, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(InputGroup, { children: [
863
888
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_seeds_react_icon2.Icon, { name: "magnifying-glass-outline", size: "small", "aria-hidden": true }),
864
889
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
865
890
  ComboboxInput,
@@ -880,29 +905,31 @@ function TreeCombobox(props) {
880
905
  onKeyDown: handleInputKeyDown
881
906
  }
882
907
  )
883
- ] }),
884
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
885
- Tree,
886
- {
887
- ref: treeRef,
888
- "aria-label": ariaLabel,
889
- "aria-labelledby": ariaLabelledBy,
890
- id: treeDomId,
891
- manageDomFocus: false,
892
- selectionMode,
893
- selectableNodes,
894
- expanded: effectiveExpanded,
895
- onExpandedChange: handleExpandedChange,
896
- selected: selectedProp,
897
- defaultSelected,
898
- onSelectionChange,
899
- renderSelectionIndicator,
900
- focusedId,
901
- onFocusedIdChange: setFocusedId,
902
- children: visibleItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TreeNode, { item }, item.id))
903
- }
904
- ),
905
- showEmpty ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EmptyState, { role: "status", "aria-live": "polite", children: emptyText }) : null
908
+ ] }) }),
909
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(ListSection, { children: [
910
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
911
+ Tree,
912
+ {
913
+ ref: treeRef,
914
+ "aria-label": ariaLabel,
915
+ "aria-labelledby": ariaLabelledBy,
916
+ id: treeDomId,
917
+ manageDomFocus: false,
918
+ selectionMode,
919
+ selectableNodes,
920
+ expanded: effectiveExpanded,
921
+ onExpandedChange: handleExpandedChange,
922
+ selected: selectedProp,
923
+ defaultSelected,
924
+ onSelectionChange,
925
+ renderSelectionIndicator,
926
+ focusedId,
927
+ onFocusedIdChange: setFocusedId,
928
+ children: visibleItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TreeNode, { item }, item.id))
929
+ }
930
+ ),
931
+ showEmpty ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(EmptyState, { role: "status", "aria-live": "polite", children: emptyText }) : null
932
+ ] })
906
933
  ] });
907
934
  }
908
935
  function TreeNode({ item }) {
@@ -917,8 +944,340 @@ function TreeNode({ item }) {
917
944
  }
918
945
  );
919
946
  }
947
+
948
+ // src/SingleTreeSearchableSelect.tsx
949
+ var React5 = __toESM(require("react"));
950
+
951
+ // src/Common/TreeSearchableSelectBase.tsx
952
+ var React4 = __toESM(require("react"));
953
+ var import_styled_components3 = __toESM(require("styled-components"));
954
+ var import_seeds_react_icon3 = require("@sproutsocial/seeds-react-icon");
955
+ var import_seeds_react_mixins2 = require("@sproutsocial/seeds-react-mixins");
956
+ var import_v2 = require("@sproutsocial/seeds-react-popout/v2");
957
+
958
+ // src/Common/flattenTree.ts
959
+ function flattenTreeItems(items) {
960
+ const out = [];
961
+ const walk = (nodes) => {
962
+ for (const node of nodes) {
963
+ out.push(node);
964
+ if (node.children) walk(node.children);
965
+ }
966
+ };
967
+ walk(items);
968
+ return out;
969
+ }
970
+
971
+ // src/Common/TreeSearchableSelectBase.tsx
972
+ var import_jsx_runtime4 = require("react/jsx-runtime");
973
+ var invalidStyles = import_styled_components3.css`
974
+ border-color: ${({ theme }) => theme.colors.form.border.error};
975
+ `;
976
+ var Trigger = import_styled_components3.default.button`
977
+ display: flex;
978
+ align-items: center;
979
+ justify-content: space-between;
980
+ gap: ${({ theme }) => theme.space[300]};
981
+ padding: ${({ theme }) => theme.space[200]} ${({ theme }) => theme.space[350]};
982
+ min-width: 160px;
983
+ width: ${({ $fullWidth }) => $fullWidth ? "100%" : "auto"};
984
+ border-radius: ${({ theme }) => theme.radii[500]};
985
+ border: 1px solid ${({ theme }) => theme.colors.form.border.base};
986
+ background: ${({ theme }) => theme.colors.form.background.base};
987
+ font-family: ${({ theme }) => theme.fontFamily};
988
+ font-size: ${({ theme }) => theme.typography[200].fontSize};
989
+ line-height: ${({ theme }) => theme.typography[200].lineHeight};
990
+ color: ${({ theme }) => theme.colors.text.body};
991
+ text-align: left;
992
+ cursor: default;
993
+ outline: none;
994
+ user-select: none;
995
+ transition: border-color ${({ theme }) => theme.duration.fast}
996
+ ${({ theme }) => theme.easing.ease_in},
997
+ box-shadow ${({ theme }) => theme.duration.fast}
998
+ ${({ theme }) => theme.easing.ease_in};
999
+
1000
+ &:focus-visible {
1001
+ ${import_seeds_react_mixins2.focusRing}
1002
+ }
1003
+
1004
+ &[aria-disabled="true"],
1005
+ &:disabled {
1006
+ opacity: 0.4;
1007
+ cursor: not-allowed;
1008
+ }
1009
+
1010
+ ${({ $isInvalid }) => $isInvalid && invalidStyles}
1011
+ `;
1012
+ var TriggerLabel = import_styled_components3.default.span`
1013
+ flex: 1;
1014
+ min-width: 0;
1015
+ overflow: hidden;
1016
+ text-overflow: ellipsis;
1017
+ white-space: nowrap;
1018
+ `;
1019
+ var PlaceholderText = import_styled_components3.default.span`
1020
+ color: ${({ theme }) => theme.colors.text.subtext};
1021
+ `;
1022
+ var Chevron = import_styled_components3.default.span`
1023
+ display: inline-flex;
1024
+ align-items: center;
1025
+ color: ${({ theme }) => theme.colors.icon.base};
1026
+ transition: transform ${({ theme }) => theme.duration.fast}
1027
+ ${({ theme }) => theme.easing.ease_in};
1028
+ ${({ $open }) => $open && import_styled_components3.css`
1029
+ transform: rotate(-180deg);
1030
+ `}
1031
+ `;
1032
+ var HiddenInput = import_styled_components3.default.input`
1033
+ clip-path: inset(50%);
1034
+ overflow: hidden;
1035
+ white-space: nowrap;
1036
+ border: 0;
1037
+ padding: 0;
1038
+ width: 1px;
1039
+ height: 1px;
1040
+ margin: -1px;
1041
+ position: fixed;
1042
+ top: 0;
1043
+ left: 0;
1044
+ `;
1045
+ var PopoutContent = import_styled_components3.default.div`
1046
+ display: flex;
1047
+ flex-direction: column;
1048
+ /* Width sizes to content within bounds: at least the trigger width, never */
1049
+ /* wider than the available viewport space. Shallow popups stay flush with */
1050
+ /* the trigger; deeply nested trees expand horizontally as needed. */
1051
+ min-width: var(--radix-popover-trigger-width);
1052
+ max-width: var(--radix-popper-available-width);
1053
+ /* Height caps at 400px (or available viewport) so a popup with many */
1054
+ /* expanded branches scrolls vertically instead of growing off-screen. */
1055
+ max-height: min(var(--radix-popper-available-height), 400px);
1056
+ width: ${({ $width }) => $width === void 0 ? "max-content" : typeof $width === "number" ? `${$width}px` : $width};
1057
+ `;
1058
+ var MAX_INLINE_LABELS = 3;
1059
+ function defaultRenderTrigger(selected, placeholder) {
1060
+ if (selected.length === 0) {
1061
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlaceholderText, { children: placeholder });
1062
+ }
1063
+ if (selected.length <= MAX_INLINE_LABELS) {
1064
+ return selected.map((s) => s.label).join(", ");
1065
+ }
1066
+ return `${selected.length} selected`;
1067
+ }
1068
+ function TreeSearchableSelectBase(props) {
1069
+ const {
1070
+ items,
1071
+ selectionMode,
1072
+ selectableNodes,
1073
+ selectedIds,
1074
+ onSelectedIdsChange,
1075
+ placeholder = "Select...",
1076
+ searchPlaceholder = "Search...",
1077
+ emptyText = "No results found.",
1078
+ renderTrigger,
1079
+ open: openProp,
1080
+ defaultOpen = false,
1081
+ onOpenChange,
1082
+ closeOnSelect,
1083
+ disabled,
1084
+ isInvalid,
1085
+ required,
1086
+ readOnly,
1087
+ name,
1088
+ fullWidth = true,
1089
+ contentWidth,
1090
+ renderSelectionIndicator,
1091
+ defaultExpanded,
1092
+ expanded,
1093
+ onExpandedChange,
1094
+ id,
1095
+ className
1096
+ } = props;
1097
+ const ariaLabel = props["aria-label"];
1098
+ const ariaLabelledBy = props["aria-labelledby"];
1099
+ const ariaDescribedBy = props["aria-describedby"];
1100
+ const isOpenControlled = openProp !== void 0;
1101
+ const [uncontrolledOpen, setUncontrolledOpen] = React4.useState(defaultOpen);
1102
+ const open = isOpenControlled ? !!openProp : uncontrolledOpen;
1103
+ const setOpen = (next) => {
1104
+ if (!isOpenControlled) setUncontrolledOpen(next);
1105
+ onOpenChange?.(next);
1106
+ };
1107
+ const [query, setQuery] = React4.useState("");
1108
+ const reactId = React4.useId();
1109
+ const popupId = `tree-select-popup-${reactId.replace(/:/g, "")}`;
1110
+ const flat = React4.useMemo(() => flattenTreeItems(items), [items]);
1111
+ const selectedItems = React4.useMemo(() => {
1112
+ const order = new Map(flat.map((it, i) => [it.id, i]));
1113
+ return selectedIds.map((id2) => flat.find((it) => it.id === id2)).filter((it) => it != null).sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
1114
+ }, [flat, selectedIds]);
1115
+ const handleSelectionChange = (next) => {
1116
+ onSelectedIdsChange(next);
1117
+ if (closeOnSelect) {
1118
+ setOpen(false);
1119
+ }
1120
+ };
1121
+ const triggerContent = renderTrigger ? renderTrigger(selectedItems) : defaultRenderTrigger(selectedItems, placeholder);
1122
+ const isMulti = selectionMode === "multiple";
1123
+ const primaryInputName = isMulti ? void 0 : name;
1124
+ const primaryInputId = id && primaryInputName == null ? `${id}-hidden-input` : void 0;
1125
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1126
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1127
+ HiddenInput,
1128
+ {
1129
+ type: "text",
1130
+ id: primaryInputId,
1131
+ name: primaryInputName,
1132
+ value: isMulti ? "" : selectedIds[0] ?? "",
1133
+ onChange: () => {
1134
+ },
1135
+ disabled,
1136
+ required: required && !isMulti,
1137
+ readOnly,
1138
+ tabIndex: -1,
1139
+ "aria-hidden": "true"
1140
+ }
1141
+ ),
1142
+ isMulti && name ? selectedIds.map((selectedId) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1143
+ "input",
1144
+ {
1145
+ type: "hidden",
1146
+ name,
1147
+ value: selectedId
1148
+ },
1149
+ selectedId
1150
+ )) : null,
1151
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1152
+ import_v2.Popout,
1153
+ {
1154
+ open,
1155
+ onOpenChange: setOpen,
1156
+ side: "bottom",
1157
+ align: "start",
1158
+ style: { padding: 0 },
1159
+ onEscapeKeyDown: (event) => {
1160
+ if (query.length > 0) {
1161
+ event.preventDefault();
1162
+ }
1163
+ },
1164
+ content: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PopoutContent, { id: popupId, $width: contentWidth, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1165
+ TreeCombobox,
1166
+ {
1167
+ "aria-label": "Search",
1168
+ items,
1169
+ placeholder: searchPlaceholder,
1170
+ emptyText,
1171
+ query,
1172
+ onQueryChange: setQuery,
1173
+ selectionMode,
1174
+ selectableNodes,
1175
+ selected: selectedIds,
1176
+ onSelectionChange: handleSelectionChange,
1177
+ renderSelectionIndicator,
1178
+ defaultExpanded,
1179
+ expanded,
1180
+ onExpandedChange
1181
+ }
1182
+ ) }),
1183
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1184
+ Trigger,
1185
+ {
1186
+ type: "button",
1187
+ id,
1188
+ className,
1189
+ $isInvalid: isInvalid,
1190
+ $fullWidth: fullWidth,
1191
+ disabled,
1192
+ role: "combobox",
1193
+ "aria-haspopup": "dialog",
1194
+ "aria-expanded": open,
1195
+ "aria-controls": open ? popupId : void 0,
1196
+ "aria-label": ariaLabel,
1197
+ "aria-labelledby": ariaLabelledBy,
1198
+ "aria-describedby": ariaDescribedBy,
1199
+ "aria-invalid": isInvalid || void 0,
1200
+ "aria-required": required || void 0,
1201
+ children: [
1202
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TriggerLabel, { children: triggerContent }),
1203
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Chevron, { $open: open, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_seeds_react_icon3.Icon, { name: "chevron-down-outline", size: "small" }) })
1204
+ ]
1205
+ }
1206
+ )
1207
+ }
1208
+ )
1209
+ ] });
1210
+ }
1211
+
1212
+ // src/SingleTreeSearchableSelect.tsx
1213
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1214
+ function SingleTreeSearchableSelect(props) {
1215
+ const {
1216
+ selectedItemId: selectedItemIdProp,
1217
+ defaultSelectedItemId = null,
1218
+ onSelectedItemIdChange,
1219
+ ...rest
1220
+ } = props;
1221
+ const isControlled = selectedItemIdProp !== void 0;
1222
+ const [internalId, setInternalId] = React5.useState(
1223
+ defaultSelectedItemId
1224
+ );
1225
+ const selectedItemId = isControlled ? selectedItemIdProp ?? null : internalId;
1226
+ const selectedIds = React5.useMemo(
1227
+ () => selectedItemId ? [selectedItemId] : [],
1228
+ [selectedItemId]
1229
+ );
1230
+ const handleSelectedIdsChange = (ids) => {
1231
+ const next = ids[0] ?? null;
1232
+ if (!isControlled) setInternalId(next);
1233
+ onSelectedItemIdChange?.(next);
1234
+ };
1235
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1236
+ TreeSearchableSelectBase,
1237
+ {
1238
+ ...rest,
1239
+ selectionMode: "single",
1240
+ selectedIds,
1241
+ onSelectedIdsChange: handleSelectedIdsChange,
1242
+ closeOnSelect: true
1243
+ }
1244
+ );
1245
+ }
1246
+
1247
+ // src/MultiTreeSearchableSelect.tsx
1248
+ var React6 = __toESM(require("react"));
1249
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1250
+ function MultiTreeSearchableSelect(props) {
1251
+ const {
1252
+ selectedItemIds: selectedItemIdsProp,
1253
+ defaultSelectedItemIds,
1254
+ onSelectedItemIdsChange,
1255
+ ...rest
1256
+ } = props;
1257
+ const isControlled = selectedItemIdsProp !== void 0;
1258
+ const [internalIds, setInternalIds] = React6.useState(() => [
1259
+ ...defaultSelectedItemIds ?? []
1260
+ ]);
1261
+ const selectedIds = isControlled ? selectedItemIdsProp ?? [] : internalIds;
1262
+ const handleSelectedIdsChange = (ids) => {
1263
+ if (!isControlled) setInternalIds(ids);
1264
+ onSelectedItemIdsChange?.(ids);
1265
+ };
1266
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1267
+ TreeSearchableSelectBase,
1268
+ {
1269
+ ...rest,
1270
+ selectionMode: "multiple",
1271
+ selectedIds,
1272
+ onSelectedIdsChange: handleSelectedIdsChange,
1273
+ closeOnSelect: false
1274
+ }
1275
+ );
1276
+ }
920
1277
  // Annotate the CommonJS export names for ESM import in node:
921
1278
  0 && (module.exports = {
1279
+ MultiTreeSearchableSelect,
1280
+ SingleTreeSearchableSelect,
922
1281
  Tree,
923
1282
  TreeCombobox,
924
1283
  TreeItem