Skip to content

Commit

Permalink
feat: add Dark Mode grid option (#305)
Browse files Browse the repository at this point in the history
* feat: add Dark Mode grid option
  • Loading branch information
ghiscoding committed Mar 5, 2024
1 parent 753da07 commit d4bfdd1
Show file tree
Hide file tree
Showing 12 changed files with 491 additions and 145 deletions.
1 change: 1 addition & 0 deletions docs/TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

## Styling

* [Dark Mode](styling/dark-mode.md)
* [Styling CSS/SASS/Themes](styling/styling.md)
* [SVG Icons](styling/svg-icons.md)

Expand Down
65 changes: 65 additions & 0 deletions docs/styling/dark-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
## Dark Mode

When enabled (defaults to false), it will show the grid in Dark Mode by adding `slick-dark-mode` CSS class to the grid. Note that it is defined as a grid option because the grid uses a few elements that could be appended to the DOM `body` (e.g. ColumnPicker, GridMenu, LongTextEditor, CompositeEditorModal, ...) and when Dark Mode is enabled, it needs to advise all of these features that we are using Dark Mode (or Light Mode by default). So whenever any of these features are in play, and before it is appended to the `body`, it will add a `slick-dark-mode` (or `ms-dark-mode` for ms-select) CSS class to that element to let it know that we are in Dark Mode.


### Toggle Light/Dark Mode

You can easily toggle light/dark mode by using `grid.setOptions()`

```ts
export class MyDemo {
isDarkModeEnabled = false;
gridOptions: GridOption;

prepareGrid() {
this.gridOptions = {
// ...
darkMode: this.isDarkModeEnabled;
}
}

toggleDarkMode() {
this.isDarkModeEnabled = !this.isDarkModeEnabled;
this.sgb.slickGrid?.setOptions({ darkMode: this.isDarkModeEnabled });

// optionally update your local grid options as well
this.gridOptions = { ...this.gridOptions, darkMode: this.isDarkModeEnabled };
}
}
```

### How to Auto-Detect Dark Mode?

By default the grid will **not** automatically enable Dark Mode, neither read the browser's color scheme (the reason are mentioned in the description above). However, you could implement your own code to detect the color scheme (for modern browser only) when loading your browser and set it in your grid options. You can see a demo of that in the first grid of [Example 1](https://ghiscoding.github.io/slickgrid-universal/#/example01)

```ts
export class MyDemo {
gridOptions: GridOption;

// auto-detect browser's color scheme function
isBrowserDarkModeEnabled() {
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
}

prepareGrid() {
this.gridOptions = {
// ...
darkMode: this.isBrowserDarkModeEnabled();
}
}
}
```

### Composite Editor Modal (for Bootstrap users)

For `Bootstrap` users, it will also require the developer to add a `data-bs-theme="dark"` attribute which is also another reason why we added `darkMode` as a grid option. So for Bootstrap users, you will have to add this required attribute by yourself for the Dark Mode to display properly. If you forget to add this attribute, you might see some of the filter inputs and other sections displayed with a white background instead of an expected dark gray backgroud.

> **Note** the `onRendered` is a new lifecycle callback of Composite Editor Modal that was added specifically for this Bootstrap use case
```ts
this.compositeEditorInstance?.openDetails({
// ...
onRendered: (modalElm) => modalElm.dataset.bsTheme = 'dark',
});
```
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
],
"scripts": {
"build:demo": "webpack --env production",
"dev": "webpack serve --env development",
"dev": "webpack serve --env development",
"delete:dist": "rimraf dist",
"lint": "eslint src/slickgrid-react --ext .ts",
"build:cjs": "tsc --project src/slickgrid-react/tsconfig.build.json --outDir dist/cjs --module commonjs --declaration false",
Expand Down Expand Up @@ -96,11 +96,11 @@
"/src/slickgrid-react"
],
"dependencies": {
"@slickgrid-universal/common": "~4.4.1",
"@slickgrid-universal/custom-footer-component": "~4.4.1",
"@slickgrid-universal/empty-warning-component": "~4.4.1",
"@slickgrid-universal/event-pub-sub": "~4.4.1",
"@slickgrid-universal/pagination-component": "~4.4.1",
"@slickgrid-universal/common": "~4.5.0",
"@slickgrid-universal/custom-footer-component": "~4.5.0",
"@slickgrid-universal/empty-warning-component": "~4.5.0",
"@slickgrid-universal/event-pub-sub": "~4.5.0",
"@slickgrid-universal/pagination-component": "~4.5.0",
"dequal": "^2.0.3",
"dompurify": "^3.0.9",
"font-awesome": "^4.7.0",
Expand All @@ -115,13 +115,13 @@
"@fnando/sparkline": "^0.3.10",
"@popperjs/core": "^2.11.8",
"@release-it/conventional-changelog": "^8.0.1",
"@slickgrid-universal/composite-editor-component": "~4.4.1",
"@slickgrid-universal/custom-tooltip-plugin": "~4.4.1",
"@slickgrid-universal/excel-export": "~4.4.1",
"@slickgrid-universal/graphql": "~4.4.1",
"@slickgrid-universal/odata": "~4.4.1",
"@slickgrid-universal/rxjs-observable": "~4.4.1",
"@slickgrid-universal/text-export": "~4.4.1",
"@slickgrid-universal/composite-editor-component": "~4.5.0",
"@slickgrid-universal/custom-tooltip-plugin": "~4.5.0",
"@slickgrid-universal/excel-export": "~4.5.0",
"@slickgrid-universal/graphql": "~4.5.0",
"@slickgrid-universal/odata": "~4.5.0",
"@slickgrid-universal/rxjs-observable": "~4.5.0",
"@slickgrid-universal/text-export": "~4.5.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
Expand Down Expand Up @@ -192,4 +192,4 @@
"resolutions": {
"caniuse-lite": "1.0.30001591"
}
}
}
47 changes: 41 additions & 6 deletions src/examples/slickgrid/Example1.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { Column, Formatters, GridOption, SlickgridReact } from '../../slickgrid-react';
import { Column, Formatters, GridOption, SlickgridReact, SlickgridReactInstance } from '../../slickgrid-react';

const NB_ITEMS = 995;

Expand All @@ -19,6 +19,9 @@ interface State {
}

export default class Example1 extends React.Component<Props, State> {
private _darkModeGrid1 = false;
reactGrid1!: SlickgridReactInstance;

constructor(public readonly props: Props) {
super(props);

Expand Down Expand Up @@ -47,6 +50,14 @@ export default class Example1 extends React.Component<Props, State> {
}));
}

reactGrid1Ready(reactGrid: SlickgridReactInstance) {
this.reactGrid1 = reactGrid;
}

isBrowserDarkModeEnabled() {
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
}

/* Define grid Options and Columns */
defineGrids() {
const columns: Column[] = [
Expand All @@ -57,7 +68,9 @@ export default class Example1 extends React.Component<Props, State> {
{ id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso },
{ id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', sortable: true, minWidth: 100 }
];
this._darkModeGrid1 = this.isBrowserDarkModeEnabled();
const gridOptions1: GridOption = {
darkMode: this._darkModeGrid1,
gridHeight: 225,
gridWidth: 800,
enableAutoResize: false,
Expand All @@ -67,6 +80,7 @@ export default class Example1 extends React.Component<Props, State> {
// copy the same Grid Options and Column Definitions to 2nd grid
// but also add Pagination in this grid
const gridOptions2: GridOption = {
darkMode: false,
gridHeight: 225,
gridWidth: 800,
enableAutoResize: false,
Expand Down Expand Up @@ -110,6 +124,16 @@ export default class Example1 extends React.Component<Props, State> {
return mockDataset;
}

toggleDarkModeGrid1() {
this._darkModeGrid1 = !this._darkModeGrid1;
if (this._darkModeGrid1) {
document.querySelector('.grid-container1')?.classList.add('dark-mode');
} else {
document.querySelector('.grid-container1')?.classList.remove('dark-mode');
}
this.reactGrid1.slickGrid?.setOptions({ darkMode: this._darkModeGrid1 });
}

render() {
return !this.state.gridOptions1 ? '' : (
<div id="demo-container" className="container-fluid">
Expand All @@ -125,11 +149,22 @@ export default class Example1 extends React.Component<Props, State> {
</h2>
<div className="subtitle">{this.state.subTitle}</div>

<h3>Grid 1</h3>
<SlickgridReact gridId="grid1"
columnDefinitions={this.state.columnDefinitions1}
gridOptions={this.state.gridOptions1!}
dataset={this.state.dataset1} />
<h3>
<div className="column">
<span className="mr-3">Grid 1</span>
<button className="btn btn-outline-secondary btn-sm ms-2" onClick={() => this.toggleDarkModeGrid1()} data-test="toggle-dark-mode">
<span>Toggle Dark Mode</span>
</button>
</div>
</h3>

<div className="grid-container1">
<SlickgridReact gridId="grid1"
columnDefinitions={this.state.columnDefinitions1}
gridOptions={this.state.gridOptions1!}
dataset={this.state.dataset1}
onReactGridCreated={$event => this.reactGrid1Ready($event.detail)} />
</div>

<hr />

Expand Down
21 changes: 21 additions & 0 deletions src/examples/slickgrid/Example24.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const taskTranslateFormatter: Formatter = (_row, _cell, value, _columnDef, _data
};

class Example24 extends React.Component<Props, State> {
private _darkModeGrid = false;
title = 'Example 24: Cell Menu & Context Menu Plugins';
subTitle = `Add Cell Menu and Context Menu
<ul>
Expand Down Expand Up @@ -128,6 +129,11 @@ class Example24 extends React.Component<Props, State> {
this.defineGrid();
}

componentWillUnmount(): void {
document.querySelector('.panel-wm-content')!.classList.remove('dark-mode');
document.querySelector<HTMLDivElement>('#demo-container')!.dataset.bsTheme = 'light';
}

reactGridReady(reactGrid: SlickgridReactInstance) {
this.reactGrid = reactGrid;
}
Expand Down Expand Up @@ -554,6 +560,18 @@ class Example24 extends React.Component<Props, State> {
this.setState((state: State) => ({ ...state, selectedLanguage: nextLanguage }));
}

toggleDarkModeGrid() {
this._darkModeGrid = !this._darkModeGrid;
if (this._darkModeGrid) {
document.querySelector<HTMLDivElement>('.panel-wm-content')!.classList.add('dark-mode');
document.querySelector<HTMLDivElement>('#demo-container')!.dataset.bsTheme = 'dark';
} else {
document.querySelector('.panel-wm-content')!.classList.remove('dark-mode');
document.querySelector<HTMLDivElement>('#demo-container')!.dataset.bsTheme = 'light';
}
this.reactGrid.slickGrid?.setOptions({ darkMode: this._darkModeGrid });
}

render() {
return !this.state.gridOptions ? '' : (
<div id="demo-container" className="container-fluid">
Expand All @@ -566,6 +584,9 @@ class Example24 extends React.Component<Props, State> {
<span className="fa fa-link"></span> code
</a>
</span>
<button className="btn btn-outline-secondary btn-sm ms-2" onClick={() => this.toggleDarkModeGrid()} data-test="toggle-dark-mode">
<span>Toggle Dark Mode</span>
</button>
</h2>
<div className="subtitle" dangerouslySetInnerHTML={{ __html: this.subTitle }}></div>

Expand Down
27 changes: 27 additions & 0 deletions src/examples/slickgrid/Example30.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ interface State extends BaseSlickGridState {
complexityLevelList: Array<{ value: number; label: string; }>;
}
export default class Example30 extends React.Component<Props, State> {
private _darkModeGrid = false;

title = 'Example 30: Composite Editor Modal';
subTitle = `Composite Editor allows you to Create, Clone, Edit, Mass Update & Mass Selection Changes inside a nice Modal Window.
<br>The modal is simply populated by looping through your column definition list and also uses a lot of the same logic as inline editing (see <a href="https://ghiscoding.gitbook.io/slickgrid-react/grid-functionalities/composite-editor-modal" target="_blank">Composite Editor - Wiki</a>.)`;
Expand Down Expand Up @@ -127,6 +129,11 @@ export default class Example30 extends React.Component<Props, State> {
this.defineGrids();
}

componentWillUnmount() {
document.querySelector('.panel-wm-content')!.classList.remove('dark-mode');
document.querySelector<HTMLDivElement>('#demo-container')!.dataset.bsTheme = 'light';
}

/* Define grid Options and Columns */
defineGrids() {
const columnDefinitions: Column[] = [
Expand Down Expand Up @@ -635,6 +642,11 @@ export default class Example30 extends React.Component<Props, State> {
resetFormButtonIconCssClass: 'fa fa-undo',
onClose: () => Promise.resolve(confirm('You have unsaved changes, are you sure you want to close this window?')),
onError: (error) => alert(error.message),
onRendered: (modalElm) => {
// Bootstrap requires extra attribute when toggling Dark Mode (data-bs-theme="dark")
// we need to manually add this attribute ourselve before opening the Composite Editor Modal
modalElm.dataset.bsTheme = this._darkModeGrid ? 'dark' : 'light';
},
onSave: (formValues, _selection, dataContext) => {
const serverResponseDelay = 50;

Expand Down Expand Up @@ -678,6 +690,18 @@ export default class Example30 extends React.Component<Props, State> {
this.reactGrid.slickGrid.setOptions({ editable: isGridEditable });
}

toggleDarkModeGrid() {
this._darkModeGrid = !this._darkModeGrid;
if (this._darkModeGrid) {
document.querySelector<HTMLDivElement>('.panel-wm-content')!.classList.add('dark-mode');
document.querySelector<HTMLDivElement>('#demo-container')!.dataset.bsTheme = 'dark';
} else {
document.querySelector('.panel-wm-content')!.classList.remove('dark-mode');
document.querySelector<HTMLDivElement>('#demo-container')!.dataset.bsTheme = 'light';
}
this.reactGrid.slickGrid?.setOptions({ darkMode: this._darkModeGrid });
}

removeUnsavedStylingFromCell(_item: any, column: Column, row: number) {
// remove unsaved css class from that cell
const cssStyleKey = `unsaved_highlight_${[column.id]}${row}`;
Expand Down Expand Up @@ -985,6 +1009,9 @@ export default class Example30 extends React.Component<Props, State> {
<div id="demo-container" className="container-fluid">
<h2>
{this.title}
<button className="btn btn-outline-secondary btn-sm ms-2" onClick={() => this.toggleDarkModeGrid()} data-test="toggle-dark-mode">
<span>Toggle Dark Mode</span>
</button>
<span className="float-end font18">
see&nbsp;
<a target="_blank"
Expand Down
30 changes: 19 additions & 11 deletions src/examples/slickgrid/example30.scss
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
$button-border-color: #ababab !default;
$slick-button-border-color: #ababab !default;

.editable-field {
// box-shadow: inset 0 0 0 1px lightblue !important;
background-color: rgba(227, 240, 251, 0.569) !important;
}

.slick-dark-mode .editable-field {
background-color: rgb(105 123 145 / 57%) !important
}

.unsaved-editable-field {
background-color: #fbfdd1 !important;
}
.button-style {
cursor: pointer;
background-color: white;
border: 1px solid #{$button-border-color};
border-radius: 2px;
justify-content: center;
text-align: center;
&:hover {
border-color: darken($button-border-color, 10%);
}

.slick-dark-mode .unsaved-editable-field {
background-color: rgba(255, 183, 50, 0.8) !important;
color: white;
}

.slick-dark-mode {
--bs-btn-color: #bebebe;
}

.panel-wm {
width: calc(100vw - 12px);
}
14 changes: 2 additions & 12 deletions src/examples/slickgrid/example32.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,7 @@ $button-border-color: #ababab !default;
.editable-field {
background-color: rgba(227, 240, 251, 0.569) !important;
}

.unsaved-editable-field {
background-color: #fbfdd1 !important;
}
.button-style {
cursor: pointer;
background-color: white;
border: 1px solid #{$button-border-color};
border-radius: 2px;
justify-content: center;
text-align: center;
&:hover {
border-color: darken($button-border-color, 10%);
}
}
}
Loading

0 comments on commit d4bfdd1

Please sign in to comment.