From 8907ea5310737b258fde7c8a011ae2d9b6e43f8f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 18 Dec 2023 23:05:17 +0100 Subject: [PATCH 1/2] Set default theme based on system preferences --- CHANGELOG.md | 1 + config/test/setupTests.ts | 1 + package-lock.json | 14 ++++---- package.json | 3 +- src/app/App.tsx | 4 +-- src/settings/UserInterfaceSettings.tsx | 37 ++++++++++++-------- src/settings/reducers/settings.ts | 3 +- test/app/App.test.tsx | 30 +++++++--------- test/settings/UserInterfaceSettings.test.tsx | 30 +++++++++------- 9 files changed, 67 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e932849f..cfab1bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed * [#338](https://github.com/shlinkio/shlink-web-client/issues/338) Extract `@shlinkio/shlink-web-component` and `@shlinkio/shlink-frontend-kit` as external libs. +* [#978](https://github.com/shlinkio/shlink-web-client/issues/978) Use system preferred theme as default theme. * Use API client from `@shlinkio/shlink-js-sdk` to consume Shlink servers. * [#902](https://github.com/shlinkio/shlink-web-client/pull/902) Docker image is no longer running as root. As a side effect, exposed port is `8080`, not `80` anymore. diff --git a/config/test/setupTests.ts b/config/test/setupTests.ts index 5862d9b0..893d13ae 100644 --- a/config/test/setupTests.ts +++ b/config/test/setupTests.ts @@ -21,3 +21,4 @@ afterEach(() => { HTMLCanvasElement.prototype.getContext = (() => {}) as any; (global as any).scrollTo = () => {}; +(global as any).matchMedia = () => ({ matches: false }); diff --git a/package-lock.json b/package-lock.json index 9dcf7468..bba94c69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@json2csv/plainjs": "^7.0.3", "@reduxjs/toolkit": "^2.0.1", - "@shlinkio/data-manipulation": "^1.0.2", + "@shlinkio/data-manipulation": "^1.0.3", "@shlinkio/shlink-frontend-kit": "^0.4.1", "@shlinkio/shlink-js-sdk": "^0.2.0", "@shlinkio/shlink-web-component": "^0.4.1", @@ -2934,9 +2934,9 @@ ] }, "node_modules/@shlinkio/data-manipulation": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.2.tgz", - "integrity": "sha512-Nzlr9cnKlehqSkoAFR8W/HJF9Nl2OU+zhOluQbSGBhUQgOjOJi00uAflib8LADKX2KUQexqDiz+sCwl8Pf4VKA==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.3.tgz", + "integrity": "sha512-DPEU5IykmDrf7Z71miIi2xavSKkxAiqu8KuUsqEGmzGb31ENW0eZvMOfN+wNJbSLaRYqPsW465AwJqxTWUAGVA==" }, "node_modules/@shlinkio/eslint-config-js-coding-standard": { "version": "2.3.0", @@ -13049,9 +13049,9 @@ "optional": true }, "@shlinkio/data-manipulation": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.2.tgz", - "integrity": "sha512-Nzlr9cnKlehqSkoAFR8W/HJF9Nl2OU+zhOluQbSGBhUQgOjOJi00uAflib8LADKX2KUQexqDiz+sCwl8Pf4VKA==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@shlinkio/data-manipulation/-/data-manipulation-1.0.3.tgz", + "integrity": "sha512-DPEU5IykmDrf7Z71miIi2xavSKkxAiqu8KuUsqEGmzGb31ENW0eZvMOfN+wNJbSLaRYqPsW465AwJqxTWUAGVA==" }, "@shlinkio/eslint-config-js-coding-standard": { "version": "2.3.0", diff --git a/package.json b/package.json index ace635db..8de6fa2e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "homepage": "", "repository": "https://github.com/shlinkio/shlink-web-client", "license": "MIT", + "type": "module", "scripts": { "lint": "npm run lint:css && npm run lint:js", "lint:css": "stylelint src/*.scss src/**/*.scss", @@ -31,7 +32,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@json2csv/plainjs": "^7.0.3", "@reduxjs/toolkit": "^2.0.1", - "@shlinkio/data-manipulation": "^1.0.2", + "@shlinkio/data-manipulation": "^1.0.3", "@shlinkio/shlink-frontend-kit": "^0.4.1", "@shlinkio/shlink-js-sdk": "^0.2.0", "@shlinkio/shlink-web-component": "^0.4.1", diff --git a/src/app/App.tsx b/src/app/App.tsx index abb566cd..2512cd7c 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,4 +1,4 @@ -import { changeThemeInMarkup } from '@shlinkio/shlink-frontend-kit'; +import { changeThemeInMarkup, getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; import { clsx } from 'clsx'; import type { FC } from 'react'; import { useEffect, useRef } from 'react'; @@ -58,7 +58,7 @@ const App: FCWithDeps = ( }, [fetchServers]); useEffect(() => { - changeThemeInMarkup(settings.ui?.theme ?? 'light'); + changeThemeInMarkup(settings.ui?.theme ?? getSystemPreferredTheme()); }, [settings.ui?.theme]); return ( diff --git a/src/settings/UserInterfaceSettings.tsx b/src/settings/UserInterfaceSettings.tsx index 01d8d29e..9d6534d1 100644 --- a/src/settings/UserInterfaceSettings.tsx +++ b/src/settings/UserInterfaceSettings.tsx @@ -1,27 +1,34 @@ import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { Theme } from '@shlinkio/shlink-frontend-kit'; -import { SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit'; +import { getSystemPreferredTheme, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit'; import type { FC } from 'react'; +import { useMemo } from 'react'; import type { AppSettings, UiSettings } from './reducers/settings'; import './UserInterfaceSettings.scss'; interface UserInterfaceProps { settings: AppSettings; setUiSettings: (settings: UiSettings) => void; + + /* Test seam */ + _matchMedia?: typeof window.matchMedia; } -export const UserInterfaceSettings: FC = ({ settings: { ui }, setUiSettings }) => ( - - - { - const theme: Theme = useDarkTheme ? 'dark' : 'light'; - setUiSettings({ ...ui, theme }); - }} - > - Use dark theme. - - -); +export const UserInterfaceSettings: FC = ({ settings: { ui }, setUiSettings, _matchMedia }) => { + const currentTheme = useMemo(() => ui?.theme ?? getSystemPreferredTheme(_matchMedia), [ui?.theme, _matchMedia]); + return ( + + + { + const theme: Theme = useDarkTheme ? 'dark' : 'light'; + setUiSettings({ ...ui, theme }); + }} + > + Use dark theme. + + + ); +}; diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index 8d3143f5..659c9181 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -2,6 +2,7 @@ import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { mergeDeepRight } from '@shlinkio/data-manipulation'; import type { Theme } from '@shlinkio/shlink-frontend-kit'; +import { getSystemPreferredTheme } from '@shlinkio/shlink-frontend-kit'; import type { Settings, ShortUrlCreationSettings, @@ -34,7 +35,7 @@ const initialState: AppSettings = { validateUrls: false, }, ui: { - theme: 'light', + theme: getSystemPreferredTheme(), }, visits: { defaultInterval: 'last30Days', diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index 32e52a4d..6a2b043a 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { createMemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import { AppFactory } from '../../src/app/App'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -18,22 +17,17 @@ describe('', () => { ShlinkVersionsContainer: () => <>ShlinkVersions, }), ); - const setUp = (activeRoute = '/') => { - const history = createMemoryHistory(); - history.push(activeRoute); - - return render( - - {}} - servers={{}} - settings={fromPartial({})} - appUpdated - resetAppUpdate={() => {}} - /> - , - ); - }; + const setUp = (activeRoute = '/') => render( + + {}} + servers={{}} + settings={fromPartial({})} + appUpdated + resetAppUpdate={() => {}} + /> + , + ); it('passes a11y checks', () => checkAccessibility(setUp())); diff --git a/test/settings/UserInterfaceSettings.test.tsx b/test/settings/UserInterfaceSettings.test.tsx index 5627f8e7..c1fe739b 100644 --- a/test/settings/UserInterfaceSettings.test.tsx +++ b/test/settings/UserInterfaceSettings.test.tsx @@ -1,4 +1,3 @@ -import type { Theme } from '@shlinkio/shlink-frontend-kit'; import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { UiSettings } from '../../src/settings/reducers/settings'; @@ -8,18 +7,25 @@ import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const setUiSettings = vi.fn(); - const setUp = (ui?: UiSettings) => renderWithEvents( - , + const setUp = (ui?: UiSettings, defaultDarkTheme = false) => renderWithEvents( + , ); it('passes a11y checks', () => checkAccessibility(setUp())); it.each([ - [{ theme: 'dark' as Theme }, true], - [{ theme: 'light' as Theme }, false], - [undefined, false], - ])('toggles switch if theme is dark', (ui, expectedChecked) => { - setUp(ui); + [{ theme: 'dark' as const }, true, true], + [{ theme: 'dark' as const }, false, true], + [{ theme: 'light' as const }, true, false], + [{ theme: 'light' as const }, false, false], + [undefined, false, false], + [undefined, true, true], + ])('toggles switch if theme is dark', (ui, defaultDarkTheme, expectedChecked) => { + setUp(ui, defaultDarkTheme); if (expectedChecked) { expect(screen.getByLabelText('Use dark theme.')).toBeChecked(); @@ -29,8 +35,8 @@ describe('', () => { }); it.each([ - [{ theme: 'dark' as Theme }], - [{ theme: 'light' as Theme }], + [{ theme: 'dark' as const }], + [{ theme: 'light' as const }], [undefined], ])('shows different icons based on theme', (ui) => { setUp(ui); @@ -38,8 +44,8 @@ describe('', () => { }); it.each([ - ['light' as Theme, 'dark' as Theme], - ['dark' as Theme, 'light' as Theme], + ['light' as const, 'dark' as const], + ['dark' as const, 'light' as const], ])('invokes setUiSettings when theme toggle value changes', async (initialTheme, expectedTheme) => { const { user } = setUp({ theme: initialTheme }); From 683c049a236e0882a6ee7baff11ff98b7f84afe7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 18 Dec 2023 23:12:51 +0100 Subject: [PATCH 2/2] Make sure set-homepage script is treated as a commonjs script --- .github/workflows/deploy-preview.yml | 2 +- scripts/{set-homepage.js => set-homepage.cjs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{set-homepage.js => set-homepage.cjs} (100%) diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index ba6d7068..bd5c2a18 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -20,7 +20,7 @@ jobs: - name: Build run: | npm ci && \ - node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \ + node ./scripts/set-homepage.cjs /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \ npm run build - name: Deploy preview uses: shlinkio/deploy-preview-action@v1.0.1 diff --git a/scripts/set-homepage.js b/scripts/set-homepage.cjs similarity index 100% rename from scripts/set-homepage.js rename to scripts/set-homepage.cjs