fix(ui): make playlist toggle switches visible in all themes (#6277)

* fix(ui): make playlist toggle switches visible in all themes

The Public and Auto-import switches in the playlist list did not set a
color, so Material-UI used the theme's secondary color. Many themes use
secondary as a surface color close to the table background, which made
checked switches nearly invisible (Catppuccin, Rosé Pine, Monokai,
Moonbase and others).

Set color="primary" on the playlist switch, like every other switch in
the app, and make primary the default MuiSwitch color in useCurrentTheme
so future switches cannot regress. Fixes #6272.

* refactor(ui): drop secondary switch overrides from themes

Dracula, Gruvbox Dark, Tokyo Night and Tokyo Night Light styled checked
MuiSwitch colorSecondary to work around the same invisible-switch problem
(Gruvbox in #5064). With primary as the default switch color and every
switch in the app using it, no switch renders with colorSecondary anymore,
so these overrides are dead code.
This commit is contained in:
Deluan Quintão 2026-10-06 06:50:11 -07:00 • committed by GitHub
commit caa2f8a0c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 57 additions and 40 deletions

View file

@ -95,6 +95,7 @@ export const ToggleField = ({ resource, source }) => {
return (
<Switch
checked={record[source]}
color="primary"
onClick={handleClick}
disabled={!isWritable(record.ownerId)}
/>

View file

@ -2,6 +2,7 @@ import React from 'react'
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { TestContext } from 'ra-test'
import { RecordContextProvider } from 'react-admin'
import { PlaylistLove, ToggleField, ToggleAutoImport } from './PlaylistList'
vi.mock('../config', () => ({
@ -14,6 +15,7 @@ vi.mock('../common', () => ({
{record?.starred ? 'starred' : 'not-starred'}
</button>
),
isWritable: (ownerId) => ownerId === 'me',
}))
describe('<PlaylistLove />', () => {
@ -55,3 +57,28 @@ describe('playlist toggles without a record', () => {
expect(container.innerHTML).toBe('')
})
})
// Secondary is a surface color in many themes, so these toggles must use primary
describe('<ToggleField />', () => {
const renderToggle = (record) =>
render(
<TestContext>
<RecordContextProvider value={record}>
<ToggleField resource="playlist" source="public" />
</RecordContextProvider>
</TestContext>,
)
it.each([
['owner', 'me', false],
['non-owner', 'someone-else', true],
])('renders a primary-colored switch for the %s', (_, ownerId, disabled) => {
renderToggle({ id: 'pl-1', public: true, ownerId })
const input = screen.getByRole('checkbox')
const switchBase = input.closest('.MuiSwitch-switchBase')
expect(input.checked).toBe(true)
expect(input.disabled).toBe(disabled)
expect(switchBase.classList).toContain('MuiSwitch-colorPrimary')
expect(switchBase.classList).not.toContain('MuiSwitch-colorSecondary')
})
})

View file

@ -185,16 +185,6 @@ export default {
color: `${foreground} !important`,
},
},
MuiSwitch: {
colorSecondary: {
'&$checked': {
color: green,
},
'&$checked + $track': {
backgroundColor: green,
},
},
},
NDAlbumGridView: {
albumName: {
marginTop: '0.5rem',

View file

@ -121,16 +121,6 @@ export default {
boxShadow: '3px 3px 5px #3c3836',
},
},
MuiSwitch: {
colorSecondary: {
'&$checked': {
color: '#458588',
},
'&$checked + $track': {
backgroundColor: '#458588',
},
},
},
NDMobileArtistDetails: {
bgContainer: {
background:

View file

@ -184,16 +184,6 @@ export default {
color: `${foreground} !important`,
},
},
MuiSwitch: {
colorSecondary: {
'&$checked': {
color: blue,
},
'&$checked + $track': {
backgroundColor: blue,
},
},
},
NDAlbumGridView: {
albumName: {
marginTop: '0.5rem',

View file

@ -184,16 +184,6 @@ export default {
color: `${foreground} !important`,
},
},
MuiSwitch: {
colorSecondary: {
'&$checked': {
color: blue,
},
'&$checked + $track': {
backgroundColor: blue,
},
},
},
NDAlbumGridView: {
albumName: {
marginTop: '0.5rem',

View file

@ -63,6 +63,8 @@ const useCurrentTheme = () => {
...theme.props,
MuiUseMediaQuery: { noSsr: true },
MuiPopover: { disableScrollLock: true },
// MUI defaults to secondary, which many themes use as a surface color
MuiSwitch: { color: 'primary' },
},
}),
[theme],

View file

@ -3,6 +3,10 @@ import { Provider } from 'react-redux'
import { createStore } from 'redux'
import mediaQuery from 'css-mediaquery'
import { renderHook } from '@testing-library/react-hooks'
import { render, screen } from '@testing-library/react'
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'
import Switch from '@material-ui/core/Switch'
import themes from './index'
import useCurrentTheme from './useCurrentTheme'
import { themeReducer } from '../reducers/themeReducer'
import { AUTO_THEME_ID } from '../consts'
@ -161,4 +165,27 @@ describe('useCurrentTheme', () => {
expect(document.body.style.backgroundColor).toBe('rgb(18, 18, 18)')
})
})
describe('switch color', () => {
it.each(Object.keys(themes))(
'renders switches with the primary color in %s',
(theme) => {
const { result } = renderHook(() => useCurrentTheme(), {
wrapper: ({ children }) => (
<Provider store={createStore(themeReducer, { theme })}>
{children}
</Provider>
),
})
render(
<ThemeProvider theme={createMuiTheme(result.current)}>
<Switch checked onChange={() => {}} />
</ThemeProvider>,
)
const switchBase = screen
.getByRole('checkbox')
.closest('.MuiSwitch-switchBase')
expect(switchBase.classList).toContain('MuiSwitch-colorPrimary')
},
)
})
})