- High contrast{' '}
- e.stopPropagation()}
- headerContent={'Under development'}
- headerComponent="h1"
- bodyContent={
- 'We are still working to add high contrast support across all PatternFly components and extensions. This beta allows you to preview our progress.'
- }
- footerContent={
- }
- component="a"
- isInline
- variant="link"
- href="/design-foundations/theming"
- target="_blank"
- >
- Learn more
-
- }
- aria-label="More info about high contrast"
- appendTo={() => document.body}
- >
- } aria-label="High contrast help" />
- {' '}
-
-
+
+ Contrast mode
);
};
export const ThemeSelector = ({ id }) => {
+ const { mode: themeVariant, setMode: setThemeVariant, modes: themeVariantModes } = useTheme(THEME_TYPES.THEME_VARIANT);
const { mode: themeMode, setMode: setThemeMode, modes: colorModes } = useTheme(THEME_TYPES.COLOR);
const {
- mode: highContrastMode,
- setMode: setHighContrastMode,
- modes: highContrastModes
- } = useTheme(THEME_TYPES.HIGH_CONTRAST);
+ mode: contrastMode,
+ setMode: setContrastMode,
+ modes: contrastModes
+ } = useTheme(THEME_TYPES.CONTRAST);
const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
- const handleThemeChange = (_event, selectedMode) => {
- setThemeMode(selectedMode);
- setIsThemeSelectOpen(false);
+ const handleThemeVariantChange = (evt) => {
+ setThemeVariant(evt.currentTarget.id);
};
- const handleHighContrastThemeSelection = (evt) => {
- setHighContrastMode(evt.currentTarget.id);
+ const handleThemeChange = (evt) => {
+ setThemeMode(evt.currentTarget.id);
+ };
+
+ const handleContrastModeChange = (evt) => {
+ setContrastMode(evt.currentTarget.id);
};
const getThemeDisplayText = (mode) => {
@@ -126,6 +102,9 @@ export const ThemeSelector = ({ id }) => {
};
const getThemeIcon = (mode) => {
+ if (!colorModes) {
+ return ;
+ }
switch (mode) {
case colorModes.LIGHT:
return SunIcon;
@@ -134,7 +113,7 @@ export const ThemeSelector = ({ id }) => {
case colorModes.SYSTEM:
return DesktopIcon;
default:
- return ;
+ return DesktopIcon; // Default to system icon
}
};
@@ -142,8 +121,6 @@ export const ThemeSelector = ({ id }) => {
);
};
diff --git a/packages/documentation-framework/hooks/useTheme.js b/packages/documentation-framework/hooks/useTheme.js
index afd7c0ae68..03eb9f72f3 100644
--- a/packages/documentation-framework/hooks/useTheme.js
+++ b/packages/documentation-framework/hooks/useTheme.js
@@ -6,15 +6,22 @@ const COLOR_MODES = {
DARK: 'dark'
};
-const HIGH_CONTRAST_MODES = {
- SYSTEM: 'high-contrast-system',
- ON: 'high-contrast-on',
- OFF: 'high-contrast-off'
+const CONTRAST_MODES = {
+ SYSTEM: 'contrast-system',
+ DEFAULT: 'contrast-default',
+ HIGH_CONTRAST: 'contrast-high',
+ GLASS: 'contrast-glass'
+};
+
+const THEME_VARIANT_MODES = {
+ DEFAULT: 'theme-default',
+ UNIFIED: 'theme-unified'
};
export const THEME_TYPES = {
COLOR: 'color',
- HIGH_CONTRAST: 'high-contrast'
+ CONTRAST: 'contrast',
+ THEME_VARIANT: 'theme-variant'
};
class ThemeManager {
@@ -61,37 +68,105 @@ class ThemeManager {
return this.defaultMode;
}
- addClass() {
+ getHtmlElement() {
if (!this.isBrowser) {
- return;
+ return null;
+ }
+ return document.querySelector('html');
+ }
+
+ addClass() {
+ const htmlElement = this.getHtmlElement();
+ if (htmlElement && !htmlElement.classList.contains(this.cssClass)) {
+ htmlElement.classList.add(this.cssClass);
}
- document.querySelector('html').classList.add(this.cssClass);
}
removeClass() {
+ const htmlElement = this.getHtmlElement();
+ if (htmlElement && htmlElement.classList.contains(this.cssClass)) {
+ htmlElement.classList.remove(this.cssClass);
+ }
+ }
+
+ updateClass() {
if (!this.isBrowser) {
return;
}
- document.querySelector('html').classList.remove(this.cssClass);
+
+ // ALWAYS read from localStorage to ensure we have the correct mode for THIS theme
+ const storedMode = this.getStoredValue();
+
+ // Validate that the stored mode is valid for this theme
+ const validModes = Object.values(this.modes);
+ if (!validModes.includes(storedMode)) {
+ console.error(`[${this.storageKey}] Invalid stored mode "${storedMode}". Valid modes:`, validModes);
+ return;
+ }
+
+ const shouldHaveClass = storedMode === this.modes.SYSTEM
+ ? this.resolve() === this.classEnabledMode
+ : storedMode === this.classEnabledMode;
+
+ if (shouldHaveClass) {
+ this.addClass();
+ } else {
+ this.removeClass();
+ }
}
+}
- updateClass(mode) {
+class ContrastThemeManager extends ThemeManager {
+ constructor({ storageKey, modes, defaultMode, mediaQueryString }) {
+ super({
+ storageKey,
+ modes,
+ defaultMode,
+ cssClass: 'pf-v6-theme-high-contrast',
+ classEnabledMode: modes.HIGH_CONTRAST,
+ mediaQueryString
+ });
+ this.glassClass = 'pf-v6-theme-glass';
+ }
+
+ updateClass() {
if (!this.isBrowser) {
return;
}
- if (mode === this.modes.SYSTEM) {
- if (this.resolve() === this.classEnabledMode) {
- this.addClass();
- } else {
- this.removeClass();
- }
- } else {
- if (mode === this.classEnabledMode) {
- this.addClass();
- } else {
- this.removeClass();
- }
+ const htmlElement = this.getHtmlElement();
+ if (!htmlElement) {
+ return;
+ }
+
+ // ALWAYS read from localStorage to ensure we have the correct mode for THIS theme
+ const storedMode = this.getStoredValue();
+
+ // Determine which class should be applied based on stored mode
+ let shouldHaveHighContrast = false;
+ let shouldHaveGlass = false;
+
+ if (storedMode === this.modes.SYSTEM) {
+ shouldHaveHighContrast = window.matchMedia(this.mediaQueryString).matches;
+ } else if (storedMode === this.modes.HIGH_CONTRAST) {
+ shouldHaveHighContrast = true;
+ } else if (storedMode === this.modes.GLASS) {
+ shouldHaveGlass = true;
+ }
+ // DEFAULT mode: both false
+
+ // Apply high contrast class
+ if (shouldHaveHighContrast && !htmlElement.classList.contains(this.cssClass)) {
+ htmlElement.classList.add(this.cssClass);
+ } else if (!shouldHaveHighContrast && htmlElement.classList.contains(this.cssClass)) {
+ htmlElement.classList.remove(this.cssClass);
+ }
+
+ // Apply glass class
+ if (shouldHaveGlass && !htmlElement.classList.contains(this.glassClass)) {
+ htmlElement.classList.add(this.glassClass);
+ } else if (!shouldHaveGlass && htmlElement.classList.contains(this.glassClass)) {
+ htmlElement.classList.remove(this.glassClass);
}
}
}
@@ -111,26 +186,34 @@ const colorThemeManager = new ThemeManager({
mediaQueryString: '(prefers-color-scheme: dark)'
});
-const highContrastThemeManager = new ThemeManager({
- storageKey: 'high-contrast-preference',
- modes: HIGH_CONTRAST_MODES,
- defaultMode: HIGH_CONTRAST_MODES.SYSTEM,
- cssClass: 'pf-v6-theme-high-contrast',
- classEnabledMode: HIGH_CONTRAST_MODES.ON,
+const themeVariantManager = new ThemeManager({
+ storageKey: 'theme-variant-preference',
+ modes: THEME_VARIANT_MODES,
+ defaultMode: THEME_VARIANT_MODES.DEFAULT,
+ cssClass: 'pf-v6-theme-unified',
+ classEnabledMode: THEME_VARIANT_MODES.UNIFIED,
+ mediaQueryString: '(prefers-color-scheme: dark)' // Not used for variant, but required
+});
+
+const contrastThemeManager = new ContrastThemeManager({
+ storageKey: 'contrast-preference',
+ modes: CONTRAST_MODES,
+ defaultMode: CONTRAST_MODES.SYSTEM,
mediaQueryString: '(prefers-contrast: more)'
});
registerThemeManager(THEME_TYPES.COLOR, colorThemeManager);
-registerThemeManager(THEME_TYPES.HIGH_CONTRAST, highContrastThemeManager);
+registerThemeManager(THEME_TYPES.THEME_VARIANT, themeVariantManager);
+registerThemeManager(THEME_TYPES.CONTRAST, contrastThemeManager);
/**
* Unified theme hook that accepts a theme type parameter
- * @param {string} themeType - The type of theme to manage (THEME_TYPES.COLOR, THEME_TYPES.HIGH_CONTRAST, instantiate and register new themes above as needed)
+ * @param {string} themeType - The type of theme to manage (THEME_TYPES.COLOR, THEME_TYPES.CONTRAST, THEME_TYPES.THEME_VARIANT)
* @returns {Object} Theme state and controls specific to the theme type
*/
export const useTheme = (themeType) => {
if (!themeType) {
- throw new Error('useTheme requires a theme type parameter. Use THEME_TYPES.COLOR or THEME_TYPES.HIGH_CONTRAST');
+ throw new Error('useTheme requires a theme type parameter. Use THEME_TYPES.COLOR, THEME_TYPES.CONTRAST, or THEME_TYPES.THEME_VARIANT');
}
const theme = themeRegistry.get(themeType);
@@ -143,16 +226,34 @@ export const useTheme = (themeType) => {
const [resolvedTheme, setResolvedTheme] = useState(theme.resolve());
useEffect(() => {
+ // Verify mode is valid for this theme
+ const validModes = Object.values(theme.modes);
+ if (!validModes.includes(mode)) {
+ console.error(`Invalid mode "${mode}" for theme ${theme.storageKey}. Valid modes:`, validModes);
+ return;
+ }
+
theme.setStoredValue(mode);
- theme.updateClass(mode);
- }, [theme, mode, resolvedTheme]);
+ theme.updateClass();
+ }, [theme, mode]);
- const handlePreferenceChange = () => {
- setResolvedTheme(theme.resolve());
- };
- const mediaQuery = theme.getMediaQuery();
+ useEffect(() => {
+ // Only update class when system preference changes AND mode is SYSTEM
+ if (mode === theme.modes.SYSTEM) {
+ theme.updateClass();
+ }
+ }, [theme, mode, resolvedTheme]);
useEffect(() => {
+ const handlePreferenceChange = () => {
+ setResolvedTheme(theme.resolve());
+ };
+
+ const mediaQuery = theme.getMediaQuery();
+
+ if (!mediaQuery) {
+ return;
+ }
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handlePreferenceChange);
return () => {
@@ -164,7 +265,7 @@ export const useTheme = (themeType) => {
mediaQuery.removeListener(handlePreferenceChange);
};
}
- }, [mediaQuery]);
+ }, []);
return {
mode,
diff --git a/packages/documentation-framework/versions.json b/packages/documentation-framework/versions.json
index bf3da485b6..64e1d05e8c 100644
--- a/packages/documentation-framework/versions.json
+++ b/packages/documentation-framework/versions.json
@@ -12,7 +12,7 @@
"@patternfly/react-component-groups": "6.4.0-prerelease.12",
"@patternfly/react-core": "6.5.0-prerelease.24",
"@patternfly/react-drag-drop": "6.5.0-prerelease.24",
- "@patternfly/react-icons": "6.5.0-prerelease.11",
+ "@patternfly/react-icons": "6.5.0-prerelease.12",
"@patternfly/react-log-viewer": "6.3.0",
"@patternfly/react-styles": "6.5.0-prerelease.9",
"@patternfly/react-table": "6.5.0-prerelease.24",
diff --git a/packages/documentation-site/package.json b/packages/documentation-site/package.json
index ac00f38f7b..4d4d83c9e1 100644
--- a/packages/documentation-site/package.json
+++ b/packages/documentation-site/package.json
@@ -28,7 +28,7 @@
"@patternfly/react-console": "6.1.0",
"@patternfly/react-data-view": "6.4.0-prerelease.8",
"@patternfly/react-docs": "7.5.0-prerelease.26",
- "@patternfly/react-icons": "6.5.0-prerelease.11",
+ "@patternfly/react-icons": "6.5.0-prerelease.12",
"@patternfly/react-log-viewer": "6.3.0",
"@patternfly/react-topology": "6.5.0-prerelease.3",
"@patternfly/react-user-feedback": "6.2.0",
diff --git a/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/Animations.md b/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/Animations.md
index a6e293384a..e448c194ed 100644
--- a/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/Animations.md
+++ b/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/Animations.md
@@ -6,40 +6,48 @@ source: demo
import { Fragment, useRef, useState, useEffect, useCallback } from 'react';
-import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
-import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
import BarsIcon from '@patternfly/react-icons/dist/js/icons/bars-icon';
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon';
-import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
-import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg';
-import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import pfLogo from '@patternfly/react-core/src/demos/assets/PF-HorizontalLogo-Color.svg';
import MultiContentCard from "@patternfly/react-component-groups/dist/dynamic/MultiContentCard";
-import { ArrowRightIcon, LockIcon, PortIcon, CubeIcon, AutomationIcon, ExclamationCircleIcon, CheckCircleIcon, ExclamationTriangleIcon, HamburgerIcon, TimesIcon} from '@patternfly/react-icons';
import UnpluggedIcon from '@patternfly/react-icons/dist/esm/icons/unplugged-icon';
import l_gallery_GridTemplateColumns_min from '@patternfly/react-tokens/dist/esm/l_gallery_GridTemplateColumns_min';
import {applicationsData} from './examples/ResourceTableData.jsx';
import SkeletonTable from "@patternfly/react-component-groups/dist/dynamic/SkeletonTable";
import t_global_text_color_subtle from '@patternfly/react-tokens/dist/esm/t_global_text_color_subtle';
import { AnimationsOverview } from './AnimationsOverview';
-import { AnimationsNotificationsDrawer } from './AnimationsNotificationsDrawer';
import { AnimationsHeaderToolbar } from './AnimationsHeaderToolbar';
import { AnimationsStartTourModal } from './AnimationsStartTourModal';
import { AnimationsEndTourModal } from './AnimationsEndTourModal';
import { AnimationsCreateDatabaseForm } from './AnimationsCreateDatabaseForm';
import { GuidedTourProvider, useGuidedTour } from './GuidedTourContext';
import BoltIcon from '@patternfly/react-icons/dist/esm/icons/bolt-icon';
-import { Table, Thead, Tbody, Tr, Th, Td, ExpandableRowContent } from '@patternfly/react-table';
+import { Table, Thead, Tbody, Tr, Th, Td, ExpandableRowContent, TableText } from '@patternfly/react-table';
import PendingIcon from '@patternfly/react-icons/dist/esm/icons/pending-icon';
import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
import InfoIcon from '@patternfly/react-icons/dist/esm/icons/info-icon';
-import ResourcesFullIcon from '@patternfly/react-icons/dist/esm/icons/resources-full-icon';
import openshiftLogo from './Summit-collage-deploying-openshift-product-icon-RH.png'
import emptyStateLogo from './Summit-collage-hybrid-cloud-dark-RH.png'
+import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-error-icon';
+import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-check-circle-icon';
+import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-warning-icon';
+import ResourcesFullIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-resources-full-icon';
+import PortIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-port-icon';
+import CogIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-settings-icon';
+import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-question-mark-circle-icon';
+import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ellipsis-vertical-icon';
+import SearchIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-search-icon';
+import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-arrow-right-icon';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-close-icon';
+import './examples/glass.css';
+
+import { CodeEditor, Language } from '@patternfly/react-code-editor';
+import { rows, columns } from '@patternfly/react-table/dist/esm/demos/sampleData';
+
## Demo
Our components can now use motion to provide clear visual feedback to users, improving engagement and usability. To see our new animations in motion, take this interactive tour, which guides you through a UI that includes animated alerts, icons, expansion, and more.
diff --git a/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/AnimationsCreateDatabaseForm.tsx b/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/AnimationsCreateDatabaseForm.tsx
index 88516ebe01..9835194814 100644
--- a/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/AnimationsCreateDatabaseForm.tsx
+++ b/packages/documentation-site/patternfly-docs/content/foundations-and-styles/styles/motion/Animations/AnimationsCreateDatabaseForm.tsx
@@ -14,7 +14,6 @@ import {
Popover,
ActionGroup
} from '@patternfly/react-core';
-import { useGuidedTour } from './GuidedTourContext';
interface Props {
onClose: () => void;
@@ -29,7 +28,6 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose
// Submit state variables
const [isSuccess, setIsSuccess] = useState(false);
const [actionCompleted, setActionCompleted] = useState(false);
- const { renderTourStepElement } = useGuidedTour();
const labelHelpRef = useRef(null);
@@ -90,8 +88,7 @@ export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose
setIsEmailValid('default');
};
- return renderTourStepElement(
- 'validationErrors',
+ return (