@visns-studio/visns-components 6.3.10 → 6.3.12

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
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.3.10",
94
+ "version": "6.3.12",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -759,21 +759,23 @@ const DataGrid = forwardRef(
759
759
  }
760
760
  );
761
761
 
762
- // Mark children for styling (getRowStyle) and
763
- // prefix their display value so nesting reads
762
+ // Mark children so the row and its display cell
763
+ // can be styled as nested.
764
+ //
765
+ // The "↳" marker is NOT written into the value
766
+ // here any more. Prepending it made the string
767
+ // itself longer, so the member's text started
768
+ // roughly a character and a half further right
769
+ // than the same column's text on every other
770
+ // row, and the indent had no fixed budget — it
771
+ // was however wide the glyph plus a space
772
+ // happened to set. It also left the row's own
773
+ // data holding a decoration, which anything
774
+ // reading that field downstream then had to
775
+ // know about. It is drawn instead by
776
+ // decorateTreeChildIndent, inside the cell.
764
777
  children.forEach((child) => {
765
778
  child.__treeChild = true;
766
-
767
- if (
768
- treeDisplayColumn &&
769
- child[treeDisplayColumn] != null &&
770
- String(
771
- child[treeDisplayColumn]
772
- ).indexOf('↳') !== 0
773
- ) {
774
- child[treeDisplayColumn] =
775
- '↳ ' + child[treeDisplayColumn];
776
- }
777
779
  });
778
780
 
779
781
  // Optional member count, e.g. "3 Headers"
@@ -979,16 +981,18 @@ const DataGrid = forwardRef(
979
981
  const { data } = rowProps;
980
982
  const style = {};
981
983
 
982
- // Tree mode: mark the summary parent and its nested
983
- // children with a left accent bar so the block reads as
984
- // one unit and children are visibly subordinate
985
- if (ajaxSetting?.treeGroupBy?.key) {
986
- if (data?.__treeParent) {
987
- style.borderLeft = '4px solid #2e7dbd';
988
- style.fontWeight = 600;
989
- } else if (data?.__treeChild) {
990
- style.borderLeft = '4px solid #93c5fd';
991
- }
984
+ // Tree mode: the parent's heavier weight. The left accent bar
985
+ // that used to be set here as `borderLeft` is now drawn by
986
+ // `.treeParentRow` / `.treeChildRow` — see rowClassName below
987
+ // and the notes in DataGrid.module.scss.
988
+ //
989
+ // A border belongs to the row's box, so a 4px one shifted the
990
+ // whole cell strip 4px right: every column edge on a tree row
991
+ // sat 4px off the same edge on an ordinary row and off the
992
+ // column header. The bar is now an inset overlay that occupies
993
+ // no space in the box model, so the columns line up exactly.
994
+ if (ajaxSetting?.treeGroupBy?.key && data?.__treeParent) {
995
+ style.fontWeight = 600;
992
996
  }
993
997
 
994
998
  if (ajaxSetting?.rowColours?.length > 0) {
@@ -1066,6 +1070,28 @@ const DataGrid = forwardRef(
1066
1070
  [ajaxSetting]
1067
1071
  );
1068
1072
 
1073
+ /**
1074
+ * The tree row's accent bar, as a class rather than a border.
1075
+ *
1076
+ * Kept out of `rowStyles` (which is a project-facing config) because
1077
+ * this is the library's own tree chrome, not something a view asks
1078
+ * for.
1079
+ */
1080
+ const treeRowClassName = useCallback(
1081
+ (data) => {
1082
+ if (!ajaxSetting?.treeGroupBy?.key || !data) {
1083
+ return '';
1084
+ }
1085
+
1086
+ if (data.__treeParent) {
1087
+ return styles.treeParentRow;
1088
+ }
1089
+
1090
+ return data.__treeChild ? styles.treeChildRow : '';
1091
+ },
1092
+ [ajaxSetting]
1093
+ );
1094
+
1069
1095
  /**
1070
1096
  * The grid's own row class plus whatever `rowStyles` adds. Passed as a
1071
1097
  * function rather than the bare string the grid used to get: the grid
@@ -1074,10 +1100,14 @@ const DataGrid = forwardRef(
1074
1100
  */
1075
1101
  const rowClassName = useCallback(
1076
1102
  (rowProps) =>
1077
- ['vs-datagrid--row', getRowClassName(rowProps)]
1103
+ [
1104
+ 'vs-datagrid--row',
1105
+ treeRowClassName(rowProps?.data),
1106
+ getRowClassName(rowProps),
1107
+ ]
1078
1108
  .filter(Boolean)
1079
1109
  .join(' '),
1080
- [getRowClassName]
1110
+ [getRowClassName, treeRowClassName]
1081
1111
  );
1082
1112
 
1083
1113
  const handleDownload = async (d) => {
@@ -4171,6 +4201,68 @@ const DataGrid = forwardRef(
4171
4201
  };
4172
4202
  };
4173
4203
 
4204
+ /**
4205
+ * The nesting marker on a tree member's display cell.
4206
+ *
4207
+ * Drawn here rather than prepended to the value, so the
4208
+ * indent is a FIXED budget spent inside the cell's content
4209
+ * box instead of however much advance width the glyph and
4210
+ * a space happen to take. The marker is a flex item of a
4211
+ * known width and the value takes the rest with
4212
+ * `min-width: 0`, so a long value ellipsizes at the column
4213
+ * edge rather than running past it — the column's left and
4214
+ * right edges are then identical on ordinary, parent and
4215
+ * member rows.
4216
+ *
4217
+ * Applied UNDER decorateTreeParentActions, which delegates
4218
+ * to this for every non-parent row.
4219
+ */
4220
+ const decorateTreeChildIndent = (gridColumn, columnDef) => {
4221
+ const tree = ajaxSetting?.treeGroupBy;
4222
+ const columnId = Array.isArray(columnDef?.id)
4223
+ ? columnDef.id.join('-')
4224
+ : columnDef?.id;
4225
+
4226
+ if (
4227
+ !tree?.key ||
4228
+ !tree.column ||
4229
+ tree.column !== columnId
4230
+ ) {
4231
+ return gridColumn;
4232
+ }
4233
+
4234
+ const originalRender = gridColumn.render;
4235
+
4236
+ return {
4237
+ ...gridColumn,
4238
+ render: (arg) => {
4239
+ const content = originalRender
4240
+ ? originalRender(arg)
4241
+ : arg?.value ?? null;
4242
+
4243
+ if (!arg?.data?.__treeChild) {
4244
+ return content;
4245
+ }
4246
+
4247
+ return (
4248
+ <span className={styles.treeChildCell}>
4249
+ <span
4250
+ className={styles.treeChildMarker}
4251
+ aria-hidden="true"
4252
+ >
4253
+ ↳
4254
+ </span>
4255
+ <span
4256
+ className={styles.treeChildValue}
4257
+ >
4258
+ {content}
4259
+ </span>
4260
+ </span>
4261
+ );
4262
+ },
4263
+ };
4264
+ };
4265
+
4174
4266
  /**
4175
4267
  * The group's collapse control.
4176
4268
  *
@@ -4449,11 +4541,18 @@ const DataGrid = forwardRef(
4449
4541
  };
4450
4542
  };
4451
4543
 
4544
+ // decorateTreeChildIndent sits INSIDE
4545
+ // decorateTreeParentActions: the latter draws parent rows
4546
+ // itself and delegates everything else downward, so the
4547
+ // member indent has to be the render it delegates to.
4452
4548
  const newColumns = columns.map((column) =>
4453
4549
  decorateColumnRowActions(
4454
4550
  decorateTreeParentActions(
4455
- guardTreeParentRender(
4456
- renderColumn(column),
4551
+ decorateTreeChildIndent(
4552
+ guardTreeParentRender(
4553
+ renderColumn(column),
4554
+ column
4555
+ ),
4457
4556
  column
4458
4557
  ),
4459
4558
  column
@@ -152,6 +152,9 @@ function Field({
152
152
  const editorRef = useRef(null);
153
153
  const fileImageInputRef = useRef(null);
154
154
  const containerRef = useRef(null);
155
+ // Wrapper for the `colour` field's popup picker — used by the
156
+ // click-outside handler below to tell inside clicks from outside ones
157
+ const colorPickerRef = useRef(null);
155
158
  const [autoValue, setAutoValue] = useState('');
156
159
  const [colorPickerShow, setColorPickerShow] = useState(false);
157
160
  const [counter, setCounter] = useState(0);
@@ -163,6 +166,42 @@ function Field({
163
166
  const [emailSuggestions, setEmailSuggestions] = useState([]);
164
167
  const [showEmailSuggestions, setShowEmailSuggestions] = useState(false);
165
168
 
169
+ /**
170
+ * `colour` field popup picker: dismiss the popover when the user clicks
171
+ * anywhere outside it or presses Escape. Picking a colour inside the
172
+ * popover deliberately keeps it open (users drag around the gradient).
173
+ * Declared here — top level of the component — because the field itself
174
+ * is rendered from a switch further down.
175
+ */
176
+ useEffect(() => {
177
+ if (!colorPickerShow) {
178
+ return;
179
+ }
180
+
181
+ const onMouseDown = (event) => {
182
+ if (
183
+ colorPickerRef.current &&
184
+ !colorPickerRef.current.contains(event.target)
185
+ ) {
186
+ setColorPickerShow(false);
187
+ }
188
+ };
189
+
190
+ const onKeyDown = (event) => {
191
+ if (event.key === 'Escape') {
192
+ setColorPickerShow(false);
193
+ }
194
+ };
195
+
196
+ document.addEventListener('mousedown', onMouseDown);
197
+ document.addEventListener('keydown', onKeyDown);
198
+
199
+ return () => {
200
+ document.removeEventListener('mousedown', onMouseDown);
201
+ document.removeEventListener('keydown', onKeyDown);
202
+ };
203
+ }, [colorPickerShow]);
204
+
166
205
  /**
167
206
  * Conflict warnings (opt-in via `settings.conflict`): the field polls
168
207
  * `conflict.url` for a map of {optionId: info} describing options that
@@ -1069,46 +1108,77 @@ function Field({
1069
1108
  ))}
1070
1109
  </div>
1071
1110
  );
1072
- case 'colour':
1111
+ case 'colour': {
1112
+ const hasColour = inputValue && inputValue !== 'null';
1113
+ const toggleColorPicker = () => {
1114
+ if (isFieldReadOnly) {
1115
+ return;
1116
+ }
1117
+
1118
+ setColorPickerShow((show) => !show);
1119
+ };
1120
+
1073
1121
  return (
1074
- <div className="cpicker-inline">
1075
- <div className="cpicker-preview-container">
1122
+ <div
1123
+ className="cpicker-inline cpicker-popup"
1124
+ ref={colorPickerRef}
1125
+ >
1126
+ <div
1127
+ className="cpicker-trigger"
1128
+ role="button"
1129
+ tabIndex={isFieldReadOnly ? -1 : 0}
1130
+ aria-haspopup="dialog"
1131
+ aria-expanded={colorPickerShow}
1132
+ aria-disabled={isFieldReadOnly}
1133
+ onClick={toggleColorPicker}
1134
+ onKeyDown={(event) => {
1135
+ if (event.key === 'Enter' || event.key === ' ') {
1136
+ event.preventDefault();
1137
+ toggleColorPicker();
1138
+ }
1139
+ }}
1140
+ >
1076
1141
  <div
1077
1142
  className="cpicker__box"
1078
- data-color={inputValue && inputValue !== 'null' ? inputValue : 'No colour selected'}
1143
+ data-color={
1144
+ hasColour ? inputValue : 'No colour selected'
1145
+ }
1079
1146
  style={
1080
- inputValue && inputValue !== 'null'
1147
+ hasColour
1081
1148
  ? { backgroundColor: inputValue }
1082
1149
  : {}
1083
1150
  }
1084
- title={inputValue && inputValue !== 'null' ? `Current colour: ${inputValue}` : 'No colour selected'}
1151
+ title={
1152
+ hasColour
1153
+ ? `Current colour: ${inputValue}`
1154
+ : 'No colour selected'
1155
+ }
1085
1156
  ></div>
1086
1157
  <span className="cpicker-value">
1087
- {inputValue && inputValue !== 'null' ? inputValue : 'No colour selected'}
1158
+ {hasColour ? inputValue : 'No colour selected'}
1088
1159
  </span>
1089
1160
  </div>
1090
- <div className="cpicker-container">
1091
- <Sketch
1092
- color={
1093
- inputValue && inputValue !== 'null'
1094
- ? inputValue
1095
- : '#000000'
1096
- }
1097
- onChange={(color) => {
1098
- onChangeColour(
1099
- { target: { value: color.hex } },
1100
- { hex: color.hex },
1101
- settings.id
1102
- );
1103
- }}
1104
- style={{
1105
- boxShadow: 'none',
1106
- border: 'none'
1107
- }}
1108
- />
1109
- </div>
1161
+ {colorPickerShow && (
1162
+ <div className="cpicker-popover" role="dialog">
1163
+ <Sketch
1164
+ color={hasColour ? inputValue : '#000000'}
1165
+ onChange={(color) => {
1166
+ onChangeColour(
1167
+ { target: { value: color.hex } },
1168
+ { hex: color.hex },
1169
+ settings.id
1170
+ );
1171
+ }}
1172
+ style={{
1173
+ boxShadow: 'none',
1174
+ border: 'none'
1175
+ }}
1176
+ />
1177
+ </div>
1178
+ )}
1110
1179
  </div>
1111
1180
  );
1181
+ }
1112
1182
  case 'copytoclipboard':
1113
1183
  return (
1114
1184
  <>
@@ -1579,3 +1579,96 @@
1579
1579
  );
1580
1580
  }
1581
1581
  }
1582
+
1583
+ /* =========================================================================
1584
+ treeGroupBy nesting: accent bar and member indent
1585
+
1586
+ Both of these used to be drawn in ways that took up space in the layout,
1587
+ which is what pushed tree rows out of line with ordinary ones:
1588
+
1589
+ - the accent bar was `border-left: 4px` set on the ROW. A border is part
1590
+ of the row's box, so the whole cell strip started 4px to the right and
1591
+ every column edge on a tree row sat 4px off the same edge on an
1592
+ ordinary row (and off the column header).
1593
+
1594
+ - the "↳" was PREPENDED TO THE VALUE, so it was ordinary text inside the
1595
+ cell's single text run. The member's own text therefore began after the
1596
+ glyph plus a space — about 11px at 14px Barlow — while the parent's
1597
+ label and every ordinary row's text began at the cell's padding edge.
1598
+ The indent was whatever the font made it, it varied with the type size,
1599
+ and a long value had that much less room before it ran out of cell.
1600
+
1601
+ Both are now drawn without occupying layout: the bar is an inset overlay,
1602
+ and the marker is a fixed-width flex item spent from INSIDE the cell's
1603
+ existing content box.
1604
+
1605
+ Density hooks, per the convention above:
1606
+
1607
+ --grid-tree-accent-width the nesting bar. default: 4px
1608
+ --grid-tree-indent member indent budget, marker included.
1609
+ default: 18px
1610
+ ========================================================================= */
1611
+
1612
+ /* The bar is painted by an overlay pinned to the row's left edge rather than
1613
+ by a border, so it costs nothing in the box model. It has to be an overlay
1614
+ rather than an inset box-shadow because `.InovuaReactDataGrid__row-cell-wrap`
1615
+ carries `background: inherit` — it repaints the row's own background across
1616
+ the full width and would cover any shadow the row painted underneath. */
1617
+ .treeParentRow,
1618
+ .treeChildRow {
1619
+ position: relative;
1620
+
1621
+ &::after {
1622
+ content: '';
1623
+ position: absolute;
1624
+ top: 0;
1625
+ bottom: 0;
1626
+ left: 0;
1627
+ width: var(--grid-tree-accent-width, 4px);
1628
+ /* Above the cell strip, which paints an opaque background of its own */
1629
+ z-index: 1;
1630
+ pointer-events: none;
1631
+ }
1632
+ }
1633
+
1634
+ .treeParentRow::after {
1635
+ background: var(--grid-tree-accent-parent, #2e7dbd);
1636
+ }
1637
+
1638
+ .treeChildRow::after {
1639
+ background: var(--grid-tree-accent-child, #93c5fd);
1640
+ }
1641
+
1642
+ /* The member's display cell: marker and value on one line, inside the cell.
1643
+ `min-width: 0` on both the wrapper and the value is what lets the value
1644
+ ellipsize at the column edge instead of overflowing past it — a flex item
1645
+ defaults to `min-width: auto`, which refuses to shrink below its content. */
1646
+ .treeChildCell {
1647
+ display: flex;
1648
+ align-items: center;
1649
+ width: 100%;
1650
+ min-width: 0;
1651
+ overflow: hidden;
1652
+ }
1653
+
1654
+ .treeChildMarker {
1655
+ /* Fixed basis, never shrinks: this IS the indent budget, so the value
1656
+ starts at the same offset on every member row whatever the glyph, the
1657
+ type size or the value's length. */
1658
+ flex: 0 0 var(--grid-tree-indent, 18px);
1659
+ width: var(--grid-tree-indent, 18px);
1660
+ display: inline-flex;
1661
+ align-items: center;
1662
+ justify-content: flex-start;
1663
+ color: var(--muted-color, #6b7688);
1664
+ line-height: 1;
1665
+ user-select: none;
1666
+ }
1667
+
1668
+ .treeChildValue {
1669
+ flex: 1 1 auto;
1670
+ min-width: 0;
1671
+ overflow: hidden;
1672
+ text-overflow: ellipsis;
1673
+ white-space: nowrap;
1674
+ }
@@ -807,6 +807,69 @@ form div:has(.react-toggle) input[type='checkbox'] {
807
807
  }
808
808
  }
809
809
 
810
+ /* Popup (click-to-open) variant of the colour picker — the collapsed
811
+ trigger row plus the floating Sketch popover. Additive: the inline
812
+ rules above are left as-is for any consumer still using them. */
813
+ .cpicker-popup {
814
+ position: relative;
815
+ gap: 0;
816
+ }
817
+
818
+ .cpicker-trigger {
819
+ display: flex;
820
+ align-items: center;
821
+ gap: 8px;
822
+ width: 100%;
823
+ box-sizing: border-box;
824
+ min-height: 42px;
825
+ padding: 0.4rem 0.65rem;
826
+ font-size: 0.85rem;
827
+ color: var(--paragraph-color);
828
+ background: #ffffff;
829
+ border: 1px solid rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.15);
830
+ border-radius: var(--br, 6px);
831
+ cursor: pointer;
832
+ transition: border 0.15s linear;
833
+ }
834
+
835
+ .cpicker-trigger:hover,
836
+ .cpicker-trigger:focus-visible {
837
+ border: 1px solid rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.85);
838
+ outline: none;
839
+ }
840
+
841
+ .cpicker-trigger[aria-disabled='true'] {
842
+ cursor: not-allowed;
843
+ opacity: 0.7;
844
+ }
845
+
846
+ /* The value chip styling only makes sense on the old inline preview —
847
+ inside the trigger it should read as plain input text */
848
+ .cpicker-trigger .cpicker-value {
849
+ background: transparent;
850
+ border: none;
851
+ padding: 0;
852
+ font-weight: 500;
853
+ }
854
+
855
+ .cpicker-popover {
856
+ position: absolute;
857
+ top: calc(100% + 4px);
858
+ left: 0;
859
+ z-index: 1100;
860
+ padding: 8px;
861
+ background: #ffffff;
862
+ border: 1px solid #dee2e6;
863
+ border-radius: 6px;
864
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
865
+ }
866
+
867
+ .cpicker-popover .w-color-sketch {
868
+ box-shadow: none !important;
869
+ border: none !important;
870
+ border-radius: 6px !important;
871
+ }
872
+
810
873
  /* Safari-specific fixes for form elements */
811
874
  @supports (-webkit-touch-callout: none) {
812
875
  /* Safari-specific styles */