@spaced-out/ui-design-system 0.6.36 → 0.6.37-beta.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/.cspell/custom-words.txt +9 -0
- package/CHANGELOG.md +7 -0
- package/lib/components/ButtonDropdown/ButtonDropdown.d.ts +10 -0
- package/lib/components/ButtonDropdown/ButtonDropdown.d.ts.map +1 -1
- package/lib/components/ButtonDropdown/ButtonDropdown.js +26 -3
- package/lib/components/ButtonTabs/ButtonTab/ButtonTab.d.ts +9 -1
- package/lib/components/ButtonTabs/ButtonTab/ButtonTab.d.ts.map +1 -1
- package/lib/components/ButtonTabs/ButtonTab/ButtonTab.js +8 -0
- package/lib/components/ButtonTabs/ButtonTabDropdown.d.ts +10 -1
- package/lib/components/ButtonTabs/ButtonTabDropdown.d.ts.map +1 -1
- package/lib/components/ButtonTabs/ButtonTabDropdown.js +31 -3
- package/lib/components/ButtonTabs/ButtonTabs.d.ts +6 -0
- package/lib/components/ButtonTabs/ButtonTabs.d.ts.map +1 -1
- package/lib/components/ButtonTabs/ButtonTabs.js +2 -0
- package/lib/components/Combobox/Combobox.d.ts +9 -0
- package/lib/components/Combobox/Combobox.d.ts.map +1 -1
- package/lib/components/Combobox/Combobox.js +21 -0
- package/lib/components/Combobox/helper.d.ts.map +1 -1
- package/lib/components/Combobox/helper.js +0 -4
- package/lib/components/Dropdown/Dropdown.d.ts +9 -0
- package/lib/components/Dropdown/Dropdown.d.ts.map +1 -1
- package/lib/components/Dropdown/Dropdown.js +92 -24
- package/lib/components/Dropdown/Dropdown.module.css +24 -0
- package/lib/components/InlineDropdown/InlineDropdown.d.ts +10 -0
- package/lib/components/InlineDropdown/InlineDropdown.d.ts.map +1 -1
- package/lib/components/InlineDropdown/InlineDropdown.js +23 -3
- package/lib/components/Input/Input.module.css +38 -0
- package/lib/components/Menu/Menu.d.ts +99 -0
- package/lib/components/Menu/Menu.d.ts.map +1 -1
- package/lib/components/Menu/Menu.js +1152 -187
- package/lib/components/Menu/Menu.module.css +191 -7
- package/lib/components/Menu/MenuFocusScope.d.ts +21 -0
- package/lib/components/Menu/MenuFocusScope.d.ts.map +1 -0
- package/lib/components/Menu/MenuFocusScope.js +37 -0
- package/lib/components/Menu/MenuOptionButton.d.ts +36 -0
- package/lib/components/Menu/MenuOptionButton.d.ts.map +1 -1
- package/lib/components/Menu/MenuOptionButton.js +137 -29
- package/lib/components/Menu/index.d.ts +2 -0
- package/lib/components/Menu/index.d.ts.map +1 -1
- package/lib/components/Menu/index.js +22 -0
- package/lib/components/Menu/menuOptionId.d.ts +12 -0
- package/lib/components/Menu/menuOptionId.d.ts.map +1 -0
- package/lib/components/Menu/menuOptionId.js +18 -0
- package/lib/components/Menu/useMenuKeyboardNavigation.d.ts +93 -0
- package/lib/components/Menu/useMenuKeyboardNavigation.d.ts.map +1 -0
- package/lib/components/Menu/useMenuKeyboardNavigation.js +471 -0
- package/lib/components/Menu/useMenuTrigger.d.ts +207 -0
- package/lib/components/Menu/useMenuTrigger.d.ts.map +1 -0
- package/lib/components/Menu/useMenuTrigger.js +389 -0
- package/lib/components/OptionButton/OptionButton.d.ts +16 -0
- package/lib/components/OptionButton/OptionButton.d.ts.map +1 -1
- package/lib/components/OptionButton/OptionButton.js +4 -1
- package/lib/components/Tabs/Tab/Tab.d.ts +8 -1
- package/lib/components/Tabs/Tab/Tab.d.ts.map +1 -1
- package/lib/components/Tabs/Tab/Tab.js +8 -0
- package/lib/components/Tabs/TabList/TabDropdown.d.ts +10 -1
- package/lib/components/Tabs/TabList/TabDropdown.d.ts.map +1 -1
- package/lib/components/Tabs/TabList/TabDropdown.js +23 -3
- package/lib/components/Tabs/TabList/TabList.d.ts +14 -0
- package/lib/components/Tabs/TabList/TabList.d.ts.map +1 -1
- package/lib/components/Tabs/TabList/TabList.js +6 -0
- package/lib/components/Typeahead/Typeahead.d.ts +9 -0
- package/lib/components/Typeahead/Typeahead.d.ts.map +1 -1
- package/lib/components/Typeahead/Typeahead.js +107 -13
- package/lib/components/Typeahead/Typeahead.module.css +13 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.d.ts.map +1 -1
- package/lib/hooks/index.js +11 -0
- package/lib/hooks/useInteractionModality/index.d.ts +2 -0
- package/lib/hooks/useInteractionModality/index.d.ts.map +1 -0
- package/lib/hooks/useInteractionModality/index.js +16 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.d.ts +39 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.d.ts.map +1 -0
- package/lib/hooks/useInteractionModality/useInteractionModality.js +111 -0
- package/lib/utils/click-away/click-away.d.ts +16 -0
- package/lib/utils/click-away/click-away.d.ts.map +1 -1
- package/lib/utils/click-away/click-away.js +49 -0
- package/mcp/package.json +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
@value (
|
|
10
10
|
colorBackgroundSecondary,
|
|
11
11
|
colorFillSecondary,
|
|
12
|
+
colorFillPrimary,
|
|
12
13
|
colorTextDisabled,
|
|
13
14
|
colorTextPrimary,
|
|
14
15
|
colorTextSecondary,
|
|
@@ -60,6 +61,17 @@
|
|
|
60
61
|
z-index: elevationMenu;
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/*
|
|
65
|
+
* The options carry their own scroll container, so the card must not scroll as
|
|
66
|
+
* well — otherwise both would show a scrollbar. `hidden` also keeps a pinned
|
|
67
|
+
* header / footer inside the border radius.
|
|
68
|
+
*/
|
|
69
|
+
.menuCardA11y.menuCardTopPaddingZero,
|
|
70
|
+
.menuCardA11y.menuCardBottomPaddingZero,
|
|
71
|
+
.menuCardNoScroll {
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
.menuCardTopPaddingZero {
|
|
64
76
|
padding-top: spaceNone;
|
|
65
77
|
}
|
|
@@ -68,7 +80,28 @@
|
|
|
68
80
|
padding-bottom: spaceNone;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
|
|
83
|
+
/*
|
|
84
|
+
* Scrolls the options only. A header / footer is a flex sibling of this element
|
|
85
|
+
* rather than something sticky inside it, so the scrollbar spans just the option
|
|
86
|
+
* list — it no longer runs the whole height of the card behind the bars — and no
|
|
87
|
+
* option can be clipped under them. `min-height: 0` is what lets a flex item
|
|
88
|
+
* actually shrink and scroll instead of growing to fit its content.
|
|
89
|
+
*/
|
|
90
|
+
.optionsScroll {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-flow: column;
|
|
93
|
+
flex: 1 1 auto;
|
|
94
|
+
min-height: 0;
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* Collapse a menu that renders nothing. `:empty` alone no longer suffices: the
|
|
100
|
+
* card always holds the (possibly empty) role="listbox" element so the trigger's
|
|
101
|
+
* aria-controls resolves, so Menu also sets `.menuCardEmpty` explicitly.
|
|
102
|
+
*/
|
|
103
|
+
.menuCard:empty,
|
|
104
|
+
.menuCard.menuCardEmpty {
|
|
72
105
|
padding: spaceNone;
|
|
73
106
|
border: none;
|
|
74
107
|
}
|
|
@@ -179,11 +212,56 @@
|
|
|
179
212
|
color: colorTextSecondary;
|
|
180
213
|
}
|
|
181
214
|
|
|
215
|
+
/*
|
|
216
|
+
* These two suppress the browser's own focus ring, which a CLICK produces:
|
|
217
|
+
* clicking an option focuses it, and `:focus-visible` deliberately does not
|
|
218
|
+
* match pointer-driven focus, so without them a clicked option grows a native
|
|
219
|
+
* outline. Unscoped, because that is true whether or not a consumer opted in.
|
|
220
|
+
*/
|
|
182
221
|
.option:focus {
|
|
183
222
|
outline: none;
|
|
184
223
|
color: colorTextPrimary;
|
|
185
224
|
}
|
|
186
225
|
|
|
226
|
+
.option:focus-within {
|
|
227
|
+
outline: none;
|
|
228
|
+
color: colorTextPrimary;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.option:focus .optionTextSecondaryLabel {
|
|
232
|
+
color: colorTextSecondary;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* Selected value (committed), shown as a stronger fill than the active highlight.
|
|
237
|
+
*
|
|
238
|
+
* Deliberately no left accent bar: a 2px inset box-shadow gets tapered by the
|
|
239
|
+
* option's 8px border-radius, so it renders as a small curved sliver at the edge
|
|
240
|
+
* that reads as a stray focus artefact rather than an intentional indicator. The
|
|
241
|
+
* checkbox / radio glyph carries the state in those variants, and forced-colors
|
|
242
|
+
* mode below re-expresses it as a full outline, which the radius follows cleanly.
|
|
243
|
+
*/
|
|
244
|
+
.option.selected {
|
|
245
|
+
background: colorFillSecondary;
|
|
246
|
+
outline: none;
|
|
247
|
+
color: colorTextPrimary;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* Focus ring — KEYBOARD ONLY. A focus indicator has to mean "focus is here", so
|
|
252
|
+
* it must not appear under the mouse pointer:
|
|
253
|
+
*
|
|
254
|
+
* - `.activeRing` is set by Menu for the combobox modes (Dropdown / Typeahead),
|
|
255
|
+
* where DOM focus stays on the input and the active option is virtual. Menu
|
|
256
|
+
* withholds it when the highlight came from the pointer.
|
|
257
|
+
* - `:focus-visible` covers the roving menus, which move real DOM focus. It is
|
|
258
|
+
* exactly the right selector here: arrow-key navigation calls .focus() after a
|
|
259
|
+
* key press so the ring shows, while hover-driven .focus() follows a pointer
|
|
260
|
+
* interaction so it does not.
|
|
261
|
+
*
|
|
262
|
+
* Declared after `.selected`, which sets no box-shadow of its own, so a selected
|
|
263
|
+
* option that also has focus shows the ring without anything to fight over.
|
|
264
|
+
*/
|
|
187
265
|
.option:focus-visible {
|
|
188
266
|
outline: none;
|
|
189
267
|
color: colorTextPrimary;
|
|
@@ -191,20 +269,57 @@
|
|
|
191
269
|
borderWidthTertiary colorFocusSecondary;
|
|
192
270
|
}
|
|
193
271
|
|
|
194
|
-
|
|
272
|
+
/*
|
|
273
|
+
* The opt-in's own indicators, scoped to it so a consumer who has not opted in
|
|
274
|
+
* keeps exactly the option styling they had. `.active` is the highlight the
|
|
275
|
+
* keyboard moves; `.activeRing` is the focus ring for the combobox modes, where
|
|
276
|
+
* DOM focus stays on the input and `:focus-visible` can never match the option.
|
|
277
|
+
*/
|
|
278
|
+
.menuCardA11y .option.active {
|
|
195
279
|
outline: none;
|
|
196
280
|
color: colorTextPrimary;
|
|
197
|
-
|
|
281
|
+
background: colorBackgroundSecondary;
|
|
198
282
|
}
|
|
199
283
|
|
|
200
|
-
.option.
|
|
201
|
-
|
|
284
|
+
.menuCardA11y .option.active .optionTextSecondaryLabel {
|
|
285
|
+
color: colorTextSecondary;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.menuCardA11y .option.activeRing {
|
|
202
289
|
outline: none;
|
|
203
290
|
color: colorTextPrimary;
|
|
291
|
+
box-shadow: inset borderWidthNone borderWidthNone borderWidthNone
|
|
292
|
+
borderWidthTertiary colorFocusSecondary;
|
|
204
293
|
}
|
|
205
294
|
|
|
206
|
-
|
|
207
|
-
|
|
295
|
+
/*
|
|
296
|
+
* Windows High Contrast / forced-colors mode drops box-shadow and flattens
|
|
297
|
+
* background, so the bar + tint indicators above vanish. Re-express the active
|
|
298
|
+
* highlight and selected state with system-color outlines (honored in HCM).
|
|
299
|
+
*/
|
|
300
|
+
@media (forced-colors: active) {
|
|
301
|
+
.menuCardA11y .option:hover,
|
|
302
|
+
.menuCardA11y .option.active {
|
|
303
|
+
outline: borderWidthTertiary solid Highlight;
|
|
304
|
+
outline-offset: calc(-1 * borderWidthTertiary);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.menuCardA11y .option.selected {
|
|
308
|
+
outline: borderWidthTertiary solid SelectedItem;
|
|
309
|
+
outline-offset: calc(-1 * borderWidthTertiary);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/*
|
|
313
|
+
* Keyboard focus must stay distinguishable from BOTH the hover highlight and
|
|
314
|
+
* the selected state, and Highlight / SelectedItem commonly resolve to the same
|
|
315
|
+
* system color — so change the outline STYLE rather than add a third color.
|
|
316
|
+
* Declared last so a selected option that also has focus still shows it.
|
|
317
|
+
*/
|
|
318
|
+
.menuCardA11y .option.activeRing,
|
|
319
|
+
.menuCardA11y .option:focus-visible {
|
|
320
|
+
outline: borderWidthTertiary dashed Highlight;
|
|
321
|
+
outline-offset: calc(-1 * borderWidthTertiary);
|
|
322
|
+
}
|
|
208
323
|
}
|
|
209
324
|
|
|
210
325
|
.disabled {
|
|
@@ -244,6 +359,55 @@
|
|
|
244
359
|
flex-flow: column;
|
|
245
360
|
}
|
|
246
361
|
|
|
362
|
+
/*
|
|
363
|
+
* Wraps only the option list so the listbox role contains option children
|
|
364
|
+
* exclusively (search/header/footer stay outside it). display:contents keeps
|
|
365
|
+
* the existing flex layout of .menuCard unchanged.
|
|
366
|
+
*/
|
|
367
|
+
.optionsListbox {
|
|
368
|
+
display: contents;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/*
|
|
372
|
+
* The listbox as the element that actually scrolls. Under virtualization
|
|
373
|
+
* react-window's own scroll box carries the role (see ListboxOuter); this is the
|
|
374
|
+
* same arrangement for a plain option list, and it matters beyond tidiness: the
|
|
375
|
+
* options are not focusable themselves whenever a combobox trigger drives the
|
|
376
|
+
* menu, so a
|
|
377
|
+
* scroll container that is NOT the combobox's popup reads as an unreachable
|
|
378
|
+
* scroll area (axe `scrollable-region-focusable`). Making the listbox the
|
|
379
|
+
* scroller means the region IS the popup, which that rule exempts.
|
|
380
|
+
*/
|
|
381
|
+
.optionsListboxScroll {
|
|
382
|
+
display: flex;
|
|
383
|
+
flex-flow: column;
|
|
384
|
+
flex: 1 1 auto;
|
|
385
|
+
min-height: 0;
|
|
386
|
+
overflow-y: auto;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/*
|
|
390
|
+
* The listbox takes a tab stop only where the menu owns its filter field, and in
|
|
391
|
+
* that shape it reports the cursor through `aria-activedescendant` — the ring is
|
|
392
|
+
* drawn on the active OPTION, which is what APG prescribes for that pattern.
|
|
393
|
+
* Leaving the browser's own ring on the container as well shows two focus
|
|
394
|
+
* indicators at once, the outer one around a box the user cannot move, which
|
|
395
|
+
* reads as a rendering fault rather than as focus. Applied to whichever element
|
|
396
|
+
* holds the role: this one, or react-window's scroll box when virtualized.
|
|
397
|
+
*/
|
|
398
|
+
.listboxFocusContainer:focus,
|
|
399
|
+
.listboxFocusContainer:focus-visible {
|
|
400
|
+
outline: none;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/*
|
|
404
|
+
* role="group" wrapper around each titled option group. display:contents keeps
|
|
405
|
+
* the existing flex layout (title + options) unchanged.
|
|
406
|
+
*/
|
|
407
|
+
.optionGroup {
|
|
408
|
+
display: contents;
|
|
409
|
+
}
|
|
410
|
+
|
|
247
411
|
.groupTitleWrapper {
|
|
248
412
|
composes: formLabelSmall from '../../styles/typography.module.css';
|
|
249
413
|
display: flex;
|
|
@@ -260,6 +424,11 @@
|
|
|
260
424
|
margin-left: spaceSmall;
|
|
261
425
|
}
|
|
262
426
|
|
|
427
|
+
/*
|
|
428
|
+
* Pinned by living OUTSIDE the options scroll container rather than by sticking
|
|
429
|
+
* inside it, so it never has the option list's scrollbar running behind it and
|
|
430
|
+
* never has an option tucked under it. `relative` positions the divider below.
|
|
431
|
+
*/
|
|
263
432
|
.menuHeader {
|
|
264
433
|
position: sticky;
|
|
265
434
|
background-color: colorBackgroundTertiary;
|
|
@@ -271,6 +440,20 @@
|
|
|
271
440
|
max-height: size50;
|
|
272
441
|
}
|
|
273
442
|
|
|
443
|
+
/*
|
|
444
|
+
* With the opt-in the options carry their own scroll container, so the bars are
|
|
445
|
+
* pinned by living OUTSIDE it rather than by sticking inside it: the option
|
|
446
|
+
* list's scrollbar never runs behind them and no option is ever tucked under
|
|
447
|
+
* them. `relative` positions the divider below.
|
|
448
|
+
*/
|
|
449
|
+
.menuCardA11y .menuHeader,
|
|
450
|
+
.menuCardA11y .menuFooter {
|
|
451
|
+
position: relative;
|
|
452
|
+
flex: 0 0 auto;
|
|
453
|
+
top: auto;
|
|
454
|
+
bottom: auto;
|
|
455
|
+
}
|
|
456
|
+
|
|
274
457
|
.menuHeader::after {
|
|
275
458
|
content: '';
|
|
276
459
|
position: absolute;
|
|
@@ -280,6 +463,7 @@
|
|
|
280
463
|
border-bottom: borderWidthPrimary solid colorBorderPrimary;
|
|
281
464
|
}
|
|
282
465
|
|
|
466
|
+
/* As `.menuHeader`, pinned by sitting outside the options scroll container. */
|
|
283
467
|
.menuFooter {
|
|
284
468
|
position: sticky;
|
|
285
469
|
background-color: colorBackgroundTertiary;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { FloatingFocusManager } from '@floating-ui/react';
|
|
3
|
+
/**
|
|
4
|
+
* floating-ui's `FloatingFocusManager`, or nothing at all.
|
|
5
|
+
*
|
|
6
|
+
* Mounting it is what manages focus, so there is no `disabled` prop to pass —
|
|
7
|
+
* the choice has to be whether the element exists. Every popup trigger in the
|
|
8
|
+
* dropdown family needs that choice: without `enableA11y` focus is managed by
|
|
9
|
+
* floating-ui exactly as it always was, and with it, `Menu` takes over, because
|
|
10
|
+
* the one thing `FloatingFocusManager` cannot express is returning focus to a
|
|
11
|
+
* trigger that lives OUTSIDE the popup.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MenuFocusScope: ({ enabled, context, initialFocus, returnFocus, children, }: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
context: React.ComponentProps<typeof FloatingFocusManager>["context"];
|
|
16
|
+
initialFocus: React.RefObject<HTMLElement | null>;
|
|
17
|
+
/** Passed through; `Typeahead` opts out, as it did before. */
|
|
18
|
+
returnFocus?: boolean;
|
|
19
|
+
children: React.ReactElement;
|
|
20
|
+
}) => React.JSX.Element;
|
|
21
|
+
//# sourceMappingURL=MenuFocusScope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MenuFocusScope.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/MenuFocusScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAC,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAGxD;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,4DAM5B;IACD,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC,SAAS,CAAC,CAAC;IACtE,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC;CAC9B,sBAYE,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.MenuFocusScope = void 0;
|
|
7
|
+
var React = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _react2 = require("@floating-ui/react");
|
|
9
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
10
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
11
|
+
/**
|
|
12
|
+
* floating-ui's `FloatingFocusManager`, or nothing at all.
|
|
13
|
+
*
|
|
14
|
+
* Mounting it is what manages focus, so there is no `disabled` prop to pass —
|
|
15
|
+
* the choice has to be whether the element exists. Every popup trigger in the
|
|
16
|
+
* dropdown family needs that choice: without `enableA11y` focus is managed by
|
|
17
|
+
* floating-ui exactly as it always was, and with it, `Menu` takes over, because
|
|
18
|
+
* the one thing `FloatingFocusManager` cannot express is returning focus to a
|
|
19
|
+
* trigger that lives OUTSIDE the popup.
|
|
20
|
+
*/
|
|
21
|
+
const MenuFocusScope = _ref => {
|
|
22
|
+
let {
|
|
23
|
+
enabled,
|
|
24
|
+
context,
|
|
25
|
+
initialFocus,
|
|
26
|
+
returnFocus,
|
|
27
|
+
children
|
|
28
|
+
} = _ref;
|
|
29
|
+
return enabled ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_react2.FloatingFocusManager, {
|
|
30
|
+
modal: false,
|
|
31
|
+
context: context,
|
|
32
|
+
returnFocus: returnFocus,
|
|
33
|
+
initialFocus: initialFocus,
|
|
34
|
+
children: children
|
|
35
|
+
}) : children;
|
|
36
|
+
};
|
|
37
|
+
exports.MenuFocusScope = MenuFocusScope;
|
|
@@ -3,8 +3,44 @@ import type { CSSProperties } from 'react';
|
|
|
3
3
|
import type { BaseMenuProps, MenuOption } from '../../components/Menu/Menu';
|
|
4
4
|
export interface MenuOptionProps extends Omit<BaseMenuProps, 'option' | 'isLastItem' | 'style'> {
|
|
5
5
|
option: MenuOption;
|
|
6
|
+
/**
|
|
7
|
+
* Last option in the list. Without `enableA11y` this is how the menu knows
|
|
8
|
+
* where Tab leaves it: the last option carries a keydown listener that calls
|
|
9
|
+
* `onTabOut`. With the opt-in, the keyboard hook owns Tab for the whole menu
|
|
10
|
+
* and this is ignored.
|
|
11
|
+
*/
|
|
6
12
|
isLastItem?: boolean;
|
|
7
13
|
style?: CSSProperties | undefined;
|
|
14
|
+
/** Total option count — set for virtualized lists so AT announces "N of total". */
|
|
15
|
+
setSize?: number;
|
|
16
|
+
/** 1-based position — set for virtualized lists (windowed DOM hides the rest). */
|
|
17
|
+
posInSet?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Make this option the active highlight when the pointer moves over it, so
|
|
20
|
+
* mouse and keyboard share one cursor (and roving focus follows the pointer).
|
|
21
|
+
* `clientX`/`clientY` let Menu drop no-op moves (scroll under a still cursor).
|
|
22
|
+
*/
|
|
23
|
+
onPointerActivate?: (key: string, el: HTMLElement, clientX: number, clientY: number) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Key of the option the POINTER most recently highlighted. Used to keep the
|
|
26
|
+
* focus ring off a highlight the mouse created, while the background tint still
|
|
27
|
+
* follows the pointer.
|
|
28
|
+
*/
|
|
29
|
+
pointerActiveKey?: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Withhold the highlight the menu opened with. Set when the menu was opened by
|
|
32
|
+
* mouse, so it does not appear to have a cursor the user never placed; cleared
|
|
33
|
+
* as soon as they move the active option themselves.
|
|
34
|
+
*/
|
|
35
|
+
suppressInitialHighlight?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether focus is inside the menu. A roving menu's highlight IS its DOM
|
|
38
|
+
* focus, so it must not be drawn once focus has moved elsewhere on the page —
|
|
39
|
+
* the active option is still remembered, it is just no longer shown. The
|
|
40
|
+
* combobox modes ignore this: there the trigger input outside the menu holds
|
|
41
|
+
* focus by design and this highlight is the user's only cursor.
|
|
42
|
+
*/
|
|
43
|
+
menuHasFocus?: boolean;
|
|
8
44
|
testId?: string;
|
|
9
45
|
}
|
|
10
46
|
export declare const MenuOptionButton: (props: MenuOptionProps) => React.JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MenuOptionButton.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/MenuOptionButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAC;AASzC,OAAO,KAAK,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"MenuOptionButton.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/MenuOptionButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,OAAO,CAAC;AASzC,OAAO,KAAK,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,0BAA0B,CAAC;AASxE,MAAM,WAAW,eACf,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;IAC9D,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAClC,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAClB,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,KACZ,IAAI,CAAC;IACV;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAcD,eAAO,MAAM,gBAAgB,GAAI,OAAO,eAAe,sBA+StD,CAAC"}
|
|
@@ -10,6 +10,7 @@ var _qa = require("../../utils/qa");
|
|
|
10
10
|
var _Button = require("../Button");
|
|
11
11
|
var _Checkbox = require("../Checkbox");
|
|
12
12
|
var _Icon = require("../Icon");
|
|
13
|
+
var _menuOptionId = require("./menuOptionId");
|
|
13
14
|
var _RadioButton = require("../RadioButton");
|
|
14
15
|
var _Truncate = require("../Truncate");
|
|
15
16
|
var _TruncatedTextWithTooltip = require("../TruncatedTextWithTooltip");
|
|
@@ -17,6 +18,22 @@ var _MenuModule = _interopRequireDefault(require("./Menu.module.css"));
|
|
|
17
18
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
18
19
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
19
20
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
21
|
+
/**
|
|
22
|
+
* Hides its child from assistive technology, adding no DOM node when it has
|
|
23
|
+
* nothing to hide — so the un-opted markup keeps exactly the shape it had.
|
|
24
|
+
*/
|
|
25
|
+
const Wrap = _ref => {
|
|
26
|
+
let {
|
|
27
|
+
hidden,
|
|
28
|
+
children
|
|
29
|
+
} = _ref;
|
|
30
|
+
return hidden ? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
children: children
|
|
33
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
|
|
34
|
+
children: children
|
|
35
|
+
});
|
|
36
|
+
};
|
|
20
37
|
const MenuOptionButton = props => {
|
|
21
38
|
const lastMenuItemRef = React.useRef(null);
|
|
22
39
|
const {
|
|
@@ -28,13 +45,24 @@ const MenuOptionButton = props => {
|
|
|
28
45
|
classNames,
|
|
29
46
|
optionsVariant = 'normal',
|
|
30
47
|
selectedKeys,
|
|
31
|
-
isLastItem,
|
|
32
|
-
onTabOut,
|
|
33
48
|
resolveLabel,
|
|
34
49
|
resolveSecondaryLabel,
|
|
35
50
|
style,
|
|
36
51
|
showLabelTooltip,
|
|
37
52
|
allowWrap = false,
|
|
53
|
+
enableA11y = false,
|
|
54
|
+
isLastItem,
|
|
55
|
+
onTabOut,
|
|
56
|
+
allowSearch = false,
|
|
57
|
+
menuId,
|
|
58
|
+
activeOptionKey,
|
|
59
|
+
externalKeyboardNav = false,
|
|
60
|
+
setSize,
|
|
61
|
+
posInSet,
|
|
62
|
+
onPointerActivate,
|
|
63
|
+
pointerActiveKey,
|
|
64
|
+
suppressInitialHighlight = false,
|
|
65
|
+
menuHasFocus = false,
|
|
38
66
|
testId
|
|
39
67
|
} = props;
|
|
40
68
|
const {
|
|
@@ -62,24 +90,68 @@ const MenuOptionButton = props => {
|
|
|
62
90
|
}
|
|
63
91
|
return selectedKeys.includes(option.key);
|
|
64
92
|
};
|
|
93
|
+
const isOptionSelected = isSelected() || key === selectedOption?.key;
|
|
94
|
+
const isOptionDisabled = Boolean(menuDisabled || disabled);
|
|
95
|
+
// Roving tabindex: Menu always passes activeOptionKey (string | null), so a
|
|
96
|
+
// single option owns the tab stop. The `undefined` fallback only matters if a
|
|
97
|
+
// MenuOptionButton is rendered outside Menu — there every option stays
|
|
98
|
+
// tabbable (default tabIndex), preserving the pre-roving behavior.
|
|
99
|
+
const isRoving = activeOptionKey !== undefined;
|
|
100
|
+
// `isActive` still drives the roving tab stop and aria-activedescendant — the
|
|
101
|
+
// menu always has an active option so the keyboard has somewhere to start.
|
|
102
|
+
// `showHighlight` is the visible half of it, withheld while the only reason an
|
|
103
|
+
// option is active is that the menu just opened under the mouse.
|
|
104
|
+
const isActive = isRoving && activeOptionKey === key;
|
|
105
|
+
const isVirtualFocusMode = allowSearch || externalKeyboardNav;
|
|
106
|
+
// Drawn only while the menu can actually be driven: under roving focus that
|
|
107
|
+
// means focus is inside it. `externalKeyboardNav` keeps it regardless, since
|
|
108
|
+
// the combobox input that owns focus lives outside this DOM.
|
|
109
|
+
const showHighlight = isActive && !suppressInitialHighlight && (externalKeyboardNav || menuHasFocus);
|
|
110
|
+
// Focus ring. Only the combobox modes need a CLASS for it: there, DOM focus
|
|
111
|
+
// stays on the trigger input and this ring is the only cursor the user has, so
|
|
112
|
+
// it is drawn from `activeOptionKey` — minus any highlight the pointer created,
|
|
113
|
+
// since a focus indicator must not follow the mouse.
|
|
114
|
+
//
|
|
115
|
+
// Roving menus move real DOM focus, so they are left to `:focus-visible` in CSS,
|
|
116
|
+
// which the browser only matches for keyboard-driven focus. That is why merely
|
|
117
|
+
// opening a roving menu with the mouse no longer paints a ring.
|
|
118
|
+
const showActiveRing = showHighlight && isVirtualFocusMode && pointerActiveKey !== key;
|
|
119
|
+
// The option is a real tab stop only under true roving focus. Under search or
|
|
120
|
+
// external combobox nav, focus stays on the input — every option is -1 and the
|
|
121
|
+
// active one is surfaced through the input's aria-activedescendant instead.
|
|
122
|
+
const optionTabIndex = externalKeyboardNav ? -1 : isRoving ? isActive && !allowSearch ? 0 : -1 : undefined;
|
|
123
|
+
const optionId = menuId ? (0, _menuOptionId.menuOptionId)(menuId, key) : undefined;
|
|
65
124
|
React.useEffect(() => {
|
|
66
125
|
setButtonSize(optionSize || size);
|
|
67
126
|
}, [optionSize, size]);
|
|
127
|
+
|
|
128
|
+
// Tab out from the last option. Only without the opt-in: with it, Tab is
|
|
129
|
+
// handled for the whole menu by useMenuKeyboardNavigation, which knows about
|
|
130
|
+
// the header and footer this listener cannot see.
|
|
68
131
|
React.useEffect(() => {
|
|
132
|
+
if (enableA11y) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
69
135
|
const handleKeyDown = event => {
|
|
70
136
|
if (event.key === 'Tab' && !event.shiftKey) {
|
|
71
137
|
// Tab pressed without shift key, calling tab out callback
|
|
72
138
|
onTabOut?.();
|
|
73
139
|
}
|
|
74
140
|
};
|
|
75
|
-
lastMenuItemRef.current
|
|
141
|
+
const node = lastMenuItemRef.current;
|
|
142
|
+
node?.addEventListener('keydown', handleKeyDown);
|
|
76
143
|
return () => {
|
|
77
|
-
|
|
144
|
+
node?.removeEventListener('keydown', handleKeyDown);
|
|
78
145
|
};
|
|
79
|
-
}, [isLastItem]);
|
|
146
|
+
}, [isLastItem, enableA11y]);
|
|
80
147
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_Button.UnstyledButton, {
|
|
81
148
|
className: (0, _classify.classify)(_MenuModule.default.option, {
|
|
82
149
|
[_MenuModule.default.selected]: isSelected() || key === selectedOption?.key,
|
|
150
|
+
// Background highlight for the single active option, from either input
|
|
151
|
+
// source, so pointer and keyboard never show two cursors.
|
|
152
|
+
[_MenuModule.default.active]: enableA11y && showHighlight,
|
|
153
|
+
// Focus ring, keyboard-driven highlights only.
|
|
154
|
+
[_MenuModule.default.activeRing]: enableA11y && showActiveRing,
|
|
83
155
|
[_MenuModule.default.optionSmall]: buttonSize === 'small',
|
|
84
156
|
[_MenuModule.default.optionMedium]: buttonSize === 'medium',
|
|
85
157
|
[_MenuModule.default.disabled]: menuDisabled || disabled,
|
|
@@ -87,36 +159,72 @@ const MenuOptionButton = props => {
|
|
|
87
159
|
[_MenuModule.default.withIconRight]: !!iconRight
|
|
88
160
|
}, classNames?.option, optionClassNames?.wrapper),
|
|
89
161
|
style: style,
|
|
90
|
-
disabled:
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
disabled: isOptionDisabled,
|
|
163
|
+
role: enableA11y ? 'option' : undefined,
|
|
164
|
+
"aria-selected": enableA11y ? isOptionSelected : undefined,
|
|
165
|
+
"aria-disabled": enableA11y && isOptionDisabled || undefined,
|
|
166
|
+
"aria-setsize": enableA11y ? setSize : undefined,
|
|
167
|
+
"aria-posinset": enableA11y ? posInSet : undefined,
|
|
168
|
+
tabIndex: enableA11y ? optionTabIndex : undefined,
|
|
169
|
+
id: enableA11y ? optionId : undefined,
|
|
170
|
+
"data-menu-option-key": enableA11y ? key : undefined,
|
|
171
|
+
onClick: e => onSelect && onSelect(option, e)
|
|
172
|
+
// Without the opt-in the selected option pulls focus to itself on mount,
|
|
173
|
+
// which is the only focus behavior this component had. The opt-in
|
|
174
|
+
// replaces it with Menu's own open-time focus, which can also decline to
|
|
175
|
+
// move focus at all (an inline menu must not steal it).
|
|
176
|
+
,
|
|
177
|
+
autoFocus: !enableA11y && selectedOption?.key === key,
|
|
178
|
+
onMouseMove: e => {
|
|
179
|
+
// Pointer drives the same single active highlight as the keyboard.
|
|
180
|
+
// Guarded so it only fires when crossing into a different option. The
|
|
181
|
+
// cursor coordinates are forwarded so Menu can ignore moves with no real
|
|
182
|
+
// pointer delta (the list scrolling under a stationary cursor during
|
|
183
|
+
// keyboard nav), which would otherwise steal focus from the keyboard.
|
|
184
|
+
if (!enableA11y || isOptionDisabled || isActive) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
onPointerActivate?.(key, e.currentTarget, e.clientX, e.clientY);
|
|
188
|
+
},
|
|
93
189
|
testId: (0, _qa.generateTestId)({
|
|
94
190
|
base: optionTestId || testId,
|
|
95
191
|
slot: 'option-button',
|
|
96
192
|
index: key
|
|
97
193
|
}),
|
|
98
|
-
...(isLastItem ? {
|
|
194
|
+
...(!enableA11y && isLastItem ? {
|
|
99
195
|
ref: lastMenuItemRef
|
|
100
196
|
} : {}),
|
|
101
|
-
children: [optionVariant === 'checkbox' &&
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
197
|
+
children: [optionVariant === 'checkbox' &&
|
|
198
|
+
/*#__PURE__*/
|
|
199
|
+
// The wrapper exists only to hide the control from AT: the option row
|
|
200
|
+
// already reports its own selected state, and a nested checkbox would
|
|
201
|
+
// be announced as a second, separate control. It is an extra DOM node,
|
|
202
|
+
// so it appears only with the opt-in.
|
|
203
|
+
(0, _jsxRuntime.jsx)(Wrap, {
|
|
204
|
+
hidden: enableA11y,
|
|
205
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Checkbox.Checkbox, {
|
|
206
|
+
tabIndex: -1,
|
|
207
|
+
disabled: menuDisabled || disabled,
|
|
208
|
+
checked: isSelected(),
|
|
209
|
+
indeterminate: indeterminate,
|
|
210
|
+
testId: (0, _qa.generateTestId)({
|
|
211
|
+
base: optionTestId || testId,
|
|
212
|
+
slot: 'checkbox',
|
|
213
|
+
index: key
|
|
214
|
+
})
|
|
110
215
|
})
|
|
111
|
-
}), optionVariant === 'radio' && /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
216
|
+
}), optionVariant === 'radio' && /*#__PURE__*/(0, _jsxRuntime.jsx)(Wrap, {
|
|
217
|
+
hidden: enableA11y,
|
|
218
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_RadioButton.RadioButton, {
|
|
219
|
+
disabled: menuDisabled || disabled,
|
|
220
|
+
value: option.key,
|
|
221
|
+
selectedValue: selectedKeys?.[0],
|
|
222
|
+
tabIndex: -1,
|
|
223
|
+
testId: (0, _qa.generateTestId)({
|
|
224
|
+
base: optionTestId || testId,
|
|
225
|
+
slot: 'radio-button',
|
|
226
|
+
index: key
|
|
227
|
+
})
|
|
120
228
|
})
|
|
121
229
|
}), !!iconLeft && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.Icon, {
|
|
122
230
|
name: iconLeft,
|
|
@@ -179,14 +287,14 @@ const MenuOptionButton = props => {
|
|
|
179
287
|
});
|
|
180
288
|
};
|
|
181
289
|
exports.MenuOptionButton = MenuOptionButton;
|
|
182
|
-
const renderLabel =
|
|
290
|
+
const renderLabel = _ref2 => {
|
|
183
291
|
let {
|
|
184
292
|
label,
|
|
185
293
|
allowWrap,
|
|
186
294
|
showLabelTooltip,
|
|
187
295
|
testId,
|
|
188
296
|
key
|
|
189
|
-
} =
|
|
297
|
+
} = _ref2;
|
|
190
298
|
if (showLabelTooltip) {
|
|
191
299
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_TruncatedTextWithTooltip.TruncatedTextWithTooltip, {
|
|
192
300
|
tooltip: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,sCAAsC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,sCAAsC,CAAC;AAKrD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+CAA+C,CAAC"}
|
|
@@ -24,4 +24,26 @@ Object.keys(_MenuOptionButton).forEach(function (key) {
|
|
|
24
24
|
return _MenuOptionButton[key];
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
+
});
|
|
28
|
+
var _useMenuTrigger = require("./useMenuTrigger");
|
|
29
|
+
Object.keys(_useMenuTrigger).forEach(function (key) {
|
|
30
|
+
if (key === "default" || key === "__esModule") return;
|
|
31
|
+
if (key in exports && exports[key] === _useMenuTrigger[key]) return;
|
|
32
|
+
Object.defineProperty(exports, key, {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () {
|
|
35
|
+
return _useMenuTrigger[key];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
var _useMenuKeyboardNavigation = require("./useMenuKeyboardNavigation");
|
|
40
|
+
Object.keys(_useMenuKeyboardNavigation).forEach(function (key) {
|
|
41
|
+
if (key === "default" || key === "__esModule") return;
|
|
42
|
+
if (key in exports && exports[key] === _useMenuKeyboardNavigation[key]) return;
|
|
43
|
+
Object.defineProperty(exports, key, {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
get: function () {
|
|
46
|
+
return _useMenuKeyboardNavigation[key];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
27
49
|
});
|