Place MaterialUI Tooltip "on top" of anchor element? (React) - material-ui

Seems impossible to place <Tooltip> "on top" (stacked/layered above) the triggering anchor element.
It always appears outside the parent, using "placement" to decide where. I believe it's the Flip tool within Popper that manages placement and ensures visibility. I've tried passing Popper option modifiers to disable flip, and adjusting the offset. Some recommendations were to skip preventing overflow, and disable GPU acceleration. I'm down a rabbit-hole of MaterialUI internals to accomplish this. I commented out what seems like unlikely overkill solutions.
Example outside MUI: react-tooltip
(includes pointer tracking, beyond this question).
<Tooltip title="My Label" placement="top" PopperProps={{
popperOptions: {
modifiers: {
flip: { enabled: false },
// computeStyle: {
// gpuAcceleration: false
// },
// preventOverflow: {
// enabled: false,
// padding: 0
// },
offset: {
offset: '-20px -20px'
}
}
}
}}></Tooltip>
<h3>My Text</h3>
</Tooltip>

You need to enable the offset like this:
<Tooltip title="My Label" placement="top" PopperProps={{
popperOptions: {
modifiers: {
flip: { enabled: false },
offset: {
enabled: true,
offset: '0px -60px'
}
}
}
}}>
<h3>My Text</h3>
</Tooltip>
this will enable you to place the tooltip on top

Related

Add "focusVisibleClassName" globally in Material UI

I want to disable the ripple effect since I don't want the effect when clicking on elements. Now this works but when doing this the :focus-visible state will also be disabled which is a bummer. Would be really nice to split this prop up into something like disableClickRipple, disableFocusRipple and so on...
Anyway, according to the docs I have to add focusVisibleClassName in order to style my own focus state but how would I do this to all elements that is affected by the disabled ripple effect? As I understand it I have to do this manually on each and every component? Can this be done globally? To me this would need to be a global setting?
I don't know if this is how MUI intended it to be, but this works:
import { createMuiTheme } from '#material-ui/core/styles';
const theme = createMuiTheme({
props: {
MuiButtonBase: {
// Disable ripple globally.
disableRipple: true,
// The class will be added (as is, ie `focus-visible`)
// To the ButtonBase root whenever the button gets focus via keyboard.
focusVisibleClassName: 'focus-visible',
},
},
overrides: {
MuiButtonBase: {
// And this is how we select it.
root: {
'&.focus-visible': {
backgroundColor: 'rgba(0, 0, 0, 0.12)', // theme.pallate.action.focus
},
},
},
},
});
export default theme;
PS. Spent an hour trying to solve this to no avail. Then I landed your question and "in order to style my own focus state" gave me the idea above.

Ag-Grid Prevent tree expand/collapse when double clicking on cell

I have some code in my grid where I'm trying to prevent a double-click event from causing a tree row to expand/collapse and I can't find documentation how to do this anywhere. The reason I want to do this is so that when I double click the cell, I want to make it editable, but due to some technical necessities, I need to run gridApi.redrawRows() when the collapse/expand happens which causes the edit field to lose focus, meaning I can never actually edit the cell by double clicking.
I'm using the following ag-grid module versions
"ag-grid-angular": "^22.1.1",
"ag-grid-community": "^22.1.1",
"ag-grid-enterprise": "^22.1.1"
And the relevant html in my grid looks like this
<ag-grid-angular
#agGrid
class="ag-theme-balham"
[modules]="modules"
[columnDefs]="columnDefs"
[rowData]="rowData"
[treeData]="true"
(rowGroupOpened)="onRowGroupOpened($event)"
(cellDoubleClicked)="handleCellDoubleClicked($event)"
[getDataPath]="getDataPath"
[defaultColDef]="defaultColDef"
[frameworkComponents]="frameworkComponents"
[autoGroupColumnDef]="autoGroupColumnDef"
(gridReady)="onGridReady($event)"
[getRowNodeId]="getRowNodeId"
>
</ag-grid-angular>
In my component file, the particular column that is responsible for auto grouping the rows in a tree-format looks like this
ngOnInit() {
this.autoGroupColumnDef = {
editable: this.isAllowedtoEdit,
headerName: "Account #",
sortable: true,
lockPosition: true,
resizable: true,
field: "accountNum",
filter: "agGroupCellRenderer",
cellRendererParams: {
suppressCount: true,
innerRenderer: 'AccountNameColumnDisplayer',
},
};
}
And the function for handling the double click on a cell is this:
handleCellDoubleClicked(cell) {
if(cell.column.colDef.field === 'accountNum') {
cell.event.stopPropagation();
return false;
}
}
My handleCellDoubleClicked() function doesn't seem to do anything. It runs but the row still expands/collapses when I double click.
I even tried this just to see what it would do and it was causing some really weird behaviors to happen
handleCellDoubleClicked(cell) {
if(cell.column.colDef.field === 'accountNum') {
cell.node.setExpanded(false)
}
}
And finally, when I try to focus on
handleCellDoubleClicked(cell) {
if(cell.column.colDef.field === 'accountNum') {
setTimeout(() => {
this.gridApi.startEditingCell({
rowIndex: node.rowIndex,
colKey: 'accountNum'
});
}, 125);
}
}
I get a warning in the console telling me that ag-grid does not recognize the column accountNum
So I'm at a bit of a loss right now. Is there something I can do? Thanks in advance!
You should be able to add the following property to your cellRendererParams to achieve this:
cellRendererParams: {
...yourOtherParams,
suppressDoubleClickExpand: true
}
Cheers!

StencilJS component with shadow dom enabled does not generate the helper CSS classes for dynamically added elements on IE11/Edge

I've created a new project using the stencil component starter. Inside my component I'm using an external JS nouislider, which injects HTML elements into my div (this.slider ref):
...
componentDidLoad() {
noUiSlider.create(this.slider, {
start: [20, 80],
range: {
'min': 0,
'max': 100
}
})
}
...
I've copied the slider's CSS into my-component.css and rewrote everything with :host selectors for the shadow dom:
:host(.my-component) .noUi-target {
position: relative;
direction: ltr
}
Everything works fine on Chrome/Firefox but the slider styles are not working on IE11/Edge because Stencil appends a helper sc-my-component class to every element that I have inside the render method and generates CSS rules like so:
.my-component.sc-my-component-h .noUi-target.sc-my-component {
position: relative;
direction: ltr
}
but the injected nouislider child HTML elements don't have the helper classes on them. I have an ugly fix for this case atm:
...
componentDidLoad() {
noUiSlider.create(this.slider, {
start: [20, 80],
range: {
'min': 0,
'max': 100
}
})
this.slider.querySelectorAll('div').forEach((child)=>{
child.classList.add('sc-my-component')
})
}
...
I'm appending the helper classes after the slider is created (the slider generates child divs only). Is there a better way to tell Stencil that I'm injecting elements inside lifecycle methods and that it needs to recognize those elements when CSS rules are being generated?
This is not an answer to your question, nevertheless this could also be interesting for you:
We are currently working on the same topic (StencilJS, shadow: true, noUiSlider) and encountered the problem, that the slider's touch events are not working correctly in shadowDOM on mobile devices. We found a solution for this and already created a PR (https://github.com/leongersen/noUiSlider/pull/1060).
I too had problems using nouislider in StencilJS but just managed to make it work.
my-slider.scss
#import '~nouislider/distribute/nouislider.css';
:host {
padding: 50px 30px;
display: block;
}
my-slider.tsx
import { Component, h, Prop, Event, EventEmitter } from '#stencil/core';
import noUiSlider from "nouislider";
#Component({
tag: 'skim-slider',
styleUrl: 'skim-slider.scss',
shadow: true
})
export class SkimSlider {
#Prop() min!: number;
#Prop() max!: number;
private slider: HTMLElement;
#Event() update: EventEmitter;
componentDidLoad() {
const slider = noUiSlider.create(this.slider, {
start: [this.min, this.max],
tooltips: [true, true],
range: {
'min': this.min,
'max': this.max
}
});
slider.on('change', (value) => this.update.emit(value));
}
render() {
return (
<div ref={e => this.slider = e}></div>
);
}
}
The trick that did it for me was 'display: block'

Is it possible to have own custom Context Menu in ag-Grid-community

Can't find the exact answer.
If i decide to opt-in for vanilla JavaScript (non-Angular & Co) ag-Grid-community edition, can i have easy to add my own custom context menu an other custom extensions?
As i seen their docs, context menu is only enterprise level feature.
I seen some treads that there is some caveats, but i personally did not dig deeper.
In general, how easy is to implement self-built features in ag-Grid-community. Or it is better to write own grid?
We have a custom context menu component in our Angular project with ag-grid community, so it's definitely possible.
How it works:
We define all grid columns in templates. If you want a context menu, you put an empty column into the column set and put a special directive on it. The directive accepts a context menu template, which is passed into a custom cellRendererFramework (a menu trigger button, basically). The directive also configures the column to ensure consistent look across grid instances.
This might be not what you've been looking for if you require for menu to open with right mouse click anywhere in a row, but I suppose it shouldn't be that hard to trigger the menu from a different event (check out ag-grid events, there might something suitable).
The snippets below should be straightforward to adapt for your framework of choice. Given you opted into vanilla JS, you'll have to use regular functions to do the same, something like this:
const grid = withContextMenu(new Grid(element, gridOptions), menuOptions).
Here's an example of how we use it:
<ag-grid-angular>
<ag-grid-column headerName='ID' field='id'></ag-grid-column>
<ag-grid-column [contextMenu]='menu'>
<mat-menu #menu='matMenu'>
<ng-template matMenuContent let-item='data'>
<button mat-menu-item (click)='restoreSnapshot(item.id)'>Restore From Snapshot</button>
<a mat-menu-item [routerLink]='[item.id, "remove"]'>Remove</a>
</ng-template>
</mat-menu>
</ag-grid-column>
</ag-grid-angular>
The directive that applies the menu:
const WIDTH = 42;
export const CONTEXT_MENU_COLID = 'context-menu';
#Directive({
selector: '[agGridContextMenu]'
})
export class AgGridContextMenuDirective implements AfterViewInit {
constructor(private gridComponent: AgGridAngular) {}
#Input()
agGridContextMenu!: ElementRef<MatMenu>;
ngAfterViewInit() {
if (!this.agGridContextMenu) return;
setTimeout(() => {
this.gridComponent.api.setColumnDefs([
...this.gridComponent.columnDefs,
{
colId: CONTEXT_MENU_COLID,
cellRendererFramework: CellRendererContextMenuComponent,
width: WIDTH,
maxWidth: WIDTH,
minWidth: WIDTH,
cellStyle: {padding: 0},
pinned: 'right',
resizable: false,
cellRendererParams: {
suppressHide: true,
contextMenu: {
menu: this.agGridContextMenu
}
}
}
]);
});
}
}
The cell renderer component:
#Component({
selector: 'cell-renderer-context-menu',
template: `
<ng-container *ngIf='params.data && params.colDef.cellRendererParams.contextMenu.menu'>
<button
type='button'
mat-icon-button
[matMenuTriggerFor]='params.colDef.cellRendererParams.contextMenu.menu'
[matMenuTriggerData]='{data: params.data}'
>
<mat-icon svgIcon='fas:ellipsis-v'></mat-icon>
</button>
</ng-container>
`,
styleUrls: ['./cell-renderer-context-menu.component.scss']
})
export class CellRendererContextMenuComponent implements ICellRendererAngularComp {
params!: ICellRendererParams;
agInit(params: ICellRendererParams) {
this.params = params;
}
refresh() {
return false;
}
}
A screenshot:
I followed this blogpost, using community edition ag-grid, and it worked! I was surprised because previously I had the experience that cell renderers didn't allow content outside of the cell boundaries to be shown, but somehow popper/tippy is getting around that (I think it adds itself to the top of the DOM with this section of code appendTo: document.body).
https://blog.ag-grid.com/creating-popups-in-ag-grid/
basically, in my javascript CellRenderer:
class MyCellRenderer{
// https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/
init(e){
this.isOpen = false;
this.container = document.createElement("span");
let menubutton = document.createElement("button");
menubutton.innerHTML="&#x1F80B"; //downward arrow
this.tippyInstance = tippy(menubutton);
this.tippyInstance.disable();
this.container.appendChild(menubutton);
menubutton.addEventListener('click', that.togglePopup.bind(this));
}
getGui() {
return this.container;
}
togglePopup() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.configureTippyInstance();
this.eMenu = this.createMenuComponent();
this.tippyInstance.setContent(this.eMenu);
} else {
this.tippyInstance.unmount();
}
}
configureTippyInstance() {
this.tippyInstance.enable();
this.tippyInstance.show();
this.tippyInstance.setProps({
trigger: 'manual',
placement: 'bottom-start',
arrow: false,
interactive: true,
appendTo: document.body,
hideOnClick: true,
onShow: (instance) => {
tippy.hideAll({ exclude: instance });
},
onClickOutside: (instance, event) => {
this.isOpen = false;
instance.unmount();
},
});
}
createMenuComponent() {
let menu = document.createElement('div');
menu.classList.add('menu-container');
let options = {};
options['Delete Row'] = this.menuItemClickHandler.bind(this);
options['Popup an Alert!'] = function(){alert("hello!");};
options['Popup an Alert 2!'] = this.menuItemClickHandler.bind(this);
for (const [key, value] of Object.entries(options)) {
let item = document.createElement('div');
item.classList.add('menu-item');
item.setAttribute('data-action', key.toLowerCase());
item.classList.add('hover_changes_color');
item.innerText = `${key}`; // string formatting example
item.addEventListener('click', value);
menu.appendChild(item);
}
return menu;
}
menuItemClickHandler(event) {
this.togglePopup();
const action = event.target.dataset.action;
if (action === 'delete row') {
this.params.api.applyTransaction({ remove: [this.params.data] });
}
if (action === 'popup an alert 2!') {
alert("2");
}
}
}
and in styles.css:
.hover_changes_color:hover {
background-color: dimgrey;
cursor: pointer;
}

owlCarousel not calculating height in accordion on page resize

I am using owlCarousel inside an accordion. Every thing is fine until i resize the window.
After resizing if the accordion was already opened it looks perfect but if it was closed during resize the carousel height goes to 0.
I have tried to use .refres() method of owl carousel but it didn't worked.
I found the solution here.
The solution was given by ses & improvised by tofumedia
As ses quoted:
OWL start for the JS Accordion, and need a resize refresh
JS:
$('.owl-carousel').owlCarousel({
margin: 0,
responsiveClass: true,
nav: false,
dots: true,
items: 1,
responsive: {
0: {
items: 1
},
600: {
items: 1
},
1000: {
items: 1
}
}
});
$("#accordion").accordion({
heightStyle: "content",
header: ".accordionHeadline",
collapsible: true,
active: 0,
beforeActivate: function (event, ui) {
window.dispatchEvent(new Event('resize'));
}
});
Or a more reliable solution which supports IE:
$("a[data-toggle='tab']").on('shown', function () {
var evt = document.createEvent('UIEvents');
evt.initUIEvent('resize', true, false,window,0);
window.dispatchEvent(evt);
});