diff --git a/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx b/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx
index 72b1bdadb3393..61bd46179be61 100644
--- a/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx
+++ b/src/platform/packages/shared/kbn-user-profile-components/src/hooks/use_update_user_profile.tsx
@@ -28,7 +28,7 @@ interface Props {
};
/** Predicate to indicate if the update requires a page reload */
pageReloadChecker?: (
- previsous: UserProfileData | null | undefined,
+ previous: UserProfileData | null | undefined,
next: UserProfileData
) => boolean;
}
diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.test.tsx b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.test.tsx
new file mode 100644
index 0000000000000..56a90203fae13
--- /dev/null
+++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.test.tsx
@@ -0,0 +1,203 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { fireEvent, render } from '@testing-library/react';
+import React from 'react';
+
+import { coreMock } from '@kbn/core/public/mocks';
+import { useUpdateUserProfile } from '@kbn/user-profile-components';
+import { AppearanceModal } from './appearance_modal';
+
+jest.mock('@kbn/user-profile-components', () => {
+ const original = jest.requireActual('@kbn/user-profile-components');
+ return {
+ ...original,
+ useUpdateUserProfile: jest.fn().mockImplementation(() => ({
+ userProfileData: {
+ userSettings: {
+ darkMode: 'light',
+ contrastMode: 'standard',
+ },
+ },
+ isLoading: false,
+ update: jest.fn(),
+ userProfileLoaded: true,
+ })),
+ };
+});
+
+jest.mock('./values_group', () => ({
+ ValuesGroup: jest.fn().mockImplementation(({ title, selectedValue, onChange }) => (
+
+
{title}
+
+
+
+
+
Selected: {selectedValue}
+
+ )),
+}));
+
+describe('AppearanceModal', () => {
+ const closeModal = jest.fn();
+ const uiSettingsClient = coreMock.createStart().uiSettings;
+ let updateMock: jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ updateMock = jest.fn();
+ (useUpdateUserProfile as jest.Mock).mockImplementation(() => ({
+ userProfileData: {
+ userSettings: {
+ darkMode: 'light',
+ contrastMode: 'standard',
+ },
+ },
+ isLoading: false,
+ update: updateMock,
+ userProfileLoaded: true,
+ }));
+ });
+
+ it('renders both color mode and contrast mode components', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Check that both the color mode and contrast mode components are rendered
+ expect(getByTestId('values-group-Color mode')).toBeInTheDocument();
+ expect(getByTestId('values-group-Interface contrast')).toBeInTheDocument();
+ });
+
+ it('updates color mode when user changes selection', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Click the dark option in the color mode group
+ fireEvent.click(getByTestId('option-dark-Color mode'));
+
+ // Check that the onChange handler was called with colorMode: dark
+ expect(getByTestId('values-group-Color mode')).toHaveTextContent('Selected: dark');
+ });
+
+ it('updates contrast mode when user changes selection', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Click the high contrast option
+ fireEvent.click(getByTestId('option-high-Interface contrast'));
+
+ // Check that the contrast mode was updated
+ expect(getByTestId('values-group-Interface contrast')).toHaveTextContent('Selected: high');
+ });
+
+ it('saves both color mode and contrast mode when saving changes', async () => {
+ const { getByText, getByTestId } = render(
+
+ );
+
+ // Change color mode to dark
+ fireEvent.click(getByTestId('option-dark-Color mode'));
+
+ // Change contrast mode to high
+ fireEvent.click(getByTestId('option-high-Interface contrast'));
+
+ // Click save button
+ fireEvent.click(getByText('Save changes'));
+
+ // Check that the update function was called with both settings
+ expect(updateMock).toHaveBeenCalledWith({
+ userSettings: {
+ darkMode: 'dark',
+ contrastMode: 'high',
+ },
+ });
+
+ // Modal should be closed
+ expect(closeModal).toHaveBeenCalled();
+ });
+
+ it('discards changes when clicking discard button', () => {
+ const { getByText, getByTestId } = render(
+
+ );
+
+ // Change color mode to dark
+ fireEvent.click(getByTestId('option-dark-Color mode'));
+
+ // Change contrast mode to high
+ fireEvent.click(getByTestId('option-high-Interface contrast'));
+
+ // Click discard button
+ fireEvent.click(getByText('Discard'));
+
+ // Check that the update function was not called
+ expect(updateMock).not.toHaveBeenCalled();
+
+ // Modal should be closed
+ expect(closeModal).toHaveBeenCalled();
+ });
+
+ it('does not update settings if no changes were made', () => {
+ const { getByText } = render(
+
+ );
+
+ // Click save button without making changes
+ fireEvent.click(getByText('Save changes'));
+
+ // Update should not be called since no changes were made
+ expect(updateMock).not.toHaveBeenCalled();
+
+ // Modal should still be closed
+ expect(closeModal).toHaveBeenCalled();
+ });
+
+ it('shows contrast options even in serverless mode', () => {
+ const { getByTestId } = render(
+
+ );
+
+ // Contrast mode should still be present in serverless mode
+ expect(getByTestId('values-group-Interface contrast')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx
index b29f15a26c8c3..55a734f535cba 100644
--- a/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx
+++ b/x-pack/platform/plugins/private/cloud_integrations/cloud_links/public/maybe_add_cloud_links/appearance_selector/appearance_modal.tsx
@@ -20,7 +20,10 @@ import {
import { i18n } from '@kbn/i18n';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
-import type { DarkModeValue as ColorMode } from '@kbn/user-profile-components';
+import type {
+ DarkModeValue as ColorMode,
+ ContrastModeValue as ContrastMode,
+} from '@kbn/user-profile-components';
import { type Value, ValuesGroup } from './values_group';
import { useAppearance } from './use_appearance_hook';
@@ -73,12 +76,125 @@ interface Props {
isServerless: boolean;
}
+const ColorModeGroup: FC<{
+ isServerless: boolean;
+ colorMode: ColorMode;
+ onChange: ({ colorMode }: { colorMode: ColorMode }, updateUserProfile: boolean) => void;
+}> = ({ isServerless, colorMode, onChange }) => {
+ return (
+ <>
+
+ title={i18n.translate('xpack.cloudLinks.userMenuLinks.appearanceModalColorModeTitle', {
+ defaultMessage: 'Color mode',
+ })}
+ values={
+ isServerless
+ ? colorModeOptions.filter(({ id }) => id !== 'space_default')
+ : colorModeOptions
+ }
+ selectedValue={colorMode}
+ onChange={(id) => {
+ onChange({ colorMode: id }, false);
+ }}
+ ariaLabel={i18n.translate(
+ 'xpack.cloudLinks.userMenuLinks.appearanceModalColorModeAriaLabel',
+ {
+ defaultMessage: 'Appearance color mode',
+ }
+ )}
+ />
+
+ {colorMode === 'space_default' && (
+ <>
+
+
+
+ {i18n.translate(
+ 'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultDescr',
+ {
+ defaultMessage:
+ 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.',
+ }
+ )}
+
- {i18n.translate(
- 'xpack.cloudLinks.userMenuLinks.appearanceModalDeprecatedSpaceDefaultDescr',
- {
- defaultMessage:
- 'All users with the Space default color mode enabled will be automatically transitioned to the System color mode.',
- }
- )}
-