4
4
- @author John Molakvoæ <[email protected] >
5
5
- @author Marco Ambrosini <[email protected] >
6
6
- @author Raimund Schlüßler <[email protected] >
7
+ - @author Grigorii K. Shartsev <[email protected] >
7
8
-
8
9
- @license GNU AGPL version 3 or any later version
9
10
-
@@ -697,8 +698,8 @@ export default {
697
698
</NcActions>
698
699
</p>
699
700
700
- <h2>Popover </h2>
701
- Has any elements, including text input element, or no buttons.
701
+ <h2>Dialog </h2>
702
+ Includes data input elements
702
703
<p>
703
704
<NcActions aria-label="Group management">
704
705
<NcActionInput trailing-button-label="Submit" label="Rename group">
@@ -714,6 +715,19 @@ export default {
714
715
</NcActionButton>
715
716
</NcActions>
716
717
</p>
718
+
719
+ <h2>Toolip</h2>
720
+ Has only text and not interactive elements
721
+ <p>
722
+ <NcActions aria-label="Contact" :inline="1">
723
+ <NcActionLink aria-label="View profile" href="/u/alice" icon="icon-user-white">
724
+ View profile
725
+ </NcActionLink>
726
+ <NcActionText icon="icon-timezone-white">
727
+ Local time: 10:12
728
+ </NcActionText>
729
+ </NcActions>
730
+ </p>
717
731
</div>
718
732
</template>
719
733
793
807
}
794
808
</style>
795
809
```
796
-
797
810
</docs >
798
811
799
812
<script >
@@ -835,7 +848,7 @@ export default {
835
848
* Provide the role for NcAction* components in the NcActions content.
836
849
* @type {import('vue').ComputedRef<boolean>}
837
850
*/
838
- ' NcActions:isSemanticMenu' : computed (() => this .isSemanticMenu ),
851
+ ' NcActions:isSemanticMenu' : computed (() => this .actionsMenuSemanticType === ' menu ' ),
839
852
}
840
853
},
841
854
@@ -992,9 +1005,10 @@ export default {
992
1005
opened: this .open ,
993
1006
focusIndex: 0 ,
994
1007
randomId: ` menu-${ GenRandomId ()} ` ,
995
- isSemanticMenu: false ,
996
- isSemanticNavigation: false ,
997
- isSemanticPopoverLike: false ,
1008
+ /**
1009
+ * @type {'menu'|'navigation'|'dialog'|'tooltip'|''}
1010
+ */
1011
+ actionsMenuSemanticType: ' ' ,
998
1012
}
999
1013
},
1000
1014
@@ -1006,6 +1020,10 @@ export default {
1006
1020
// If it has a name, we use a secondary button
1007
1021
: this .menuName ? ' secondary' : ' tertiary' )
1008
1022
},
1023
+
1024
+ withFocusTrap () {
1025
+ return this .actionsMenuSemanticType === ' dialog'
1026
+ },
1009
1027
},
1010
1028
1011
1029
watch: {
@@ -1020,16 +1038,25 @@ export default {
1020
1038
},
1021
1039
1022
1040
methods: {
1041
+ /**
1042
+ * Get the name of the action component
1043
+ *
1044
+ * @param {import('vue').VNode} action - a vnode with a NcAction* component instance
1045
+ * @return {string} the name of the action component
1046
+ */
1047
+ getActionName (action ) {
1048
+ return action? .componentOptions ? .Ctor ? .extendOptions ? .name ?? action? .componentOptions ? .tag
1049
+ },
1050
+
1023
1051
/**
1024
1052
* Do we have exactly one Action and
1025
1053
* is it allowed as a standalone element?
1026
1054
*
1027
- * @param {Array } action The action to check
1055
+ * @param {import('vue').VNode } action The action to check
1028
1056
* @return {boolean}
1029
1057
*/
1030
1058
isValidSingleAction (action ) {
1031
- const componentName = action? .componentOptions ? .Ctor ? .extendOptions ? .name ?? action? .componentOptions ? .tag
1032
- return [' NcActionButton' , ' NcActionLink' , ' NcActionRouter' ].includes (componentName)
1059
+ return [' NcActionButton' , ' NcActionLink' , ' NcActionRouter' ].includes (this .getActionName (action))
1033
1060
},
1034
1061
1035
1062
/**
@@ -1088,8 +1115,10 @@ export default {
1088
1115
// close everything
1089
1116
this .focusIndex = 0
1090
1117
1091
- // focus back the menu button
1092
- this .$refs .menuButton .$el .focus ()
1118
+ if (returnFocus) {
1119
+ // Focus back the menu button
1120
+ this .$refs .menuButton .$el .focus ()
1121
+ }
1093
1122
},
1094
1123
1095
1124
onOpen (event ) {
@@ -1127,8 +1156,10 @@ export default {
1127
1156
* @param {object} event The keydown event
1128
1157
*/
1129
1158
onKeydown (event ) {
1130
- if (event .key === ' Tab' && ! this .isSemanticPopoverLike ) {
1131
- this .closeMenu (false )
1159
+ if (event .key === ' Tab' && ! this .withFocusTrap ) {
1160
+ // Return focus to restore Tab sequence
1161
+ // So browser will correctly move focus to the next element
1162
+ this .closeMenu (true )
1132
1163
}
1133
1164
1134
1165
if (event .key === ' ArrowUp' ) {
@@ -1222,6 +1253,12 @@ export default {
1222
1253
},
1223
1254
onBlur (event ) {
1224
1255
this .$emit (' blur' , event )
1256
+
1257
+ // When there is no focusable elements to handle Tab press from actions menu
1258
+ // It requries manual closing
1259
+ if (this .actionsMenuSemanticType === ' tooltip' ) {
1260
+ this .closeMenu (false )
1261
+ }
1225
1262
},
1226
1263
onClick (event ) {
1227
1264
/**
@@ -1248,46 +1285,64 @@ export default {
1248
1285
* This also ensure that we don't get 'text' elements, which would
1249
1286
* become problematic later on.
1250
1287
*/
1251
- const actions = (this .$slots .default || []).filter (
1252
- action => action? .componentOptions ? .tag || action? .componentOptions ? .Ctor ? .extendOptions ? .name ,
1253
- )
1254
-
1255
- const getActionName = (action ) => action? .componentOptions ? .Ctor ? .extendOptions ? .name ?? action? .componentOptions ? .tag
1256
-
1257
- const menuItemsActions = [' NcActionButton' , ' NcActionButtonGroup' , ' NcActionCheckbox' , ' NcActionRadio' ]
1258
- const textInputActions = [' NcActionInput' , ' NcActionTextEditable' ]
1259
- const linkActions = [' NcActionLink' , ' NcActionRouter' ]
1288
+ const actions = (this .$slots .default || []).filter (action => this .getActionName (action))
1260
1289
1261
- const hasTextInputAction = actions .some (action => textInputActions .includes (getActionName (action)))
1262
- const hasMenuItemAction = actions .some (action => menuItemsActions .includes (getActionName (action)))
1263
- const hasLinkAction = actions .some (action => linkActions .includes (getActionName (action)))
1264
-
1265
- // We consider the NcActions to have role="menu" if it consists some button-like action and not text inputs
1266
- this .isSemanticMenu = hasMenuItemAction && ! hasTextInputAction
1267
- // We consider the NcActions to be navigation if it consists some link-like action
1268
- this .isSemanticNavigation = hasLinkAction && ! hasMenuItemAction && ! hasTextInputAction
1269
- // If it is not a menu and not a navigation, it is a popover with items: a form or just a text
1270
- this .isSemanticPopoverLike = ! this .isSemanticMenu && ! this .isSemanticNavigation
1290
+ // Check that we have at least one action
1291
+ if (actions .length === 0 ) {
1292
+ return
1293
+ }
1271
1294
1272
- const popupRole = this .isSemanticMenu
1273
- ? ' menu'
1274
- : hasTextInputAction
1275
- ? ' dialog'
1276
- : ' true'
1295
+ /**
1296
+ * Separate the actions into inline and menu actions
1297
+ */
1277
1298
1278
1299
/**
1279
- * Filter and list actions that are allowed to be displayed inline
1300
+ * @type {import('vue').VNode[]}
1280
1301
*/
1281
- let inlineActions = actions .filter (this .isValidSingleAction )
1282
- if (this .forceMenu && inlineActions .length > 0 && this .inline > 0 ) {
1302
+ let validInlineActions = actions .filter (this .isValidSingleAction )
1303
+ if (this .forceMenu && validInlineActions .length > 0 && this .inline > 0 ) {
1283
1304
Vue .util .warn (' Specifying forceMenu will ignore any inline actions rendering.' )
1284
- inlineActions = []
1305
+ validInlineActions = []
1285
1306
}
1307
+ /**
1308
+ * @type {import('vue').VNode[]}
1309
+ */
1310
+ const inlineActions = validInlineActions .slice (0 , this .inline )
1311
+ /**
1312
+ * @type {import('vue').VNode[]}
1313
+ */
1314
+ const menuActions = actions .filter (action => ! inlineActions .includes (action))
1286
1315
1287
- // Check that we have at least one action
1288
- if (actions .length === 0 ) {
1289
- return
1316
+ /**
1317
+ * Determine what kind of menu we have.
1318
+ * It defines focus behavior and a11y.
1319
+ */
1320
+
1321
+ const menuItemsActions = [' NcActionButton' , ' NcActionButtonGroup' , ' NcActionCheckbox' , ' NcActionRadio' ]
1322
+ const textInputActions = [' NcActionInput' , ' NcActionTextEditable' ]
1323
+ const linkActions = [' NcActionLink' , ' NcActionRouter' ]
1324
+
1325
+ const hasTextInputAction = menuActions .some (action => textInputActions .includes (this .getActionName (action)))
1326
+ const hasMenuItemAction = menuActions .some (action => menuItemsActions .includes (this .getActionName (action)))
1327
+ const hasLinkAction = menuActions .some (action => linkActions .includes (this .getActionName (action)))
1328
+
1329
+ if (hasTextInputAction) {
1330
+ this .actionsMenuSemanticType = ' dialog'
1331
+ } else if (hasMenuItemAction) {
1332
+ this .actionsMenuSemanticType = ' menu'
1333
+ } else if (hasLinkAction) {
1334
+ this .actionsMenuSemanticType = ' navigation'
1335
+ } else {
1336
+ this .actionsMenuSemanticType = ' tooltip'
1337
+ }
1338
+
1339
+ const actionsRoleToHtmlPopupRole = {
1340
+ dialog: ' dialog' ,
1341
+ menu: ' menu' ,
1342
+ navigation: ' true' ,
1343
+ tooltip: ' true' ,
1290
1344
}
1345
+ const popupRole = actionsRoleToHtmlPopupRole[this .actionsMenuSemanticType ]
1291
1346
1292
1347
/**
1293
1348
* Render the provided action
@@ -1393,10 +1448,8 @@ export default {
1393
1448
container: this .container ,
1394
1449
popoverBaseClass: ' action-item__popper' ,
1395
1450
popupRole,
1396
- // Menu and navigation should not have focus trap
1397
- // Tab should close the menu and move focus to the next UI element
1398
- setReturnFocus: ! this .isSemanticPopoverLike ? null : this .$refs .menuButton ? .$el ,
1399
- focusTrap: this .isSemanticPopoverLike ,
1451
+ setReturnFocus: this .withFocusTrap ? this .$refs .menuButton ? .$el : null ,
1452
+ focusTrap: this .withFocusTrap ,
1400
1453
},
1401
1454
// For some reason the popover component
1402
1455
// does not react to props given under the 'props' key,
@@ -1470,8 +1523,8 @@ export default {
1470
1523
* If we have a single action only and didn't force a menu,
1471
1524
* we render the action as a standalone button
1472
1525
*/
1473
- if (actions .length === 1 && inlineActions .length === 1 && ! this .forceMenu ) {
1474
- return renderInlineAction (inlineActions [0 ])
1526
+ if (actions .length === 1 && validInlineActions .length === 1 && ! this .forceMenu ) {
1527
+ return renderInlineAction (actions [0 ])
1475
1528
}
1476
1529
1477
1530
// If we completely re-render the children
@@ -1490,9 +1543,6 @@ export default {
1490
1543
* If we some inline actions to render, render them, then the menu
1491
1544
*/
1492
1545
if (inlineActions .length > 0 && this .inline > 0 ) {
1493
- const renderedInlineActions = inlineActions .slice (0 , this .inline )
1494
- // Filter already rendered actions
1495
- const menuActions = actions .filter (action => ! renderedInlineActions .includes (action))
1496
1546
return h (' div' ,
1497
1547
{
1498
1548
class: [
@@ -1502,7 +1552,7 @@ export default {
1502
1552
},
1503
1553
[
1504
1554
// Render inline actions
1505
- ... renderedInlineActions .map (renderInlineAction),
1555
+ ... inlineActions .map (renderInlineAction),
1506
1556
// render the rest within the popover menu
1507
1557
menuActions .length > 0
1508
1558
? h (' div' ,
0 commit comments