react-bootstrap-typeahead How do I trigger a callback (update state) on `Enter` but not when the user is selecting hint? - react-bootstrap-typeahead

I'm trying to make an input that will allow user to select multiple items. When finished selecting, he can press Enter to push his selections into an array in the state. The input will then clear and be ready for his next set of selections. (Imagine populating a 2-d array quickly). However, with the code below, if the user presses Enter to select the hint, my callback, pushToState, will trigger as it's in onKeyDown. What's the correct way to implement this behavior?
<Typeahead
id="basic-typeahead-multiple"
labelKey="name"
multiple
onChange={setMultiSelections}
options={options}
selected={multiSelections}
onKeyDown={(event) => {
if (event.key === 'Enter') {
pushToState(multiSelections);
setMultiSelections([]);
}
}}
/>

Your use case is similar to the one in this question. You basically need to determine whether the user is selecting a menu item or not. You can do that by checking the activeIndex of the typeahead:
// Track the index of the highlighted menu item.
const [activeIndex, setActiveIndex] = useState(-1);
const onKeyDown = useCallback(
(e) => {
// Check whether the 'enter' key was pressed, and also make sure that
// no menu items are highlighted.
if (event.key === 'Enter' && activeIndex === -1) {
pushToState(multiSelections);
setMultiSelections([]);
}
},
[activeIndex]
);
return (
<Typeahead
id="basic-typeahead-multiple"
labelKey="name"
multiple
onChange={setMultiSelections}
options={options}
onChange={setMultiSelections}
onKeyDown={onKeyDown}
selected={multiSelections} >
{(state) => {
// Passing a child render function to the component exposes partial
// internal state, including the index of the highlighted menu item.
setActiveIndex(state.activeIndex);
}}
</Typeahead>
);

Related

React Query - How to find out which mutation led to refetching of query

In my simple ToDo app i am using useQuery() to fetch ToDo's from the server as well as useMutation() to create, update and delete ToDo's. When a mutation is successful, i invalidate the query so it gets refetched.
If i press the delete button of a ToDo item, i want the corresponding button to show a loading spinner until the mutation is done and the new ToDo's have been fetched. For that purpose i am using the useIsFetching() hook in my components, which works fine. However, here is the problem:
If i now execute a mutation, every button (meaning the "Delete" button as well as the "Submit" and "Save changes" button to post or update a ToDo) will show the loading spinner instead of just the one that i pressed. This makes sense since they all depend on the same value of useIsFetching(). I need a way to figure out which mutation led to the refetching of the query so i can conditionally render the loading spinner for the appropriate button. This seems to be a very common problem for me yet i cannot find a (not overcomplicated) solution for it. Is there something i'm missing?
The solution Ahmed Sbai said above is good (you can use state instead of local variables), and here is another approach for you.
You can check condition based on isLoading in the object returned from useMutation().
Updated: As written in this TkDodo's blog, the "magic" is here:
If you want your mutation to stay in loading state while your related queries update, you have to return the result of invalidateQueries() from the callback.
Therefore, you won't need to use the useIsFetching() hook, too.
function App() {
const addMutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ['todos'],
})
}
})
const updateMutation = useMutation({
mutationFn: (id, data) => {
return axios.patch(`/todos/${id}`, data)
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ['todos'],
})
}
})
const deleteMutation = useMutation({
mutationFn: (id) => {
return axios.delete(`/todos/${id}`)
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ['todos'],
})
}
})
return (
<div>
{/* ... */}
<button
onClick={() => addMutation.mutate(...)}
loading={addMutation.isLoading}
>
Submit
</button>
<button
onClick={() => updateMutation.mutate(...)}
loading={updateMutation.isLoading}
>
Save changes
</button>
<button
onClick={() => deleteMutation.mutate(...)}
loading={deleteMutation.isLoading}
>
Delete
</button>
</div>
)
}
If you want any further information, please read more in Docs.
You can simply create a variable var loadingType = 0 and update its value each time the user click on a button for example if the delete button is clicked then loadingType = 1 if update button loadingType = 2, etc. Then based on the value of loadingType you know which loading spinner you have to use.

React-Bootstrap-Typeahead: Capture moment before setting selection into box (to allow conditional rejection)

In React-Boostrap-Typeahead, I need to capture the moment after the mouse has been clicked, but before the menu selection gets loaded into the Typeahead's box.
This is because I need to check items being selected, and for some of them, I need to display an alert with a Yes/No warning. Only if Yes is clicked can I proceed with setting that value into the box. Otherwise I need to reject the selection and keep it whatever it was prior to the mouse click.
I can't use onChange because by that point the selection is already in the box.
I can't use onInputChange because that is for typing rather than for menu selection. I need the post-menu-select, pre-box change.
If there are any workarounds please let me know.
You should be able to achieve what you're after using onChange:
const ref = useRef();
const [selected, setSelected] = useState([]);
return (
<Typeahead
id="example"
onChange={(selections) => {
if (!selections.length || window.confirm('Are you sure?')) {
return setSelected(selections);
}
ref.current.clear();
}}
options={options}
ref={ref}
selected={selected}
/>
);
Working example: https://codesandbox.io/s/objective-colden-w7ko9

Enter on cypress after type is not input the value

I just did a cypress curse on Udemy and I am trying to learn a bit more.
I am using this simple code line to get a box and enter a value inside using type and enter, however I am getting no error and the value is not showing inside the box.
here is the code line cy.get('[class="multiselect__tags"]').eq('10').type('2,000,000{enter}')
how I am new I am using index to get the box since in the page there is more than 18 boxes with the attributes.
cy.get('[class="multiselect__tags"]').eq('10').type('2,000,000{enter}')
cy.get('[class="multiselect__tags"]').eq('11').type('2,000,000{enter}')
cy.get('[class="multiselect__tags"]').eq('12').type('1,500{enter}')
Here is the DOM
here is how it shows in the test
The Vue-multiselect control allows the user to select items from a list, or type a value and press enter to select it.
The result is a "tag" for each of the selected items is added to the input box.
To select by typing, this is how it can be done on the demo page
it('selects from vue-multiselect by typing', () => {
cy.viewport(1000,1000)
cy.visit('https://vue-multiselect.js.org/')
cy.get('div.multiselect').eq(0) // there is no id to work with so just get 1st
.type('NO Dependencies{enter}'); // select an item
cy.get('div.multiselect').eq(0) // same parent as above
.within(() => { // now work within that select
cy.get('span.multiselect__tag') // check the tags
.should('have.length', 1) // only one selected
.invoke('text')
.should('contain', 'NO Dependencies') // has the expected text
})
cy.get('div.multiselect').eq(0) // same select
.type('GitHub Stars{enter}'); // select another item
cy.get('div.multiselect').eq(0)
.within(() => {
cy.get('span.multiselect__tag') // check the tags
.should('have.length', 2) // now two selected
.invoke('text')
.should('contain', 'GitHub Stars') // has the expected text
})
})
To select by clicking the dropdown list
it('selects from vue-multiselect by clicking', () => {
cy.viewport(1000,1000)
cy.visit('https://vue-multiselect.js.org/')
cy.get('div.multiselect').eq(0)
.within(() => {
cy.get('div.multiselect__select').click() // open the dropdown
cy.get('li.multiselect__element')
.contains('NO Dependencies')
.click() // select an item
})
cy.get('div.multiselect').eq(0) // same parent as above
.within(() => { // now work within that select
cy.get('span.multiselect__tag') // check the tags
.should('have.length', 1) // only one selected
.invoke('text')
.should('contain', 'NO Dependencies') // has the expected text
})
})
In your web page, the multiselect has a data-vv-name attribute which should pinpoint the particular control we want,
const controlSelector = 'div.multiselect[data-vv-name="data.eoConditions.liability"]';
cy.get(controlSelector)
.type('2,000,000{enter}'); // select an item
cy.get(controlSelector) // same parent as above
.within(() => { // work within that control
cy.get('span.multiselect__tag') // check the tags
.should('have.length', 1) // only one selected
.invoke('text')
.should('contain', '2,000,000') // has the expected text
})
})
I'm not sure if you can select two values on this particular control, it does not make sense to do so since there could be only one liability limit.

ag-grid column menu reset columns event?

Does ag-grid have a grid event that corresponds to to the "Reset Columns" item that is at the bottom of each column menu?
I need to do some special processing on "Reset Columns", and different handling of column "move", "resize", (etc.). I setup an event handler for the "columnEverythingChanged" event and a different event handler for "columnMoved" (etc.). I found that:
1) When no changes have been made to any column and I press "Reset Columns", "columnEverythingChanged" gets called. Fine.
2) When one or more columns have been changed and I press "Reset Columns", both "columnEverythingChanged" AND "columnMoved" (or other) get called.
My problem: in case (2), my "columnMoved" logic should not run.
A secondary problem: "columnEverythingChanged" also gets called at application startup. Not a big deal, but I had to hack around it.
This was so long ago, I don't remember the details. But in my code, I see that I use the columnMoved, columnResized, columnVisible, filterChanged, and sortChanged events. I run the same function on all of these events. That function debounces the event before doing my special processing.
you can use GlobalListener to work with multiple events without issue
gridOption.api.addGlobalListener(GlobalListenerSaveColumnState);
function GlobalListenerSaveColumnState(type, event) {
if (type == "columnVisible" || type == "columnResized"|| type == "columnMoved"|| type == "columnPinned" || type == "dragStopped") {
UpdateGridDefaultsIntoDb(event);
}
}
I solved this by 'overriding' the default getMainMenuItems function, which basically creates that "hamburger" icon, and reveals the options inside the Main Menu. When overriden, you can supply the list of items you want to include, and you can give it your own function you want to run when "Reset Columns" is clicked.
this.gridOptions = {
viewportDatasource: {
init: this.init,
setViewportRange: this.setViewportRange,
},
rowModelType: 'viewport',
sideBar: localStorage.getItem('test_pivot') == 'true' ? 'columns' : '',
suppressRowClickSelection: false,
suppressMultiSort: true,
rowMultiSelectWithClick: true,
rowSelection: 'multiple',
getMainMenuItems: (params) => this.getMainMenuItems(params), // <- This piece right here
getContextMenuItems: this.getContextMenuItems,
};
Later you define that getMainMenuItems()
getMainMenuItems(params) {
params.defaultItems[params.defaultItems.length - 1] = {
name: this.translate.instant('i18n.reset_columns'),
action: () => {
this.onResetColumnStateClick(null); // <- The custom functionality you want to execute when Reset Columns is clicked
},
};
return params.defaultItems;
}
I believe their documentation has another example containing the getMainMenuItems function
https://ag-grid.com/javascript-data-grid/column-menu/
https://plnkr.co/edit/?open=main.js&preview

How to keep focus within modal dialog?

I'm developing an app with Angular and Semantic-UI. The app should be accessible, this means it should be compliant with WCAG 2.0.
To reach this purpose the modals should keep focus within the dialog and prevents users from going outside or move with "tabs" between elements of the page that lays under the modal.
I have found some working examples, like the following:
JQuery dialog: https://jqueryui.com/dialog/#modal-confirmation
dialog HTML 5.1 element: https://demo.agektmr.com/dialog
ARIA modal dialog example:
http://w3c.github.io/aria-practices/examples/dialog-modal/dialog.html
(that I have reproduced on Plunker)
Here is my try to create an accessible modal with Semantic-UI: https://plnkr.co/edit/HjhkZg
As you can see I used the following attributes:
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
But they don't solve my issue. Do you know any way to make my modal keeping focus and lose it only when user click on cancel/confirm buttons?
There is currently no easy way to achieve this. The inert attribute was proposed to try to solve this problem by making any element with the attribute and all of it's children inaccessible. However, adoption has been slow and only recently did it land in Chrome Canary behind a flag.
Another proposed solution is making a native API that would keep track of the modal stack, essentially making everything not currently the top of the stack inert. I'm not sure the status of the proposal, but it doesn't look like it will be implemented any time soon.
So where does that leave us?
Unfortunately without a good solution. One solution that is popular is to create a query selector of all known focusable elements and then trap focus to the modal by adding a keydown event to the last and first elements in the modal. However, with the rise of web components and shadow DOM, this solution can no longer find all focusable elements.
If you always control all the elements within the dialog (and you're not creating a generic dialog library), then probably the easiest way to go is to add an event listener for keydown on the first and last focusable elements, check if tab or shift tab was used, and then focus the first or last element to trap focus.
If you're creating a generic dialog library, the only thing I have found that works reasonably well is to either use the inert polyfill or make everything outside of the modal have a tabindex=-1.
var nonModalNodes;
function openDialog() {
var modalNodes = Array.from( document.querySelectorAll('dialog *') );
// by only finding elements that do not have tabindex="-1" we ensure we don't
// corrupt the previous state of the element if a modal was already open
nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (!modalNodes.includes(node)) {
// save the previous tabindex state so we can restore it on close
node._prevTabindex = node.getAttribute('tabindex');
node.setAttribute('tabindex', -1);
// tabindex=-1 does not prevent the mouse from focusing the node (which
// would show a focus outline around the element). prevent this by disabling
// outline styles while the modal is open
// #see https://www.sitepoint.com/when-do-elements-take-the-focus/
node.style.outline = 'none';
}
}
}
function closeDialog() {
// close the modal and restore tabindex
if (this.type === 'modal') {
document.body.style.overflow = null;
// restore or remove tabindex from nodes
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (node._prevTabindex) {
node.setAttribute('tabindex', node._prevTabindex);
node._prevTabindex = null;
}
else {
node.removeAttribute('tabindex');
}
node.style.outline = null;
}
}
}
The different "working examples" do not work as expected with a screenreader.
They do not trap the screenreader visual focus inside the modal.
For this to work, you have to :
Set the aria-hidden attribute on any other nodes
disable keyboard focusable elements inside those trees (links using tabindex=-1, controls using disabled, ...)
The jQuery :focusable pseudo selector can be useful to find focusable elements.
add a transparent layer over the page to disable mouse selection.
or you can use the css pointer-events: none property when the browser handles it with non SVG elements, not in IE
This focus-trap plugin is excellent at making sure that focus stays trapped inside of dialogue elements.
It sounds like your problem can be broken down into 2 categories:
focus on dialog box
Add a tabindex of -1 to the main container which is the DOM element that has role="dialog". Set the focus to the container.
wrapping the tab key
I found no other way of doing this except by getting the tabbable elements within the dialog box and listening it on keydown. When I know the element in focus (document.activeElement) is the last one on the list, I make it wrap
"focus" events can be intercepted in the capture phase, so you can listen for them at the document.body level, squelch them before they reach the target element, and redirect focus back to a control in your modal dialog. This example assumes a modal dialog with an input element gets displayed and assigned to the variable currDialog:
document.body.addEventListener("focus", (event) => {
if (currDialog && !currDialog.contains(event.target)) {
event.preventDefault();
event.stopPropagation();
currDialog.querySelector("input").focus();
}
}, {capture: true});
You may also want to contain such a dialog in a fixed-position, clear (or low-opacity) backdrop element that takes up the full screen in order to capture and suppress mouse/pointer events, so that no browser feedback (hover, etc.) occurs that could give the user the impression that the background is active.
Don't use any solution requiring you to look up "tabbable" elements. Instead, use keydown and either click events or a backdrop in an effective manor.
(Angular1)
See Asheesh Kumar's answer at https://stackoverflow.com/a/31292097/1754995 for something similar to what I am going for below.
(Angular2-x, I haven't done Angular1 in a while)
Say you have 3 components: BackdropComponent, ModalComponent (has an input), and AppComponent (has an input, the BackdropComponent, and the ModalComponent). You display BackdropComponent and ModalComponent with the correct z-index, both are currently displayed/visible.
What you need to do is have a general window.keydown event with preventDefault() to stop all tabbing when the backdrop/modal component is displayed. I recommend you put that on a BackdropComponent. Then you need a keydown.tab event with stopPropagation() to handle tabbing for the ModalComponent. Both the window.keydown and keydown.tab could probably be in the ModalComponent but there is purpose in a BackdropComponent further than just modals.
This should prevent clicking and tabbing to the AppComponent input and only click or tab to the ModalComponent input [and browser stuffs] when the modal is shown.
If you don't want to use a backdrop to prevent clicking, you can use use click events similarly to the keydown events described above.
Backdrop Component:
#Component({
selector: 'my-backdrop',
host: {
'tabindex': '-1',
'(window:keydown)': 'preventTabbing($event)'
},
...
})
export class BackdropComponent {
...
private preventTabbing(event: KeyboardEvent) {
if (event.keyCode === 9) { // && backdrop shown?
event.preventDefault();
}
}
...
}
Modal Component:
#Component({
selector: 'my-modal',
host: {
'tabindex': '-1',
'(keydown.tab)': 'onTab($event)'
},
...
})
export class ModalComponent {
...
private onTab(event: KeyboardEvent) {
event.stopPropagation();
}
...
}
Here's my solution. It traps Tab or Shift+Tab as necessary on first/last element of modal dialog (in my case found with role="dialog"). Eligible elements being checked are all visible input controls whose HTML may be input,select,textarea,button.
$(document).on('keydown', function(e) {
var target = e.target;
var shiftPressed = e.shiftKey;
// If TAB key pressed
if (e.keyCode == 9) {
// If inside a Modal dialog (determined by attribute role="dialog")
if ($(target).parents('[role=dialog]').length) {
// Find first or last input element in the dialog parent (depending on whether Shift was pressed).
// Input elements must be visible, and can be Input/Select/Button/Textarea.
var borderElem = shiftPressed ?
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first()
:
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
if ($(borderElem).length) {
if ($(target).is($(borderElem))) {
return false;
} else {
return true;
}
}
}
}
return true;
});
we can use the focus trap npm package.
npm i focus-trap
This might help someone who is looking for solution in Angular.
Step 1: Add keydown event on dialog component
#HostListener('document:keydown', ['$event'])
handleTabKeyWInModel(event: any) {
this.sharedService.handleTabKeyWInModel(event, '#modal_id', this.elementRef.nativeElement, 'input,button,select,textarea,a,[tabindex]:not([tabindex="-1"])');
}
This will filters the elements which are preseneted in the Modal dialog.
Step 2: Add common method to handle focus in shared service (or you can add it in your component as well)
handleTabKeyWInModel(e, modelId: string, nativeElement, tagsList: string) {
if (e.keyCode === 9) {
const focusable = nativeElement.querySelector(modelId).querySelectorAll(tagsList);
if (focusable.length) {
const first = focusable[0];
const last = focusable[focusable.length - 1];
const shift = e.shiftKey;
if (shift) {
if (e.target === first) { // shift-tab pressed on first input in dialog
last.focus();
e.preventDefault();
}
} else {
if (e.target === last) { // tab pressed on last input in dialog
first.focus();
e.preventDefault();
}
}
}
}
}
Now this method will take the modal dialog native element and start evaluate on every tab key. Finally we will filter the event on first and last so that we can focus on appropriate elements (on first after last element tab click and on last shift+tab event on first element).
Happy Coding.. :)
I used one of the methods suggested by Steven Lambert, namely, listening to keydown events and intercepting "tab" and "shift+tab" keys. Here's my sample code (Angular 5):
import { Directive, ElementRef, Attribute, HostListener, OnInit } from '#angular/core';
/**
* This directive allows to override default tab order for page controls.
* Particularly useful for working around the modal dialog TAB issue
* (when tab key allows to move focus outside of dialog).
*
* Usage: add "custom-taborder" and "tab-next='next_control'"/"tab-prev='prev_control'" attributes
* to the first and last controls of the dialog.
*
* For example, the first control is <input type="text" name="ctlName">
* and the last one is <button type="submit" name="btnOk">
*
* You should modify the above declarations as follows:
* <input type="text" name="ctlName" custom-taborder tab-prev="btnOk">
* <button type="submit" name="btnOk" custom-taborder tab-next="ctlName">
*/
#Directive({
selector: '[custom-taborder]'
})
export class CustomTabOrderDirective {
private elem: HTMLInputElement;
private nextElemName: string;
private prevElemName: string;
private nextElem: HTMLElement;
private prevElem: HTMLElement;
constructor(
private elemRef: ElementRef
, #Attribute('tab-next') public tabNext: string
, #Attribute('tab-prev') public tabPrev: string
) {
this.elem = this.elemRef.nativeElement;
this.nextElemName = tabNext;
this.prevElemName = tabPrev;
}
ngOnInit() {
if (this.nextElemName) {
var elems = document.getElementsByName(this.nextElemName);
if (elems && elems.length && elems.length > 0)
this.nextElem = elems[0];
}
if (this.prevElemName) {
var elems = document.getElementsByName(this.prevElemName);
if (elems && elems.length && elems.length > 0)
this.prevElem = elems[0];
}
}
#HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (event.key !== "Tab")
return;
if (!event.shiftKey && this.nextElem) {
this.nextElem.focus();
event.preventDefault();
}
if (event.shiftKey && this.prevElem) {
this.prevElem.focus();
event.preventDefault();
}
}
}
To use this directive, just import it to your module and add to Declarations section.
I've been successful using Angular Material's A11yModule.
Using your favorite package manager install these to packages into your Angular app.
**"#angular/material": "^10.1.2"**
**"#angular/cdk": "^10.1.2"**
In your Angular module where you import the Angular Material modules add this:
**import {A11yModule} from '#angular/cdk/a11y';**
In your component HTML apply the cdkTrapFocus directive to any parent element, example: div, form, etc.
Run the app, tabbing will now be contained within the decorated parent element.
For jquery users:
Assign role="dialog" to your modal
Find first and last interactive element inside the dialog modal.
Check if current target is one of them(depending on shift key is
pressed or not).
If target element is one of first or last interactive element of the
dialog, return false
Working code sample:
//on keydown inside dialog
$('.modal[role=dialog]').on('keydown', e => {
let target = e.target;
let shiftPressed = e.shiftKey;
// If TAB is pressed
if (e.keyCode === 9) {
// Find first and last element in the ,modal-dialog parent.
// Elements must be interactive i.e. visible, and can be Input/Select/Button/Textarea.
let first = $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first();
let last = $(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
let borderElem = shiftPressed ? first : last //border element on the basis of shift key pressed
if ($(borderElem).length) {
return !$(target).is($(borderElem)); //if target is border element , return false
}
}
return true;
});
I read through most of the answers, while the package focus-trap seems like a good option. #BenVida shared a very simple VanillaJS solution here in another Stack Overflow post.
Here is the code:
const container=document.querySelector("_selector_for_the_container_")
//optional: needed only if the container element is not focusable already
container.setAttribute("tabindex","0")
container.addEventListener("focusout", (ev)=>{
if (!container.contains(ev.relatedTarget)) container.focus()
})