dce-reactkit 5.0.6 → 5.0.8

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.
@@ -13,6 +13,7 @@ type Props = {
13
13
  * values updated
14
14
  */
15
15
  onChanged: (updatedItems: PickableItem[]) => void;
16
+ startWithGroupsExpanded?: boolean;
16
17
  };
17
18
  declare const NestableItemList: React.FC<Props>;
18
19
  export default NestableItemList;
@@ -15,6 +15,7 @@ type Props = {
15
15
  onChanged: (updatedItems: PickableItem[]) => void;
16
16
  noBottomMargin?: boolean;
17
17
  hideSelectAllOrNoneButtons?: boolean;
18
+ startWithGroupsExpanded?: boolean;
18
19
  };
19
20
  declare const ItemPicker: React.FC<Props>;
20
21
  export default ItemPicker;
@@ -1,11 +1,14 @@
1
+ /// <reference types="react" />
1
2
  /**
2
3
  * An item that can be chosen (for use within ItemPicker)
3
4
  * @author Gabe Abrams
4
5
  */
5
6
  type PickableItem = ({
6
7
  id: number | string;
7
- name: string;
8
+ name: React.ReactNode;
9
+ ariaLabel?: string;
8
10
  link?: string;
11
+ tooltip?: string;
9
12
  } & ({
10
13
  isGroup: false;
11
14
  checked: boolean;
@@ -22,6 +22,8 @@ export declare const _setStubResponse: (opts: {
22
22
  * @param opts.path - the path of the server endpoint
23
23
  * @param [opts.method=GET] - the method of the endpoint
24
24
  * @param [opts.params] - query/body parameters to include
25
+ * @param [opts.headers] - custom headers to include; values must already be
26
+ * string to avoid issue with header serialization
25
27
  * @returns response from server
26
28
  */
27
29
  declare const visitServerEndpoint: (opts: {
@@ -30,5 +32,8 @@ declare const visitServerEndpoint: (opts: {
30
32
  params?: {
31
33
  [x: string]: any;
32
34
  } | undefined;
35
+ headers?: {
36
+ [x: string]: string;
37
+ } | undefined;
33
38
  }) => Promise<any>;
34
39
  export default visitServerEndpoint;
package/dist/index.d.ts CHANGED
@@ -442,8 +442,10 @@ declare const CopiableBox: React$1.FC<Props$c>;
442
442
  */
443
443
  type PickableItem = ({
444
444
  id: number | string;
445
- name: string;
445
+ name: React.ReactNode;
446
+ ariaLabel?: string;
446
447
  link?: string;
448
+ tooltip?: string;
447
449
  } & ({
448
450
  isGroup: false;
449
451
  checked: boolean;
@@ -468,6 +470,7 @@ type Props$b = {
468
470
  onChanged: (updatedItems: PickableItem[]) => void;
469
471
  noBottomMargin?: boolean;
470
472
  hideSelectAllOrNoneButtons?: boolean;
473
+ startWithGroupsExpanded?: boolean;
471
474
  };
472
475
  declare const ItemPicker: React$1.FC<Props$b>;
473
476
 
@@ -862,6 +865,8 @@ declare const initClient: (opts: ({
862
865
  * @param opts.path - the path of the server endpoint
863
866
  * @param [opts.method=GET] - the method of the endpoint
864
867
  * @param [opts.params] - query/body parameters to include
868
+ * @param [opts.headers] - custom headers to include; values must already be
869
+ * string to avoid issue with header serialization
865
870
  * @returns response from server
866
871
  */
867
872
  declare const visitServerEndpoint: (opts: {
@@ -870,6 +875,9 @@ declare const visitServerEndpoint: (opts: {
870
875
  params?: {
871
876
  [x: string]: any;
872
877
  } | undefined;
878
+ headers?: {
879
+ [x: string]: string;
880
+ } | undefined;
873
881
  }) => Promise<any>;
874
882
 
875
883
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "5.0.6",
3
+ "version": "5.0.8",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -16,6 +16,7 @@ import {
16
16
 
17
17
  // Import shared components
18
18
  import CheckboxButton from '../CheckboxButton';
19
+ import Tooltip from '../Tooltip';
19
20
  import Variant from '../../types/Variant';
20
21
 
21
22
  // Import types
@@ -35,6 +36,8 @@ type Props = {
35
36
  * values updated
36
37
  */
37
38
  onChanged: (updatedItems: PickableItem[]) => void,
39
+ // If true, groups start expanded instead of collapsed
40
+ startWithGroupsExpanded?: boolean,
38
41
  };
39
42
 
40
43
  /*------------------------------------------------------------------------*/
@@ -106,6 +109,7 @@ const NestableItemList: React.FC<Props> = (props) => {
106
109
  const {
107
110
  items,
108
111
  onChanged,
112
+ startWithGroupsExpanded,
109
113
  } = props;
110
114
 
111
115
  /* -------------- State ------------- */
@@ -113,7 +117,7 @@ const NestableItemList: React.FC<Props> = (props) => {
113
117
  // Create initial map of child expanded booleans
114
118
  const initChildExpanded: { [k: string]: boolean } = {};
115
119
  items.forEach((item) => {
116
- initChildExpanded[String(item.id)] = false;
120
+ initChildExpanded[String(item.id)] = !!startWithGroupsExpanded;
117
121
  });
118
122
 
119
123
  // Initial state
@@ -229,6 +233,29 @@ const NestableItemList: React.FC<Props> = (props) => {
229
233
  <div>
230
234
  {
231
235
  items.map((item) => {
236
+ // Human-readable label for accessibility. Prefer the explicit
237
+ // ariaLabel, then fall back to the name if it is a plain string
238
+ const accessibleName = (
239
+ item.ariaLabel
240
+ ?? (typeof item.name === 'string' ? item.name : '')
241
+ );
242
+
243
+ // Checkbox and Text
244
+ const checkbox = (
245
+ <CheckboxButton
246
+ className={`NestableItemList-CheckboxButton-${item.id}`}
247
+ text={item.name}
248
+ checked={item.isGroup ? allChecked(item.children) : item.checked}
249
+ dashed={item.isGroup ? !noneChecked(item.children) : false}
250
+ onChanged={(checked) => {
251
+ onChanged(changeChecked(item.id, checked, items));
252
+ }}
253
+ ariaLabel={`Select ${accessibleName}`}
254
+ checkedVariant={Variant.Light}
255
+ uncheckedVariant={Variant.Light}
256
+ />
257
+ );
258
+
232
259
  return (
233
260
  <div key={item.id}>
234
261
  {/* Dropdown Button */}
@@ -252,7 +279,7 @@ const NestableItemList: React.FC<Props> = (props) => {
252
279
  id: item.id,
253
280
  });
254
281
  }}
255
- aria-label={`${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}`}
282
+ aria-label={`${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${accessibleName}`}
256
283
  >
257
284
  <FontAwesomeIcon
258
285
  icon={childExpanded[item.id] ? faChevronDown : faChevronRight}
@@ -261,19 +288,18 @@ const NestableItemList: React.FC<Props> = (props) => {
261
288
  )}
262
289
  </span>
263
290
 
264
- {/* Checkbox and Text */}
265
- <CheckboxButton
266
- className={`NestableItemList-CheckboxButton-${item.id}`}
267
- text={item.name}
268
- checked={item.isGroup ? allChecked(item.children) : item.checked}
269
- dashed={item.isGroup ? !noneChecked(item.children) : false}
270
- onChanged={(checked) => {
271
- onChanged(changeChecked(item.id, checked, items));
272
- }}
273
- ariaLabel={`Select ${item.name}`}
274
- checkedVariant={Variant.Light}
275
- uncheckedVariant={Variant.Light}
276
- />
291
+ {/* Checkbox and Text (optionally wrapped in a tooltip) */}
292
+ {
293
+ item.tooltip
294
+ ? (
295
+ <Tooltip text={item.tooltip}>
296
+ <span className="d-inline-block">
297
+ {checkbox}
298
+ </span>
299
+ </Tooltip>
300
+ )
301
+ : checkbox
302
+ }
277
303
 
278
304
  {/* Children */}
279
305
  {(item.isGroup && childExpanded[item.id]) && (
@@ -288,6 +314,7 @@ const NestableItemList: React.FC<Props> = (props) => {
288
314
  onChanged={(updatedItems) => {
289
315
  onChanged(changeItems(item.id, updatedItems));
290
316
  }}
317
+ startWithGroupsExpanded={startWithGroupsExpanded}
291
318
  />
292
319
  </div>
293
320
  )}
@@ -35,6 +35,8 @@ type Props = {
35
35
  noBottomMargin?: boolean,
36
36
  // If true, hide select all or none buttons
37
37
  hideSelectAllOrNoneButtons?: boolean,
38
+ // If true, groups start expanded instead of collapsed
39
+ startWithGroupsExpanded?: boolean,
38
40
  };
39
41
 
40
42
  /*------------------------------------------------------------------------*/
@@ -55,6 +57,7 @@ const ItemPicker: React.FC<Props> = (props) => {
55
57
  onChanged,
56
58
  noBottomMargin,
57
59
  hideSelectAllOrNoneButtons,
60
+ startWithGroupsExpanded,
58
61
  } = props;
59
62
 
60
63
  /*------------------------------------------------------------------------*/
@@ -171,6 +174,7 @@ const ItemPicker: React.FC<Props> = (props) => {
171
174
  <NestableItemList
172
175
  items={items}
173
176
  onChanged={onChanged}
177
+ startWithGroupsExpanded={startWithGroupsExpanded}
174
178
  />
175
179
  </div>
176
180
  </TabBox>
@@ -7,9 +7,15 @@ type PickableItem = (
7
7
  // Unique id of the item
8
8
  id: number | string,
9
9
  // Name of the item (human readable)
10
- name: string,
10
+ name: React.ReactNode,
11
+ // Accessible label for the item, used for screen readers. Required when
12
+ // "name" is not a plain string (e.g. a custom ReactNode); when "name" is
13
+ // a string, it is used as the label if this is not provided
14
+ ariaLabel?: string,
11
15
  // Link to view the item in the browser
12
16
  link?: string,
17
+ // If included, show this text in a tooltip when hovering over the item
18
+ tooltip?: string,
13
19
  }
14
20
  & (
15
21
  | {
@@ -63,6 +63,14 @@ const Tooltip: React.FC<Props> = (props) => {
63
63
  // Import bootstrap tooltip
64
64
  const BSTooltip = (await import('bootstrap')).Tooltip;
65
65
 
66
+ // The child may have unmounted while the dynamic import above was
67
+ // resolving (React nulls the ref on unmount). Constructing a
68
+ // Bootstrap tooltip with a null element causes an error,
69
+ // so bail out instead
70
+ if (!childRef.current) {
71
+ return;
72
+ }
73
+
66
74
  // Initialize
67
75
  t = new BSTooltip(
68
76
  childRef.current,
@@ -78,6 +86,15 @@ const Tooltip: React.FC<Props> = (props) => {
78
86
  return () => {
79
87
  if (t) {
80
88
  t.dispose();
89
+ // Bootstrap's hide() queues a transition-end callback that can
90
+ // fire AFTER dispose and touch the (now null) trigger map and
91
+ // element (twbs/bootstrap issue #37474). Point those private
92
+ // fields at harmless stand-ins so the late callback is a no-op
93
+ // instead of a crash.
94
+ /* eslint-disable no-underscore-dangle */
95
+ t._activeTrigger = {};
96
+ t._element = document.createElement('noscript');
97
+ /* eslint-enable no-underscore-dangle */
81
98
  }
82
99
  };
83
100
  },
@@ -96,6 +96,8 @@ export const _setStubResponse = (
96
96
  * @param opts.path - the path of the server endpoint
97
97
  * @param [opts.method=GET] - the method of the endpoint
98
98
  * @param [opts.params] - query/body parameters to include
99
+ * @param [opts.headers] - custom headers to include; values must already be
100
+ * string to avoid issue with header serialization
99
101
  * @returns response from server
100
102
  */
101
103
  const visitServerEndpoint = async (
@@ -103,6 +105,8 @@ const visitServerEndpoint = async (
103
105
  path: string,
104
106
  method?: ('GET' | 'POST' | 'DELETE' | 'PUT'),
105
107
  params?: { [key in string]: any },
108
+ // Headers have no serialization layer, so use string
109
+ headers?: { [key in string]: string },
106
110
  },
107
111
  ): Promise<any> => {
108
112
  // Set default method
@@ -159,6 +163,7 @@ const visitServerEndpoint = async (
159
163
  path: opts.path,
160
164
  method: opts.method ?? 'GET',
161
165
  params,
166
+ headers: opts.headers,
162
167
  });
163
168
 
164
169
  // Check for failure