@patternfly/documentation-framework 6.10.32 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 6.11.0 (2025-06-16)
7
+
8
+
9
+ ### Features
10
+
11
+ * **ThemeSwitcher:** Enhanced theme switcher with system preference detection and tri-state selection ([#4639](https://github.com/patternfly/patternfly-org/issues/4639)) ([1b05742](https://github.com/patternfly/patternfly-org/commit/1b057425216a484dda1f46ba37842c22b77c7b87))
12
+
13
+
14
+
15
+
16
+
17
+ ## 6.10.33 (2025-06-16)
18
+
19
+ **Note:** Version bump only for package @patternfly/documentation-framework
20
+
21
+
22
+
23
+
24
+
6
25
  ## 6.10.32 (2025-06-16)
7
26
 
8
27
  **Note:** Version bump only for package @patternfly/documentation-framework
@@ -1,4 +1,4 @@
1
- import React, { useContext, useEffect } from 'react';
1
+ import React, { useContext, useEffect, useState, useCallback } from 'react';
2
2
  import { useLocation } from '@reach/router';
3
3
  import {
4
4
  Button,
@@ -8,6 +8,10 @@ import {
8
8
  debounce,
9
9
  Label,
10
10
  Switch,
11
+ Select,
12
+ SelectOption,
13
+ SelectList,
14
+ MenuToggle,
11
15
  Tooltip,
12
16
  Stack,
13
17
  StackItem
@@ -19,6 +23,9 @@ import * as reactTableModule from '@patternfly/react-table';
19
23
  import * as reactTableDeprecatedModule from '@patternfly/react-table/deprecated';
20
24
  import { css } from '@patternfly/react-styles';
21
25
  import { getParameters } from 'codesandbox/lib/api/define';
26
+ import SunIcon from '@patternfly/react-icons/dist/esm/icons/sun-icon';
27
+ import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon';
28
+ import DesktopIcon from '@patternfly/react-icons/dist/esm/icons/desktop-icon';
22
29
  import { ExampleToolbar } from './exampleToolbar.jsx';
23
30
  import { AutoLinkHeader } from '../autoLinkHeader/autoLinkHeader';
24
31
  import {
@@ -32,9 +39,89 @@ import {
32
39
  import { convertToReactComponent } from '@patternfly/ast-helpers';
33
40
  import missingThumbnail from './missing-thumbnail.jpg';
34
41
  import { RtlContext } from '../../layouts';
42
+ import { useTheme } from '../../hooks/useTheme';
35
43
 
36
44
  const errorComponent = (err) => <pre>{err.toString()}</pre>;
37
45
 
46
+ // Full-screen theme selector component using shared theme hook
47
+ const FullScreenThemeSelector = () => {
48
+ const { themeMode, setThemeMode, THEME_MODES } = useTheme();
49
+ const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
50
+
51
+ const handleThemeChange = (_event, selectedMode) => {
52
+ setThemeMode(selectedMode);
53
+ setIsThemeSelectOpen(false);
54
+ };
55
+
56
+ const getThemeDisplayText = (mode) => {
57
+ switch (mode) {
58
+ case THEME_MODES.LIGHT:
59
+ return 'Light';
60
+ case THEME_MODES.DARK:
61
+ return 'Dark';
62
+ default:
63
+ return 'System';
64
+ }
65
+ };
66
+
67
+ const getThemeIcon = (mode) => {
68
+ switch (mode) {
69
+ case THEME_MODES.LIGHT:
70
+ return <SunIcon />;
71
+ case THEME_MODES.DARK:
72
+ return <MoonIcon />;
73
+ default:
74
+ return <DesktopIcon />;
75
+ }
76
+ };
77
+
78
+ return (
79
+ <Select
80
+ id="ws-example-theme-select"
81
+ isOpen={isThemeSelectOpen}
82
+ selected={themeMode}
83
+ onSelect={handleThemeChange}
84
+ onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
85
+ toggle={(toggleRef) => (
86
+ <MenuToggle
87
+ ref={toggleRef}
88
+ onClick={() => setIsThemeSelectOpen(!isThemeSelectOpen)}
89
+ isExpanded={isThemeSelectOpen}
90
+ icon={getThemeIcon(themeMode)}
91
+ aria-label={`Theme selection, current: ${getThemeDisplayText(themeMode)}`}
92
+ >
93
+ {getThemeDisplayText(themeMode)}
94
+ </MenuToggle>
95
+ )}
96
+ shouldFocusToggleOnSelect
97
+ >
98
+ <SelectList>
99
+ <SelectOption
100
+ value={THEME_MODES.SYSTEM}
101
+ icon={<DesktopIcon />}
102
+ description="Follow system preference"
103
+ >
104
+ System
105
+ </SelectOption>
106
+ <SelectOption
107
+ value={THEME_MODES.LIGHT}
108
+ icon={<SunIcon />}
109
+ description="Always use light mode"
110
+ >
111
+ Light
112
+ </SelectOption>
113
+ <SelectOption
114
+ value={THEME_MODES.DARK}
115
+ icon={<MoonIcon />}
116
+ description="Always use dark mode"
117
+ >
118
+ Dark
119
+ </SelectOption>
120
+ </SelectList>
121
+ </Select>
122
+ );
123
+ };
124
+
38
125
  class ErrorBoundary extends React.Component {
39
126
  constructor(props) {
40
127
  super(props);
@@ -199,21 +286,10 @@ export const Example = ({
199
286
  {(hasDarkThemeSwitcher || hasRTLSwitcher) && (
200
287
  <Flex
201
288
  direction={{ default: 'column' }}
202
- gap={{ default: 'gapLg' }}
203
- className="ws-full-page-utils pf-v6-m-dir-ltr "
289
+ gap={{ default: 'gapMd' }}
290
+ className="ws-full-page-utils pf-v6-m-dir-ltr"
204
291
  >
205
- {hasDarkThemeSwitcher && (
206
- <Switch
207
- id="ws-example-theme-switch"
208
- label="Dark theme"
209
- defaultChecked={false}
210
- onChange={() =>
211
- document
212
- .querySelector('html')
213
- .classList.toggle('pf-v6-theme-dark')
214
- }
215
- />
216
- )}
292
+ {hasDarkThemeSwitcher && <FullScreenThemeSelector />}
217
293
  {hasRTLSwitcher && (
218
294
  <Switch
219
295
  id="ws-example-rtl-switch"
@@ -0,0 +1,137 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ const THEME_MODES = {
4
+ SYSTEM: 'system',
5
+ LIGHT: 'light',
6
+ DARK: 'dark'
7
+ };
8
+
9
+ const THEME_STORAGE_KEY = 'theme-preference';
10
+ const DARK_MODE_CLASS = 'pf-v6-theme-dark';
11
+
12
+ /**
13
+ * Custom hook for managing theme state with system preference detection
14
+ * @returns {Object} Theme state and controls
15
+ * @returns {string} themeMode - Current theme mode ('system'|'light'|'dark')
16
+ * @returns {Function} setThemeMode - Function to change theme mode
17
+ * @returns {string} resolvedTheme - The actual applied theme ('light'|'dark')
18
+ * @returns {Object} THEME_MODES - Available theme mode constants
19
+ */
20
+ export const useTheme = () => {
21
+ const getStoredThemeMode = () => {
22
+ if (typeof window === 'undefined' || !window.localStorage) {
23
+ return null;
24
+ }
25
+ return localStorage.getItem(THEME_STORAGE_KEY);
26
+ };
27
+
28
+ const setStoredThemeMode = (mode) => {
29
+ if (typeof window === 'undefined' || !window.localStorage) {
30
+ return;
31
+ }
32
+ localStorage.setItem(THEME_STORAGE_KEY, mode);
33
+ };
34
+
35
+ const getResolvedTheme = (mode) => {
36
+ // SSR-safe check for window and matchMedia
37
+ if (typeof window === 'undefined' || !window.matchMedia) {
38
+ return 'light';
39
+ }
40
+
41
+ if (mode === THEME_MODES.SYSTEM) {
42
+ try {
43
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
44
+ } catch (error) {
45
+ // Fallback if matchMedia fails
46
+ console.warn('matchMedia not supported, defaulting to light theme');
47
+ return 'light';
48
+ }
49
+ }
50
+ return mode;
51
+ };
52
+
53
+ const updateThemeClass = (resolvedTheme) => {
54
+ if (typeof window === 'undefined' || !document) {
55
+ return;
56
+ }
57
+
58
+ const htmlElement = document.querySelector('html');
59
+ if (!htmlElement) {
60
+ return;
61
+ }
62
+
63
+ if (resolvedTheme === 'dark') {
64
+ htmlElement.classList.add(DARK_MODE_CLASS);
65
+ } else {
66
+ htmlElement.classList.remove(DARK_MODE_CLASS);
67
+ }
68
+ };
69
+
70
+ const [themeMode, setThemeModeState] = useState(() => {
71
+ const stored = getStoredThemeMode();
72
+ return stored && Object.values(THEME_MODES).includes(stored) ? stored : THEME_MODES.SYSTEM;
73
+ });
74
+
75
+ const [resolvedTheme, setResolvedTheme] = useState(() => getResolvedTheme(themeMode));
76
+
77
+ const setThemeMode = useCallback((newMode) => {
78
+ setThemeModeState(newMode);
79
+ setStoredThemeMode(newMode);
80
+
81
+ const newResolvedTheme = getResolvedTheme(newMode);
82
+ setResolvedTheme(newResolvedTheme);
83
+ updateThemeClass(newResolvedTheme);
84
+ }, []);
85
+
86
+ // Listen for system preference changes
87
+ useEffect(() => {
88
+ // Enhanced SSR-safe check
89
+ if (typeof window === 'undefined' || !window.matchMedia) {
90
+ return;
91
+ }
92
+
93
+ let mediaQuery;
94
+ try {
95
+ mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
96
+ } catch (error) {
97
+ console.warn('matchMedia not supported, skipping system theme detection');
98
+ return;
99
+ }
100
+
101
+ const handleSystemThemeChange = (e) => {
102
+ if (themeMode === THEME_MODES.SYSTEM) {
103
+ const newSystemTheme = e.matches ? 'dark' : 'light';
104
+ setResolvedTheme(newSystemTheme);
105
+ updateThemeClass(newSystemTheme);
106
+ }
107
+ };
108
+
109
+ // Check if addEventListener is available (some older browsers might not support it)
110
+ if (mediaQuery.addEventListener) {
111
+ mediaQuery.addEventListener('change', handleSystemThemeChange);
112
+ return () => {
113
+ mediaQuery.removeEventListener('change', handleSystemThemeChange);
114
+ };
115
+ } else if (mediaQuery.addListener) {
116
+ // Fallback for older browsers
117
+ mediaQuery.addListener(handleSystemThemeChange);
118
+ return () => {
119
+ mediaQuery.removeListener(handleSystemThemeChange);
120
+ };
121
+ }
122
+ }, [themeMode]);
123
+
124
+ // Initial theme application
125
+ useEffect(() => {
126
+ const initialResolvedTheme = getResolvedTheme(themeMode);
127
+ setResolvedTheme(initialResolvedTheme);
128
+ updateThemeClass(initialResolvedTheme);
129
+ }, [themeMode]);
130
+
131
+ return {
132
+ themeMode,
133
+ setThemeMode,
134
+ resolvedTheme,
135
+ THEME_MODES
136
+ };
137
+ };
@@ -23,17 +23,20 @@ import {
23
23
  SkipToContent,
24
24
  Switch,
25
25
  SearchInput,
26
- ToggleGroup,
27
- ToggleGroupItem,
26
+ Select,
27
+ SelectOption,
28
+ SelectList,
28
29
  MastheadLogo
29
30
  } from '@patternfly/react-core';
30
31
  import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
31
32
  import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon';
32
33
  import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon';
33
34
  import SunIcon from '@patternfly/react-icons/dist/esm/icons/sun-icon';
35
+ import DesktopIcon from '@patternfly/react-icons/dist/esm/icons/desktop-icon';
34
36
  import { SideNav, TopNav, GdprBanner } from '../../components';
35
37
  import staticVersions from '../../versions.json';
36
38
  import { Footer } from '@patternfly/documentation-framework/components';
39
+ import { useTheme } from '../../hooks/useTheme';
37
40
 
38
41
  export const RtlContext = createContext(false);
39
42
 
@@ -46,8 +49,9 @@ const HeaderTools = ({
46
49
  topNavItems,
47
50
  isRTL,
48
51
  setIsRTL,
49
- isDarkTheme,
50
- setIsDarkTheme
52
+ themeMode,
53
+ setThemeMode,
54
+ THEME_MODES
51
55
  }) => {
52
56
  const latestVersion = versions.Releases.find((version) => version.latest);
53
57
  const previousReleases = Object.values(versions.Releases).filter((version) => !version.hidden && !version.latest);
@@ -55,6 +59,7 @@ const HeaderTools = ({
55
59
  const [isDropdownOpen, setDropdownOpen] = useState(false);
56
60
  const [searchValue, setSearchValue] = React.useState('');
57
61
  const [isSearchExpanded, setIsSearchExpanded] = React.useState(false);
62
+ const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
58
63
 
59
64
  const getDropdownItem = (version, isLatest = false) => (
60
65
  <DropdownItem itemId={version.name} key={version.name} to={isLatest ? '/' : `/${version.name}`}>
@@ -70,10 +75,31 @@ const HeaderTools = ({
70
75
  setIsSearchExpanded(!isSearchExpanded);
71
76
  };
72
77
 
73
- const toggleDarkTheme = (_evt, selected) => {
74
- const darkThemeToggleClicked = !selected === isDarkTheme;
75
- document.querySelector('html').classList.toggle('pf-v6-theme-dark', darkThemeToggleClicked);
76
- setIsDarkTheme(darkThemeToggleClicked);
78
+ const handleThemeChange = (_event, selectedMode) => {
79
+ setThemeMode(selectedMode);
80
+ setIsThemeSelectOpen(false);
81
+ };
82
+
83
+ const getThemeDisplayText = (mode) => {
84
+ switch (mode) {
85
+ case THEME_MODES.LIGHT:
86
+ return 'Light';
87
+ case THEME_MODES.DARK:
88
+ return 'Dark';
89
+ default:
90
+ return 'System';
91
+ }
92
+ };
93
+
94
+ const getThemeIcon = (mode) => {
95
+ switch (mode) {
96
+ case THEME_MODES.LIGHT:
97
+ return <SunIcon />;
98
+ case THEME_MODES.DARK:
99
+ return <MoonIcon />;
100
+ default:
101
+ return <DesktopIcon />;
102
+ }
77
103
  };
78
104
 
79
105
  useEffect(() => {
@@ -92,24 +118,6 @@ const HeaderTools = ({
92
118
  </ToolbarItem>
93
119
  )}
94
120
  <ToolbarGroup align={{ default: 'alignEnd' }}>
95
- {hasDarkThemeSwitcher && (
96
- <ToolbarItem>
97
- <ToggleGroup aria-label="Dark theme toggle group">
98
- <ToggleGroupItem
99
- aria-label="light theme toggle"
100
- icon={<SunIcon />}
101
- isSelected={!isDarkTheme}
102
- onChange={toggleDarkTheme}
103
- />
104
- <ToggleGroupItem
105
- aria-label="dark theme toggle"
106
- icon={<MoonIcon />}
107
- isSelected={isDarkTheme}
108
- onChange={toggleDarkTheme}
109
- />
110
- </ToggleGroup>
111
- </ToolbarItem>
112
- )}
113
121
  {hasRTLSwitcher && (
114
122
  <ToolbarItem>
115
123
  <Switch
@@ -147,6 +155,53 @@ const HeaderTools = ({
147
155
  <GithubIcon />
148
156
  </Button>
149
157
  </ToolbarItem>
158
+ {hasDarkThemeSwitcher && (
159
+ <ToolbarItem>
160
+ <Select
161
+ id="ws-theme-select"
162
+ isOpen={isThemeSelectOpen}
163
+ selected={themeMode}
164
+ onSelect={handleThemeChange}
165
+ onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
166
+ toggle={(toggleRef) => (
167
+ <MenuToggle
168
+ ref={toggleRef}
169
+ onClick={() => setIsThemeSelectOpen(!isThemeSelectOpen)}
170
+ isExpanded={isThemeSelectOpen}
171
+ icon={getThemeIcon(themeMode)}
172
+ aria-label={`Theme selection, current: ${getThemeDisplayText(themeMode)}`}
173
+ >
174
+ {getThemeDisplayText(themeMode)}
175
+ </MenuToggle>
176
+ )}
177
+ shouldFocusToggleOnSelect
178
+ >
179
+ <SelectList>
180
+ <SelectOption
181
+ value={THEME_MODES.SYSTEM}
182
+ icon={<DesktopIcon />}
183
+ description="Follow system preference"
184
+ >
185
+ System
186
+ </SelectOption>
187
+ <SelectOption
188
+ value={THEME_MODES.LIGHT}
189
+ icon={<SunIcon />}
190
+ description="Always use light mode"
191
+ >
192
+ Light
193
+ </SelectOption>
194
+ <SelectOption
195
+ value={THEME_MODES.DARK}
196
+ icon={<MoonIcon />}
197
+ description="Always use dark mode"
198
+ >
199
+ Dark
200
+ </SelectOption>
201
+ </SelectList>
202
+ </Select>
203
+ </ToolbarItem>
204
+ )}
150
205
  {hasVersionSwitcher && (
151
206
  <ToolbarItem>
152
207
  <Dropdown
@@ -245,7 +300,8 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
245
300
 
246
301
  const [versions, setVersions] = useState({ ...staticVersions });
247
302
  const [isRTL, setIsRTL] = useState(false);
248
- const [isDarkTheme, setIsDarkTheme] = React.useState(false);
303
+
304
+ const { themeMode, setThemeMode, resolvedTheme, THEME_MODES } = useTheme();
249
305
 
250
306
  useEffect(() => {
251
307
  if (typeof window === 'undefined') {
@@ -338,8 +394,9 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
338
394
  topNavItems={topNavItems}
339
395
  isRTL={isRTL}
340
396
  setIsRTL={setIsRTL}
341
- isDarkTheme={isDarkTheme}
342
- setIsDarkTheme={setIsDarkTheme}
397
+ themeMode={themeMode}
398
+ setThemeMode={setThemeMode}
399
+ THEME_MODES={THEME_MODES}
343
400
  />
344
401
  )}
345
402
  </MastheadContent>
@@ -360,7 +417,7 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
360
417
  defaultManagedSidebarIsOpen={navOpenProp}
361
418
  >
362
419
  {children}
363
- {process.env.hasFooter && <Footer isDarkTheme={isDarkTheme} />}
420
+ {process.env.hasFooter && <Footer isDarkTheme={resolvedTheme === 'dark'} />}
364
421
  </Page>
365
422
  <div id="ws-page-banners">{hasGdprBanner && <GdprBanner />}</div>
366
423
  </RtlContext.Provider>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@patternfly/documentation-framework",
3
3
  "description": "A framework to build documentation for PatternFly.",
4
- "version": "6.10.32",
4
+ "version": "6.11.0",
5
5
  "author": "Red Hat",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "@babel/preset-env": "^7.24.3",
13
13
  "@babel/preset-react": "^7.24.1",
14
14
  "@mdx-js/util": "1.6.16",
15
- "@patternfly/ast-helpers": "^1.4.0-alpha.218",
15
+ "@patternfly/ast-helpers": "^1.4.0-alpha.220",
16
16
  "@reach/router": "npm:@gatsbyjs/reach-router@1.3.9",
17
17
  "autoprefixer": "9.8.6",
18
18
  "babel-loader": "^9.1.3",
@@ -79,5 +79,5 @@
79
79
  "react": "^17.0.0 || ^18.0.0",
80
80
  "react-dom": "^17.0.0 || ^18.0.0"
81
81
  },
82
- "gitHead": "c268f0f5cde082e593c2c62f8aa0e1b2bdafe8a5"
82
+ "gitHead": "3481a949f8c8f17ed8d74faba651b339acae54df"
83
83
  }