Skip to content
110 changes: 95 additions & 15 deletions packages/documentation-framework/components/example/example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useState, useCallback } from 'react';
import { useLocation } from '@reach/router';
import {
Button,
Expand All @@ -8,6 +8,10 @@ import {
debounce,
Label,
Switch,
Select,
SelectOption,
SelectList,
MenuToggle,
Tooltip,
Stack,
StackItem
Expand All @@ -19,6 +23,9 @@ import * as reactTableModule from '@patternfly/react-table';
import * as reactTableDeprecatedModule from '@patternfly/react-table/deprecated';
import { css } from '@patternfly/react-styles';
import { getParameters } from 'codesandbox/lib/api/define';
import SunIcon from '@patternfly/react-icons/dist/esm/icons/sun-icon';
import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon';
import DesktopIcon from '@patternfly/react-icons/dist/esm/icons/desktop-icon';
import { ExampleToolbar } from './exampleToolbar.jsx';
import { AutoLinkHeader } from '../autoLinkHeader/autoLinkHeader';
import {
Expand All @@ -32,9 +39,93 @@ import {
import { convertToReactComponent } from '@patternfly/ast-helpers';
import missingThumbnail from './missing-thumbnail.jpg';
import { RtlContext } from '../../layouts';
import { useTheme } from '../../hooks/useTheme';

const errorComponent = (err) => <pre>{err.toString()}</pre>;

// Full-screen theme selector component using shared theme hook
const FullScreenThemeSelector = () => {
const { themeMode, setThemeMode, THEME_MODES } = useTheme();
const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);

const handleThemeChange = (_event, selectedMode) => {
setThemeMode(selectedMode);
setIsThemeSelectOpen(false);
};

const getThemeDisplayText = (mode) => {
switch (mode) {
case THEME_MODES.SYSTEM:
return 'System';
case THEME_MODES.LIGHT:
return 'Light';
case THEME_MODES.DARK:
return 'Dark';
default:
return 'System';
}
};

const getThemeIcon = (mode) => {
switch (mode) {
case THEME_MODES.SYSTEM:
return <DesktopIcon />;
case THEME_MODES.LIGHT:
return <SunIcon />;
case THEME_MODES.DARK:
return <MoonIcon />;
default:
return <DesktopIcon />;
}
};

return (
<Select
id="ws-example-theme-select"
isOpen={isThemeSelectOpen}
selected={themeMode}
onSelect={handleThemeChange}
onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
toggle={(toggleRef) => (
<MenuToggle
ref={toggleRef}
onClick={() => setIsThemeSelectOpen(!isThemeSelectOpen)}
isExpanded={isThemeSelectOpen}
icon={getThemeIcon(themeMode)}
aria-label="Theme selection"
>
{getThemeDisplayText(themeMode)}
</MenuToggle>
)}
shouldFocusToggleOnSelect
>
<SelectList>
<SelectOption
value={THEME_MODES.SYSTEM}
icon={<DesktopIcon />}
description="Follow system preference"
>
System
</SelectOption>
<SelectOption
value={THEME_MODES.LIGHT}
icon={<SunIcon />}
description="Always use light theme"
>
Light
</SelectOption>
<SelectOption
value={THEME_MODES.DARK}
icon={<MoonIcon />}
description="Always use dark theme"
>
Dark
</SelectOption>
</SelectList>
</Select>
);
};

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -199,21 +290,10 @@ export const Example = ({
{(hasDarkThemeSwitcher || hasRTLSwitcher) && (
<Flex
direction={{ default: 'column' }}
gap={{ default: 'gapLg' }}
className="ws-full-page-utils pf-v6-m-dir-ltr "
gap={{ default: 'gapMd' }}
className="ws-full-page-utils pf-v6-m-dir-ltr"
>
{hasDarkThemeSwitcher && (
<Switch
id="ws-example-theme-switch"
label="Dark theme"
defaultChecked={false}
onChange={() =>
document
.querySelector('html')
.classList.toggle('pf-v6-theme-dark')
}
/>
)}
{hasDarkThemeSwitcher && <FullScreenThemeSelector />}
{hasRTLSwitcher && (
<Switch
id="ws-example-rtl-switch"
Expand Down
93 changes: 93 additions & 0 deletions packages/documentation-framework/hooks/useTheme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useState, useEffect, useCallback } from 'react';

const THEME_MODES = {
SYSTEM: 'system',
LIGHT: 'light',
DARK: 'dark'
};

const THEME_STORAGE_KEY = 'theme-preference';
const DARK_MODE_CLASS = 'pf-v6-theme-dark';

export const useTheme = () => {
const getStoredThemeMode = () => {
if (typeof window === 'undefined' || !window.localStorage) return null;
return localStorage.getItem(THEME_STORAGE_KEY);
};

const setStoredThemeMode = (mode) => {
if (typeof window === 'undefined' || !window.localStorage) return;
localStorage.setItem(THEME_STORAGE_KEY, mode);
};

const getResolvedTheme = (mode) => {
if (typeof window === 'undefined') return 'light';
if (mode === THEME_MODES.SYSTEM) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return mode;
};

const updateThemeClass = (resolvedTheme) => {
if (typeof window === 'undefined') return;
const htmlElement = document.querySelector('html');
if (resolvedTheme === 'dark') {
htmlElement.classList.add(DARK_MODE_CLASS);
} else {
htmlElement.classList.remove(DARK_MODE_CLASS);
}
};

const [themeMode, setThemeModeState] = useState(() => {
const stored = getStoredThemeMode();
return stored && Object.values(THEME_MODES).includes(stored) ? stored : THEME_MODES.SYSTEM;
});

const [resolvedTheme, setResolvedTheme] = useState(() => getResolvedTheme(themeMode));

const setThemeMode = useCallback((newMode) => {
setThemeModeState(newMode);
setStoredThemeMode(newMode);

const newResolvedTheme = getResolvedTheme(newMode);
setResolvedTheme(newResolvedTheme);
updateThemeClass(newResolvedTheme);
}, []);

// Listen for system preference changes
useEffect(() => {
if (typeof window === 'undefined') return;

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleSystemThemeChange = (e) => {
if (themeMode === THEME_MODES.SYSTEM) {
const newSystemTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(newSystemTheme);
updateThemeClass(newSystemTheme);
}
};

mediaQuery.addEventListener('change', handleSystemThemeChange);

return () => {
mediaQuery.removeEventListener('change', handleSystemThemeChange);
};
}, [themeMode]);

// Initial theme application
useEffect(() => {
const initialResolvedTheme = getResolvedTheme(themeMode);
setResolvedTheme(initialResolvedTheme);
updateThemeClass(initialResolvedTheme);
}, [themeMode]);

return {
themeMode,
setThemeMode,
resolvedTheme,
THEME_MODES
};
};

export { THEME_MODES };
Loading
Loading