@khanacademy/wonder-blocks-dropdown 9.1.5 → 9.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @khanacademy/wonder-blocks-dropdown
2
2
 
3
+ ## 9.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [4887c59]
8
+ - Updated dependencies [86e1901]
9
+ - Updated dependencies [61f7837]
10
+ - @khanacademy/wonder-blocks-modal@7.1.5
11
+ - @khanacademy/wonder-blocks-search-field@5.1.5
12
+
13
+ ## 9.2.0
14
+
15
+ ### Minor Changes
16
+
17
+ - af828a5: Integrates Announcer for value announcements in SingleSelect and MultiSelect
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [af828a5]
22
+ - Updated dependencies [fe17b20]
23
+ - @khanacademy/wonder-blocks-announcer@1.0.0
24
+
3
25
  ## 9.1.5
4
26
 
5
27
  ### Patch Changes
package/dist/es/index.js CHANGED
@@ -19,6 +19,7 @@ import { Popper } from 'react-popper';
19
19
  import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
20
20
  import { detectOverflow } from '@popperjs/core';
21
21
  import caretDownIcon from '@phosphor-icons/core/bold/caret-down-bold.svg';
22
+ import { announceMessage } from '@khanacademy/wonder-blocks-announcer';
22
23
  import caretDownIcon$1 from '@phosphor-icons/core/regular/caret-down.svg';
23
24
  import xIcon from '@phosphor-icons/core/regular/x.svg';
24
25
  import { TextField } from '@khanacademy/wonder-blocks-form';
@@ -855,7 +856,6 @@ function getSelectOpenerLabel(showOpenerLabelAsText, props) {
855
856
  }
856
857
 
857
858
  const VIRTUALIZE_THRESHOLD = 125;
858
- const StyledSpan$1 = addStyle("span");
859
859
  class DropdownCore extends React.Component {
860
860
  static sameItemsFocusable(prevItems, currentItems) {
861
861
  if (prevItems.length !== currentItems.length) {
@@ -1357,23 +1357,6 @@ class DropdownCore extends React.Component {
1357
1357
  referenceElement: openerElement
1358
1358
  }, isReferenceHidden => this.renderDropdownMenu(listRenderer, isReferenceHidden));
1359
1359
  }
1360
- renderLiveRegion() {
1361
- const {
1362
- items,
1363
- open
1364
- } = this.props;
1365
- const {
1366
- labels
1367
- } = this.state;
1368
- const totalItems = items.length;
1369
- return React.createElement(StyledSpan$1, {
1370
- "aria-live": "polite",
1371
- "aria-atomic": "true",
1372
- "aria-relevant": "additions text",
1373
- style: styles$6.srOnly,
1374
- "data-testid": "dropdown-live-region"
1375
- }, open && labels.someResults(totalItems));
1376
- }
1377
1360
  render() {
1378
1361
  const {
1379
1362
  open,
@@ -1387,7 +1370,7 @@ class DropdownCore extends React.Component {
1387
1370
  onKeyUp: !disabled ? this.handleKeyUp : undefined,
1388
1371
  style: [styles$6.menuWrapper, style],
1389
1372
  className: className
1390
- }, this.renderLiveRegion(), opener, open && this.renderDropdown());
1373
+ }, opener, open && this.renderDropdown());
1391
1374
  }
1392
1375
  }
1393
1376
  DropdownCore.defaultProps = {
@@ -2137,10 +2120,30 @@ const SingleSelect = props => {
2137
2120
  const handleClick = e => {
2138
2121
  handleOpenChanged(!open);
2139
2122
  };
2123
+ const handleAnnouncement = message => {
2124
+ announceMessage({
2125
+ message
2126
+ });
2127
+ };
2128
+ React.useEffect(() => {
2129
+ const optionItems = React.Children.toArray(children);
2130
+ const selectedItem = optionItems.find(option => option.props.value === selectedValue);
2131
+ if (selectedItem) {
2132
+ const label = getLabel(selectedItem.props);
2133
+ if (label) {
2134
+ handleAnnouncement(label);
2135
+ }
2136
+ }
2137
+ }, [selectedValue, children]);
2140
2138
  const renderOpener = (isDisabled, dropdownId) => {
2141
2139
  const items = React.Children.toArray(children);
2142
2140
  const selectedItem = items.find(option => option.props.value === selectedValue);
2143
- const menuText = selectedItem ? getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props) : placeholder;
2141
+ let menuContent;
2142
+ if (selectedItem) {
2143
+ menuContent = getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props);
2144
+ } else {
2145
+ menuContent = placeholder;
2146
+ }
2144
2147
  const dropdownOpener = React.createElement(Id, {
2145
2148
  id: id
2146
2149
  }, uniqueOpenerId => {
@@ -2153,7 +2156,7 @@ const SingleSelect = props => {
2153
2156
  disabled: isDisabled,
2154
2157
  ref: handleOpenerRef,
2155
2158
  role: "combobox",
2156
- text: menuText,
2159
+ text: menuContent,
2157
2160
  opened: open,
2158
2161
  error: hasError,
2159
2162
  onBlur: onOpenerBlurValidation
@@ -2169,7 +2172,7 @@ const SingleSelect = props => {
2169
2172
  ref: handleOpenerRef,
2170
2173
  testId: testId,
2171
2174
  onBlur: onOpenerBlurValidation
2172
- }), menuText);
2175
+ }), menuContent);
2173
2176
  });
2174
2177
  return dropdownOpener;
2175
2178
  };
@@ -2177,6 +2180,14 @@ const SingleSelect = props => {
2177
2180
  const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2178
2181
  const items = getMenuItems(allChildren);
2179
2182
  const isDisabled = numEnabledOptions === 0 || disabled;
2183
+ const {
2184
+ someResults
2185
+ } = labels;
2186
+ React.useEffect(() => {
2187
+ if (open) {
2188
+ handleAnnouncement(someResults(items.length));
2189
+ }
2190
+ }, [items.length, someResults, open]);
2180
2191
  return React.createElement(Id, {
2181
2192
  id: dropdownId
2182
2193
  }, uniqueDropdownId => React.createElement(DropdownCore$1, {
@@ -2236,7 +2247,9 @@ const MultiSelect = props => {
2236
2247
  required
2237
2248
  } = props,
2238
2249
  sharedProps = _objectWithoutPropertiesLoose(props, _excluded);
2239
- const labels = _extends({}, defaultLabels, propLabels);
2250
+ const labels = React.useMemo(() => {
2251
+ return _extends({}, defaultLabels, propLabels);
2252
+ }, [propLabels]);
2240
2253
  const [open, setOpen] = React.useState(false);
2241
2254
  const [searchText, setSearchText] = React.useState("");
2242
2255
  const [lastSelectedValues, setLastSelectedValues] = React.useState([]);
@@ -2298,7 +2311,7 @@ const MultiSelect = props => {
2298
2311
  onChange([]);
2299
2312
  onSelectedValuesChangeValidation();
2300
2313
  };
2301
- const getMenuTextOrNode = children => {
2314
+ const getMenuTextOrNode = React.useCallback(children => {
2302
2315
  const {
2303
2316
  noneSelected,
2304
2317
  someSelected,
@@ -2325,7 +2338,7 @@ const MultiSelect = props => {
2325
2338
  default:
2326
2339
  return someSelected(selectedValues.length);
2327
2340
  }
2328
- };
2341
+ }, [implicitAllEnabled, labels, selectedValues, showOpenerLabelAsText]);
2329
2342
  const getShortcuts = numOptions => {
2330
2343
  const {
2331
2344
  selectAllLabel,
@@ -2420,11 +2433,34 @@ const MultiSelect = props => {
2420
2433
  const handleClick = e => {
2421
2434
  handleOpenChanged(!open);
2422
2435
  };
2436
+ const handleAnnouncement = message => {
2437
+ announceMessage({
2438
+ message
2439
+ });
2440
+ };
2441
+ const maybeGetOpenerStringValue = React.useCallback((children, openerContent) => {
2442
+ let openerStringValue;
2443
+ if (selectedValues.length === 1) {
2444
+ const selectedItem = children.find(option => option.props.value === selectedValues[0]);
2445
+ openerStringValue = selectedItem ? getLabel(selectedItem == null ? void 0 : selectedItem.props) : undefined;
2446
+ } else if (typeof openerContent === "string") {
2447
+ openerStringValue = openerContent;
2448
+ }
2449
+ return openerStringValue;
2450
+ }, [selectedValues]);
2451
+ React.useEffect(() => {
2452
+ const optionItems = React.Children.toArray(children);
2453
+ const openerContent = getMenuTextOrNode(optionItems);
2454
+ const openerStringValue = maybeGetOpenerStringValue(optionItems, openerContent);
2455
+ if (openerStringValue) {
2456
+ handleAnnouncement(openerStringValue);
2457
+ }
2458
+ }, [children, getMenuTextOrNode, maybeGetOpenerStringValue]);
2423
2459
  const renderOpener = (allChildren, isDisabled, dropdownId) => {
2424
2460
  const {
2425
2461
  noneSelected
2426
2462
  } = labels;
2427
- const menuContent = getMenuTextOrNode(allChildren);
2463
+ const openerContent = getMenuTextOrNode(allChildren);
2428
2464
  const dropdownOpener = React.createElement(Id, {
2429
2465
  id: id
2430
2466
  }, uniqueOpenerId => {
@@ -2439,7 +2475,7 @@ const MultiSelect = props => {
2439
2475
  disabled: isDisabled,
2440
2476
  ref: handleOpenerRef,
2441
2477
  role: "combobox",
2442
- text: menuContent,
2478
+ text: openerContent,
2443
2479
  opened: open
2444
2480
  }, opener) : React.createElement(SelectOpener, _extends({}, sharedProps, {
2445
2481
  error: hasError,
@@ -2447,13 +2483,13 @@ const MultiSelect = props => {
2447
2483
  id: uniqueOpenerId,
2448
2484
  "aria-label": ariaLabel,
2449
2485
  "aria-controls": dropdownId,
2450
- isPlaceholder: menuContent === noneSelected,
2486
+ isPlaceholder: openerContent === noneSelected,
2451
2487
  onOpenChanged: handleOpenChanged,
2452
2488
  onBlur: onOpenerBlurValidation,
2453
2489
  open: open,
2454
2490
  ref: handleOpenerRef,
2455
2491
  testId: testId
2456
- }), menuContent);
2492
+ }), openerContent);
2457
2493
  });
2458
2494
  return dropdownOpener;
2459
2495
  };
@@ -2467,6 +2503,11 @@ const MultiSelect = props => {
2467
2503
  const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2468
2504
  const filteredItems = getMenuItems(allChildren);
2469
2505
  const isDisabled = numEnabledOptions === 0 || disabled;
2506
+ React.useEffect(() => {
2507
+ if (open) {
2508
+ handleAnnouncement(someSelected(filteredItems.length));
2509
+ }
2510
+ }, [filteredItems.length, someSelected, open]);
2470
2511
  return React.createElement(Id, {
2471
2512
  id: dropdownId
2472
2513
  }, uniqueDropdownId => React.createElement(DropdownCore$1, {
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var reactPopper = require('react-popper');
22
22
  var wonderBlocksModal = require('@khanacademy/wonder-blocks-modal');
23
23
  var core = require('@popperjs/core');
24
24
  var caretDownIcon = require('@phosphor-icons/core/bold/caret-down-bold.svg');
25
+ var wonderBlocksAnnouncer = require('@khanacademy/wonder-blocks-announcer');
25
26
  var caretDownIcon$1 = require('@phosphor-icons/core/regular/caret-down.svg');
26
27
  var xIcon = require('@phosphor-icons/core/regular/x.svg');
27
28
  var wonderBlocksForm = require('@khanacademy/wonder-blocks-form');
@@ -890,7 +891,6 @@ function getSelectOpenerLabel(showOpenerLabelAsText, props) {
890
891
  }
891
892
 
892
893
  const VIRTUALIZE_THRESHOLD = 125;
893
- const StyledSpan$1 = wonderBlocksCore.addStyle("span");
894
894
  class DropdownCore extends React__namespace.Component {
895
895
  static sameItemsFocusable(prevItems, currentItems) {
896
896
  if (prevItems.length !== currentItems.length) {
@@ -1392,23 +1392,6 @@ class DropdownCore extends React__namespace.Component {
1392
1392
  referenceElement: openerElement
1393
1393
  }, isReferenceHidden => this.renderDropdownMenu(listRenderer, isReferenceHidden));
1394
1394
  }
1395
- renderLiveRegion() {
1396
- const {
1397
- items,
1398
- open
1399
- } = this.props;
1400
- const {
1401
- labels
1402
- } = this.state;
1403
- const totalItems = items.length;
1404
- return React__namespace.createElement(StyledSpan$1, {
1405
- "aria-live": "polite",
1406
- "aria-atomic": "true",
1407
- "aria-relevant": "additions text",
1408
- style: styles$6.srOnly,
1409
- "data-testid": "dropdown-live-region"
1410
- }, open && labels.someResults(totalItems));
1411
- }
1412
1395
  render() {
1413
1396
  const {
1414
1397
  open,
@@ -1422,7 +1405,7 @@ class DropdownCore extends React__namespace.Component {
1422
1405
  onKeyUp: !disabled ? this.handleKeyUp : undefined,
1423
1406
  style: [styles$6.menuWrapper, style],
1424
1407
  className: className
1425
- }, this.renderLiveRegion(), opener, open && this.renderDropdown());
1408
+ }, opener, open && this.renderDropdown());
1426
1409
  }
1427
1410
  }
1428
1411
  DropdownCore.defaultProps = {
@@ -2172,10 +2155,30 @@ const SingleSelect = props => {
2172
2155
  const handleClick = e => {
2173
2156
  handleOpenChanged(!open);
2174
2157
  };
2158
+ const handleAnnouncement = message => {
2159
+ wonderBlocksAnnouncer.announceMessage({
2160
+ message
2161
+ });
2162
+ };
2163
+ React__namespace.useEffect(() => {
2164
+ const optionItems = React__namespace.Children.toArray(children);
2165
+ const selectedItem = optionItems.find(option => option.props.value === selectedValue);
2166
+ if (selectedItem) {
2167
+ const label = getLabel(selectedItem.props);
2168
+ if (label) {
2169
+ handleAnnouncement(label);
2170
+ }
2171
+ }
2172
+ }, [selectedValue, children]);
2175
2173
  const renderOpener = (isDisabled, dropdownId) => {
2176
2174
  const items = React__namespace.Children.toArray(children);
2177
2175
  const selectedItem = items.find(option => option.props.value === selectedValue);
2178
- const menuText = selectedItem ? getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props) : placeholder;
2176
+ let menuContent;
2177
+ if (selectedItem) {
2178
+ menuContent = getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props);
2179
+ } else {
2180
+ menuContent = placeholder;
2181
+ }
2179
2182
  const dropdownOpener = React__namespace.createElement(wonderBlocksCore.Id, {
2180
2183
  id: id
2181
2184
  }, uniqueOpenerId => {
@@ -2188,7 +2191,7 @@ const SingleSelect = props => {
2188
2191
  disabled: isDisabled,
2189
2192
  ref: handleOpenerRef,
2190
2193
  role: "combobox",
2191
- text: menuText,
2194
+ text: menuContent,
2192
2195
  opened: open,
2193
2196
  error: hasError,
2194
2197
  onBlur: onOpenerBlurValidation
@@ -2204,7 +2207,7 @@ const SingleSelect = props => {
2204
2207
  ref: handleOpenerRef,
2205
2208
  testId: testId,
2206
2209
  onBlur: onOpenerBlurValidation
2207
- }), menuText);
2210
+ }), menuContent);
2208
2211
  });
2209
2212
  return dropdownOpener;
2210
2213
  };
@@ -2212,6 +2215,14 @@ const SingleSelect = props => {
2212
2215
  const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2213
2216
  const items = getMenuItems(allChildren);
2214
2217
  const isDisabled = numEnabledOptions === 0 || disabled;
2218
+ const {
2219
+ someResults
2220
+ } = labels;
2221
+ React__namespace.useEffect(() => {
2222
+ if (open) {
2223
+ handleAnnouncement(someResults(items.length));
2224
+ }
2225
+ }, [items.length, someResults, open]);
2215
2226
  return React__namespace.createElement(wonderBlocksCore.Id, {
2216
2227
  id: dropdownId
2217
2228
  }, uniqueDropdownId => React__namespace.createElement(DropdownCore$1, {
@@ -2271,7 +2282,9 @@ const MultiSelect = props => {
2271
2282
  required
2272
2283
  } = props,
2273
2284
  sharedProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded);
2274
- const labels = _extends__default["default"]({}, defaultLabels, propLabels);
2285
+ const labels = React__namespace.useMemo(() => {
2286
+ return _extends__default["default"]({}, defaultLabels, propLabels);
2287
+ }, [propLabels]);
2275
2288
  const [open, setOpen] = React__namespace.useState(false);
2276
2289
  const [searchText, setSearchText] = React__namespace.useState("");
2277
2290
  const [lastSelectedValues, setLastSelectedValues] = React__namespace.useState([]);
@@ -2333,7 +2346,7 @@ const MultiSelect = props => {
2333
2346
  onChange([]);
2334
2347
  onSelectedValuesChangeValidation();
2335
2348
  };
2336
- const getMenuTextOrNode = children => {
2349
+ const getMenuTextOrNode = React__namespace.useCallback(children => {
2337
2350
  const {
2338
2351
  noneSelected,
2339
2352
  someSelected,
@@ -2360,7 +2373,7 @@ const MultiSelect = props => {
2360
2373
  default:
2361
2374
  return someSelected(selectedValues.length);
2362
2375
  }
2363
- };
2376
+ }, [implicitAllEnabled, labels, selectedValues, showOpenerLabelAsText]);
2364
2377
  const getShortcuts = numOptions => {
2365
2378
  const {
2366
2379
  selectAllLabel,
@@ -2455,11 +2468,34 @@ const MultiSelect = props => {
2455
2468
  const handleClick = e => {
2456
2469
  handleOpenChanged(!open);
2457
2470
  };
2471
+ const handleAnnouncement = message => {
2472
+ wonderBlocksAnnouncer.announceMessage({
2473
+ message
2474
+ });
2475
+ };
2476
+ const maybeGetOpenerStringValue = React__namespace.useCallback((children, openerContent) => {
2477
+ let openerStringValue;
2478
+ if (selectedValues.length === 1) {
2479
+ const selectedItem = children.find(option => option.props.value === selectedValues[0]);
2480
+ openerStringValue = selectedItem ? getLabel(selectedItem == null ? void 0 : selectedItem.props) : undefined;
2481
+ } else if (typeof openerContent === "string") {
2482
+ openerStringValue = openerContent;
2483
+ }
2484
+ return openerStringValue;
2485
+ }, [selectedValues]);
2486
+ React__namespace.useEffect(() => {
2487
+ const optionItems = React__namespace.Children.toArray(children);
2488
+ const openerContent = getMenuTextOrNode(optionItems);
2489
+ const openerStringValue = maybeGetOpenerStringValue(optionItems, openerContent);
2490
+ if (openerStringValue) {
2491
+ handleAnnouncement(openerStringValue);
2492
+ }
2493
+ }, [children, getMenuTextOrNode, maybeGetOpenerStringValue]);
2458
2494
  const renderOpener = (allChildren, isDisabled, dropdownId) => {
2459
2495
  const {
2460
2496
  noneSelected
2461
2497
  } = labels;
2462
- const menuContent = getMenuTextOrNode(allChildren);
2498
+ const openerContent = getMenuTextOrNode(allChildren);
2463
2499
  const dropdownOpener = React__namespace.createElement(wonderBlocksCore.Id, {
2464
2500
  id: id
2465
2501
  }, uniqueOpenerId => {
@@ -2474,7 +2510,7 @@ const MultiSelect = props => {
2474
2510
  disabled: isDisabled,
2475
2511
  ref: handleOpenerRef,
2476
2512
  role: "combobox",
2477
- text: menuContent,
2513
+ text: openerContent,
2478
2514
  opened: open
2479
2515
  }, opener) : React__namespace.createElement(SelectOpener, _extends__default["default"]({}, sharedProps, {
2480
2516
  error: hasError,
@@ -2482,13 +2518,13 @@ const MultiSelect = props => {
2482
2518
  id: uniqueOpenerId,
2483
2519
  "aria-label": ariaLabel,
2484
2520
  "aria-controls": dropdownId,
2485
- isPlaceholder: menuContent === noneSelected,
2521
+ isPlaceholder: openerContent === noneSelected,
2486
2522
  onOpenChanged: handleOpenChanged,
2487
2523
  onBlur: onOpenerBlurValidation,
2488
2524
  open: open,
2489
2525
  ref: handleOpenerRef,
2490
2526
  testId: testId
2491
- }), menuContent);
2527
+ }), openerContent);
2492
2528
  });
2493
2529
  return dropdownOpener;
2494
2530
  };
@@ -2502,6 +2538,11 @@ const MultiSelect = props => {
2502
2538
  const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2503
2539
  const filteredItems = getMenuItems(allChildren);
2504
2540
  const isDisabled = numEnabledOptions === 0 || disabled;
2541
+ React__namespace.useEffect(() => {
2542
+ if (open) {
2543
+ handleAnnouncement(someSelected(filteredItems.length));
2544
+ }
2545
+ }, [filteredItems.length, someSelected, open]);
2505
2546
  return React__namespace.createElement(wonderBlocksCore.Id, {
2506
2547
  id: dropdownId
2507
2548
  }, uniqueDropdownId => React__namespace.createElement(DropdownCore$1, {
@@ -22,7 +22,7 @@ type OptionItemProps = PropsFor<typeof OptionItem>;
22
22
  */
23
23
  export declare function getLabel(props: OptionItemProps): string;
24
24
  /**
25
- * Returns the label for the SelectOpener in the SingleSelect and MultiSelect.
25
+ * Returns the label for the SelectOpener in SingleSelect.
26
26
  * If the label is a Node, and `labelAsText` is undefined, returns the label.
27
27
  */
28
28
  export declare function getSelectOpenerLabel(showOpenerLabelAsText: boolean, props: OptionItemProps): string | JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-dropdown",
3
- "version": "9.1.5",
3
+ "version": "9.2.1",
4
4
  "design": "v1",
5
5
  "description": "Dropdown variants for Wonder Blocks.",
6
6
  "main": "dist/index.js",
@@ -13,14 +13,15 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@babel/runtime": "^7.24.5",
16
+ "@khanacademy/wonder-blocks-announcer": "1.0.0",
16
17
  "@khanacademy/wonder-blocks-cell": "4.1.4",
17
18
  "@khanacademy/wonder-blocks-clickable": "6.1.4",
18
19
  "@khanacademy/wonder-blocks-core": "12.2.1",
19
20
  "@khanacademy/wonder-blocks-icon": "5.1.3",
20
21
  "@khanacademy/wonder-blocks-layout": "3.1.4",
21
- "@khanacademy/wonder-blocks-modal": "7.1.4",
22
+ "@khanacademy/wonder-blocks-modal": "7.1.5",
22
23
  "@khanacademy/wonder-blocks-pill": "3.1.4",
23
- "@khanacademy/wonder-blocks-search-field": "5.1.4",
24
+ "@khanacademy/wonder-blocks-search-field": "5.1.5",
24
25
  "@khanacademy/wonder-blocks-timing": "7.0.2",
25
26
  "@khanacademy/wonder-blocks-tokens": "5.1.1",
26
27
  "@khanacademy/wonder-blocks-typography": "3.1.3"