Skip to content

Commit 924c5a7

Browse files
authored
feat: Appearance Setting (Dark Mode) (#457)
* feat: Create setting for appearance mode (system, light, dark) * chore: Middleware for updating appearance * feat: Improve UI for radio & checkbox * feat: Set appearance mode on load
1 parent bf4c190 commit 924c5a7

File tree

15 files changed

+436
-49
lines changed

15 files changed

+436
-49
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`components/fields/radiogroup.tsx should render 1`] = `
4+
<fieldset
5+
className="mb-1"
6+
id="appearance"
7+
>
8+
<div>
9+
<legend
10+
className="mb-1 text-base font-medium dark:text-white"
11+
>
12+
Appearance
13+
</legend>
14+
<p
15+
className="text-sm text-gray-500"
16+
>
17+
This is some helper text
18+
</p>
19+
</div>
20+
<div
21+
aria-labelledby="appearance"
22+
className="flex items-center space-x-4 py-2"
23+
role="group"
24+
>
25+
<div
26+
className="flex items-center mt-1"
27+
>
28+
<input
29+
checked={false}
30+
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
31+
id="appearance_one"
32+
name="appearance"
33+
onChange={[MockFunction]}
34+
type="radio"
35+
value="one"
36+
/>
37+
<label
38+
className="ml-3 block text-sm font-medium text-gray-700 dark:text-white"
39+
htmlFor="appearance_one"
40+
>
41+
Value 1
42+
</label>
43+
</div>
44+
<div
45+
className="flex items-center mt-1"
46+
>
47+
<input
48+
checked={true}
49+
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
50+
id="appearance_two"
51+
name="appearance"
52+
onChange={[MockFunction]}
53+
type="radio"
54+
value="two"
55+
/>
56+
<label
57+
className="ml-3 block text-sm font-medium text-gray-700 dark:text-white"
58+
htmlFor="appearance_two"
59+
>
60+
Value 2
61+
</label>
62+
</div>
63+
</div>
64+
</fieldset>
65+
`;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react';
2+
import * as TestRendener from 'react-test-renderer';
3+
import { fireEvent, render } from '@testing-library/react';
4+
5+
import { FieldRadioGroup } from './radiogroup';
6+
7+
describe('components/fields/radiogroup.tsx', () => {
8+
const props = {
9+
label: 'Appearance',
10+
name: 'appearance',
11+
placeholder: 'This is some helper text',
12+
options: [
13+
{ label: 'Value 1', value: 'one' },
14+
{ label: 'Value 2', value: 'two' },
15+
],
16+
onChange: jest.fn(),
17+
value: 'two',
18+
};
19+
20+
it('should render ', () => {
21+
const tree = TestRendener.create(<FieldRadioGroup {...props} />);
22+
expect(tree).toMatchSnapshot();
23+
});
24+
25+
it('should check that NProgress is getting called in getDerivedStateFromProps (loading)', function () {
26+
const { getByLabelText } = render(<FieldRadioGroup {...props} />);
27+
fireEvent.click(getByLabelText('Value 1'));
28+
expect(props.onChange).toHaveBeenCalledTimes(1);
29+
});
30+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as React from 'react';
2+
import { RadioGroupItem } from '../../../types';
3+
4+
export const FieldRadioGroup = ({
5+
label,
6+
placeholder,
7+
name,
8+
options,
9+
onChange,
10+
value,
11+
}: {
12+
name: string;
13+
label: string;
14+
placeholder?: string;
15+
options: RadioGroupItem[];
16+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
17+
value: string;
18+
}) => {
19+
return (
20+
<fieldset id={name} className="mb-1">
21+
<div>
22+
<legend className="mb-1 text-base font-medium dark:text-white">
23+
{label}
24+
</legend>
25+
{placeholder && <p className="text-sm text-gray-500">{placeholder}</p>}
26+
</div>
27+
28+
<div
29+
className="flex items-center space-x-4 py-2"
30+
role="group"
31+
aria-labelledby={name}
32+
>
33+
{options.map((item) => {
34+
return (
35+
<div
36+
className="flex items-center mt-1"
37+
key={`radio_item_${item.value.toLowerCase()}`}
38+
>
39+
<input
40+
type="radio"
41+
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
42+
id={`${name}_${item.value.toLowerCase()}`}
43+
name={name}
44+
value={item.value}
45+
onChange={onChange}
46+
checked={item.value === value}
47+
/>
48+
<label
49+
htmlFor={`${name}_${item.value.toLowerCase()}`}
50+
className="ml-3 block text-sm font-medium text-gray-700 dark:text-white"
51+
>
52+
{item.label}
53+
</label>
54+
</div>
55+
);
56+
})}
57+
</div>
58+
</fieldset>
59+
);
60+
};

src/js/components/ui/checkbox.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,35 @@ interface IFieldCheckbox {
55
label: string;
66
checked: boolean;
77
onChange: any;
8+
placeholder?: string;
89
}
910

1011
export const FieldCheckbox = (props: IFieldCheckbox) => {
1112
return (
12-
<div className="mt-1 mb-2">
13-
<label className="inline-flex items-center mt-2">
13+
<div className="flex items-start mt-1 mb-3">
14+
<div className="flex items-center h-5">
1415
<input
1516
type="checkbox"
16-
className="h-5 w-5"
17+
id={props.name}
18+
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
1719
checked={props.checked}
1820
onChange={props.onChange}
1921
/>
22+
</div>
2023

21-
<span className="ml-4 text-gray-700 dark:text-white">
24+
<div className="ml-3 text-sm">
25+
<label
26+
htmlFor={props.name}
27+
className="font-medium text-gray-700 dark:text-gray-200"
28+
>
2229
{props.label}
23-
</span>
24-
</label>
30+
</label>
31+
{props.placeholder && (
32+
<p className="text-gray-500 dark:text-gray-300">
33+
{props.placeholder}
34+
</p>
35+
)}
36+
</div>
2537
</div>
2638
);
2739
};

src/js/middleware/settings.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { UPDATE_SETTING } from '../actions';
2-
import { restoreSetting, setAutoLaunch } from '../utils/comms';
2+
import { setAppearance } from '../utils/appearance';
3+
import { setAutoLaunch } from '../utils/comms';
34

45
export default () => (next) => (action) => {
56
switch (action.type) {
67
case UPDATE_SETTING:
78
if (action.setting === 'openAtStartup') {
89
setAutoLaunch(action.value);
910
}
11+
12+
if (action.setting === 'appearance') {
13+
setAppearance(action.value);
14+
}
1015
}
1116

1217
return next(action);

src/js/reducers/__snapshots__/settings.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
exports[`reducers/settings.ts should handle UPDATE_SETTING 1`] = `
44
Object {
5+
"appearance": "SYSTEM",
56
"markOnClick": false,
67
"openAtStartup": false,
78
"participating": true,
@@ -12,6 +13,7 @@ Object {
1213

1314
exports[`reducers/settings.ts should handle UPDATE_SETTING 2`] = `
1415
Object {
16+
"appearance": "SYSTEM",
1517
"markOnClick": false,
1618
"openAtStartup": true,
1719
"participating": false,
@@ -22,6 +24,7 @@ Object {
2224

2325
exports[`reducers/settings.ts should return the initial state 1`] = `
2426
Object {
27+
"appearance": "SYSTEM",
2528
"markOnClick": false,
2629
"openAtStartup": false,
2730
"participating": false,

src/js/reducers/settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { UPDATE_SETTING } from '../actions';
22
import { SettingsState } from '../../types/reducers';
3+
import { Appearance } from '../../types';
34

45
const initialState: SettingsState = {
56
participating: false,
67
playSound: true,
78
showNotifications: true,
89
markOnClick: false,
910
openAtStartup: false,
11+
appearance: Appearance.SYSTEM,
1012
};
1113

1214
export default function reducer(state = initialState, action): SettingsState {

0 commit comments

Comments
 (0)