diff --git a/vnext/ReactUWP/Modules/AppThemeModuleUwp.cpp b/vnext/ReactUWP/Modules/AppThemeModuleUwp.cpp index 56afded3dd4..41ecdda0eae 100644 --- a/vnext/ReactUWP/Modules/AppThemeModuleUwp.cpp +++ b/vnext/ReactUWP/Modules/AppThemeModuleUwp.cpp @@ -13,6 +13,7 @@ namespace winrt { using namespace Windows::UI::Xaml; + using namespace Windows::UI::ViewManagement; } namespace react { namespace uwp { @@ -26,6 +27,17 @@ AppTheme::AppTheme(const std::shared_ptr& reactInstance, const s , m_queueThread(defaultQueueThread) { m_currentTheme = winrt::Application::Current().RequestedTheme(); + m_isHighContrast = m_accessibilitySettings.HighContrast(); + m_highContrastColors = getHighContrastColors(); + + m_highContrastChangedRevoker = m_accessibilitySettings.HighContrastChanged(winrt::auto_revoke, + [this](const auto&, const auto&) { + + folly::dynamic eventData = folly::dynamic::object("highContrastColors", getHighContrastColors()) + ("isHighContrast", getIsHighContrast()); + + fireEvent("highContrastChanged", std::move(eventData)); + }); m_colorValuesChangedRevoker = m_uiSettings.ColorValuesChanged(winrt::auto_revoke, [this](const auto&, const auto&) { @@ -50,6 +62,39 @@ const std::string AppTheme::getCurrentTheme() return m_currentTheme == winrt::ApplicationTheme::Light ? AppTheme::light : AppTheme::dark; } +bool AppTheme::getIsHighContrast() +{ + return m_accessibilitySettings.HighContrast();; +} + +// Returns the RBG values for the 8 relevant High Contrast elements. +folly::dynamic AppTheme::getHighContrastColors() { + winrt::Windows::UI::Color ButtonFaceColor = m_uiSettings.UIElementColor(winrt::UIElementType::ButtonFace); + winrt::Windows::UI::Color ButtonTextColor = m_uiSettings.UIElementColor(winrt::UIElementType::ButtonText); + winrt::Windows::UI::Color GrayTextColor = m_uiSettings.UIElementColor(winrt::UIElementType::GrayText); + winrt::Windows::UI::Color HighlightColor = m_uiSettings.UIElementColor(winrt::UIElementType::Highlight); + winrt::Windows::UI::Color HighlightTextColor = m_uiSettings.UIElementColor(winrt::UIElementType::HighlightText); + winrt::Windows::UI::Color HotlightColor = m_uiSettings.UIElementColor(winrt::UIElementType::Hotlight); + winrt::Windows::UI::Color WindowColor = m_uiSettings.UIElementColor(winrt::UIElementType::Window); + winrt::Windows::UI::Color WindowTextColor = m_uiSettings.UIElementColor(winrt::UIElementType::WindowText); + + folly::dynamic rbgValues = folly::dynamic::object("ButtonFaceColor", formatRGB(ButtonFaceColor)) + ("ButtonTextColor", formatRGB(ButtonTextColor)) + ("GrayTextColor", formatRGB(GrayTextColor)) + ("HighlightColor", formatRGB(HighlightColor)) + ("HighlightTextColor", formatRGB(HighlightTextColor)) + ("HotlightColor", formatRGB(HotlightColor)) + ("WindowColor", formatRGB(WindowColor)) + ("WindowTextColor", formatRGB(WindowTextColor)); + return rbgValues; +} + +std::string AppTheme::formatRGB(winrt::Windows::UI::Color ElementColor) { + char colorChars[8]; + sprintf_s(colorChars, "#%02x%02x%02x", ElementColor.R, ElementColor.G, ElementColor.B); + return colorChars; +} + void AppTheme::fireEvent(std::string const& eventName, folly::dynamic&& eventData) { if (auto instance = m_wkReactInstance.lock()) @@ -57,5 +102,4 @@ void AppTheme::fireEvent(std::string const& eventName, folly::dynamic&& eventDat instance->CallJsFunction("RCTDeviceEventEmitter", "emit", folly::dynamic::array(eventName, std::move(eventData))); } } - } } // namespace react::uwp diff --git a/vnext/ReactUWP/Modules/AppThemeModuleUwp.h b/vnext/ReactUWP/Modules/AppThemeModuleUwp.h index 075ae73fe27..17cbb368e09 100644 --- a/vnext/ReactUWP/Modules/AppThemeModuleUwp.h +++ b/vnext/ReactUWP/Modules/AppThemeModuleUwp.h @@ -4,11 +4,8 @@ #pragma once #include - #include - #include - #include namespace react { namespace uwp { @@ -20,15 +17,23 @@ class AppTheme : public react::windows::AppTheme virtual ~AppTheme(); const std::string getCurrentTheme() override; + bool getIsHighContrast() override; private: + // High Contrast Color helper methods + folly::dynamic getHighContrastColors(); + std::string formatRGB(winrt::Windows::UI::Color ElementColor); + void fireEvent(std::string const& eventName, folly::dynamic&& eventData); std::weak_ptr m_wkReactInstance; std::shared_ptr m_queueThread; winrt::Windows::UI::Xaml::ApplicationTheme m_currentTheme{ winrt::Windows::UI::Xaml::ApplicationTheme::Light }; + bool m_isHighContrast; + folly::dynamic m_highContrastColors; winrt::Windows::UI::ViewManagement::AccessibilitySettings m_accessibilitySettings{ }; + winrt::Windows::UI::ViewManagement::AccessibilitySettings::HighContrastChanged_revoker m_highContrastChangedRevoker{ }; winrt::Windows::UI::ViewManagement::UISettings m_uiSettings{ }; winrt::Windows::UI::ViewManagement::UISettings::ColorValuesChanged_revoker m_colorValuesChangedRevoker{ }; }; diff --git a/vnext/ReactWindowsCore/Modules/AppThemeModule.cpp b/vnext/ReactWindowsCore/Modules/AppThemeModule.cpp index 92632babd1d..213a9b093db 100644 --- a/vnext/ReactWindowsCore/Modules/AppThemeModule.cpp +++ b/vnext/ReactWindowsCore/Modules/AppThemeModule.cpp @@ -19,6 +19,16 @@ const std::string AppTheme::getCurrentTheme() return AppTheme::light; } +bool AppTheme::getIsHighContrast() +{ + return false; +} + +folly::dynamic AppTheme::getHighContrastColors() +{ + return {}; +} + // // AppThemeModule // @@ -31,7 +41,9 @@ AppThemeModule::AppThemeModule(std::shared_ptr&& appTheme) auto AppThemeModule::getConstants() -> std::map { return { - { "initialAppTheme", folly::dynamic { m_appTheme->getCurrentTheme() } } + { "initialAppTheme", folly::dynamic { m_appTheme->getCurrentTheme() } }, + { "initialHighContrast", folly::dynamic { m_appTheme->getIsHighContrast() }}, + { "initialHighContrastColors", folly::dynamic {m_appTheme->getHighContrastColors()}} }; } diff --git a/vnext/ReactWindowsCore/Modules/AppThemeModule.h b/vnext/ReactWindowsCore/Modules/AppThemeModule.h index 8990961366b..da1a693457f 100644 --- a/vnext/ReactWindowsCore/Modules/AppThemeModule.h +++ b/vnext/ReactWindowsCore/Modules/AppThemeModule.h @@ -19,6 +19,8 @@ class AppTheme virtual ~AppTheme(); virtual const std::string getCurrentTheme(); + virtual bool getIsHighContrast(); + virtual folly::dynamic getHighContrastColors(); }; class AppThemeModule : public facebook::xplat::module::CxxModule @@ -29,7 +31,7 @@ class AppThemeModule : public facebook::xplat::module::CxxModule AppThemeModule(std::shared_ptr && appTheme); // CxxModule - std::string getName() override { return name; } + std::string getName() override { return name; }; auto getConstants() -> std::map override; auto getMethods() -> std::vector override; diff --git a/vnext/src/Libraries/Modules/AppTheme/AppTheme.ts b/vnext/src/Libraries/AppTheme/AppTheme.ts similarity index 60% rename from vnext/src/Libraries/Modules/AppTheme/AppTheme.ts rename to vnext/src/Libraries/AppTheme/AppTheme.ts index 078bd1d962c..c82142922a2 100644 --- a/vnext/src/Libraries/Modules/AppTheme/AppTheme.ts +++ b/vnext/src/Libraries/AppTheme/AppTheme.ts @@ -3,11 +3,20 @@ 'use strict'; import { NativeEventEmitter } from 'react-native'; +import { IHighContrastColors } from './AppThemeTypes'; class AppThemeModule extends NativeEventEmitter { get currentTheme(): string { return ''; } + + get isHighContrast(): boolean { + return false; + } + + get currentHighContrastColorValues(): IHighContrastColors { + return { } as IHighContrastColors; + } } export const AppTheme = new AppThemeModule(); diff --git a/vnext/src/Libraries/Modules/AppTheme/AppTheme.uwp.ts b/vnext/src/Libraries/AppTheme/AppTheme.uwp.ts similarity index 59% rename from vnext/src/Libraries/Modules/AppTheme/AppTheme.uwp.ts rename to vnext/src/Libraries/AppTheme/AppTheme.uwp.ts index 403d0a3ee1b..a66a1d48835 100644 --- a/vnext/src/Libraries/Modules/AppTheme/AppTheme.uwp.ts +++ b/vnext/src/Libraries/AppTheme/AppTheme.uwp.ts @@ -4,17 +4,27 @@ import { NativeEventEmitter, NativeModules } from 'react-native'; const MissingNativeEventEmitterShim = require('MissingNativeEventEmitterShim'); +import { IHighContrastColors, IHighContrastChangedEvent } from './AppThemeTypes'; const NativeAppTheme = NativeModules.RTCAppTheme; class AppThemeModule extends NativeEventEmitter { public isAvailable: boolean; + private _isHighContrast: boolean; private _currentTheme: string; + private _highContrastColors: IHighContrastColors; constructor() { super(NativeAppTheme); this.isAvailable = true; + this._highContrastColors = NativeAppTheme.initialHighContrastColors; + this._isHighContrast = NativeAppTheme.initialHighContrast; + this.addListener('highContrastChanged', (nativeEvent: IHighContrastChangedEvent) => { + this._isHighContrast = nativeEvent.isHighContrast; + this._highContrastColors = nativeEvent.highContrastColors; + }); + this._currentTheme = NativeAppTheme.initialAppTheme; this.addListener('appThemeChanged', ({currentTheme}:{currentTheme: string}) => { this._currentTheme = currentTheme; @@ -24,6 +34,14 @@ class AppThemeModule extends NativeEventEmitter { get currentTheme(): string { return this._currentTheme; } + + get isHighContrast(): boolean { + return this._isHighContrast; + } + + get currentHighContrastColors(): IHighContrastColors { + return this._highContrastColors; + } } // This module depends on the native `RCTAppTheme` module. If you don't include it, @@ -31,6 +49,8 @@ class AppThemeModule extends NativeEventEmitter { class MissingNativeAppThemeShim extends MissingNativeEventEmitterShim { public isAvailable = false; public currentTheme = ''; + public isHighContrast = false; + public currentHighContrastColors = {} as IHighContrastColors; } export const AppTheme = (NativeAppTheme ? new AppThemeModule() : new MissingNativeAppThemeShim()); diff --git a/vnext/src/Libraries/AppTheme/AppThemeTypes.ts b/vnext/src/Libraries/AppTheme/AppThemeTypes.ts new file mode 100644 index 00000000000..fd96afdb9e6 --- /dev/null +++ b/vnext/src/Libraries/AppTheme/AppThemeTypes.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface IHighContrastColors { + ButtonFaceColor: string; + ButtonTextColor: string; + GrayTextColor: string; + HighlightColor: string; + HighlightTextColor: string; + HotlightColor: string; + WindowColor: string; + WindowTextColor: string; +} + +export interface IAppThemeChangedEvent { + currentTheme: string; +} + +export interface IHighContrastChangedEvent { + isHighContrast: boolean; + highContrastColors: IHighContrastColors; +} \ No newline at end of file diff --git a/vnext/src/Libraries/Modules/AppTheme/__mocks__/AppTheme.uwp.ts b/vnext/src/Libraries/Modules/AppTheme/__mocks__/AppTheme.uwp.ts deleted file mode 100644 index 4661293b872..00000000000 --- a/vnext/src/Libraries/Modules/AppTheme/__mocks__/AppTheme.uwp.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed under the MIT License. -'use strict'; - -import { NativeEventEmitter } from 'react-native'; - -class AppThemeModule extends NativeEventEmitter { - get currentTheme(): string { - return 'light'; - } -} - -export const AppTheme = new AppThemeModule(); -export default AppTheme; \ No newline at end of file diff --git a/vnext/src/RNTester/AccessibilityExample.tsx b/vnext/src/RNTester/AccessibilityExample.tsx index b540c901d7e..847c34ea4ff 100644 --- a/vnext/src/RNTester/AccessibilityExample.tsx +++ b/vnext/src/RNTester/AccessibilityExample.tsx @@ -4,7 +4,9 @@ /* tslint:disable */ import React = require('react'); -import { FlatList, Text, TouchableHighlight, View } from 'react-native'; +import { FlatList, Text, TouchableHighlight, View, StyleSheet } from 'react-native'; +import { AppTheme } from '../../src/index.uwp'; +import { IAppThemeChangedEvent } from 'src/Libraries/AppTheme/AppThemeTypes'; class AccessibilityBaseExample extends React.Component { public render() { @@ -12,13 +14,13 @@ class AccessibilityBaseExample extends React.Component { The following has accessibilityLabel and accessibilityHint: The following has accessible and accessibilityLabel: @@ -27,6 +29,81 @@ class AccessibilityBaseExample extends React.Component { } } +class HighContrastExample extends React.Component { + state = { + isHighContrast: AppTheme.isHighContrast, + highContrastColorValues: AppTheme.currentHighContrastColors, + currentTheme: AppTheme.currentTheme + }; + + componentDidMount() { + AppTheme.addListener('highContrastChanged', this.onHighContrastChanged); + AppTheme.addListener('appThemeChanged', this.onAppThemeChanged); + } + + componenetWillUnmount() { + AppTheme.removeListener('highContrastChanged', this.onHighContrastChanged); + AppTheme.removeListener('appThemeChanged', this.onAppThemeChanged); + } + + // TODO: Make args props + onHighContrastChanged = (event: IAppThemeChangedEvent) => { + this.setState({isHighContrast : AppTheme.isHighContrast, + highContrastColorValues : AppTheme.currentHighContrastColors}); + }; + + onAppThemeChanged = (event: any) => { + this.setState({currentTheme : AppTheme.currentTheme}); + } + + public render() { + return ( + + The following has HighContrast Event awareness: + + isHighContrast: {this.state.isHighContrast ? 'True' : 'False'} + + + ButtonFace High Contrast Hex Value: {this.state.highContrastColorValues.ButtonFaceColor} + + + ButtonText High Contrast Color Hex Value: {this.state.highContrastColorValues.ButtonTextColor} + + + GrayText High Contrast Color Hex Value: {this.state.highContrastColorValues.GrayTextColor} + + + Highlight High Contrast Color Hex Value: {this.state.highContrastColorValues.HighlightColor} + + + HighlightText High Contrast Color Hex Value: {this.state.highContrastColorValues.HighlightTextColor} + + + Hotlight High Contrast Color Hex Value: {this.state.highContrastColorValues.HotlightColor} + + + Window High Contrast Color Hex Value: {this.state.highContrastColorValues.WindowColor} + + + WindowText High Contrast Color Hex Value: {this.state.highContrastColorValues.WindowTextColor} + + + ); + } + + styles = StyleSheet.create ({ + enabled: { + width: 250, + height: 50 + }, + disabled: { + width: 250, + height: 50, + backgroundColor: '#808080' + } + }); +} + class TouchableExamples extends React.Component<{}, any> { public state = { pressedCount: 0, @@ -37,7 +114,7 @@ class TouchableExamples extends React.Component<{}, any> { The following TouchableHighlight has accessibilityLabel, accessibilityHint, accessibilityRole, toolip: { > Blue - Pressed {this.state.pressedCount} times + Pressed {this.state.pressedCount} times ); } @@ -103,7 +180,7 @@ class AccessibilityStateExamples extends React.Component { private disablePress = () => { this.setState({viewDisabled: !this.state.viewDisabled}); - } +} private selectPress = (index: number) => { let tmp = this.state.itemsSelected; @@ -128,10 +205,16 @@ export const examples = [ return ; }, }, + { + title: 'HighContrast', + render: function(): JSX.Element { + return ; + }, + }, { title: 'States', render: function(): JSX.Element { return ; }, } -]; +]; \ No newline at end of file diff --git a/vnext/src/RNTester/RNTesterList.uwp.ts b/vnext/src/RNTester/RNTesterList.uwp.ts index 4ee911de007..fb436f6e3bc 100644 --- a/vnext/src/RNTester/RNTesterList.uwp.ts +++ b/vnext/src/RNTester/RNTesterList.uwp.ts @@ -126,6 +126,10 @@ const APIExamples: Array = [ key: 'AppStateExample', module: require('react-native/RNTester/js/AppStateExample') }, + { + key: 'ThemingExample', + module: require('./ThemingExample'), + }, { key: 'BorderExample', module: require('react-native/RNTester/js/BorderExample'), diff --git a/vnext/src/RNTester/ThemingExample.uwp.tsx b/vnext/src/RNTester/ThemingExample.uwp.tsx new file mode 100644 index 00000000000..5dd261cf3e2 --- /dev/null +++ b/vnext/src/RNTester/ThemingExample.uwp.tsx @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* tslint:disable */ + +import React = require('react'); +import { Text, View, Button } from 'react-native'; +import { AppTheme } from '../../src/index.uwp'; + +class ThemeExample extends React.Component { + state = { + currentTheme: AppTheme.currentTheme + }; + + componentDidMount() { + AppTheme.addListener('appThemeChanged', this.onAppThemeChanged); + } + + componentWillUnmount() { + AppTheme.removeListener('appThemeChanged', this.onAppThemeChanged); + } + + onAppThemeChanged = (event: any) => { + const currentTheme = AppTheme.currentTheme; + this.setState({currentTheme}); + }; + + _onPress = () => { + } + + public render() { + return ( + + currentTheme: {this.state.currentTheme} + + + ); + } +} + +export const displayName = (_undefined?: string) => {}; +export const title = 'AppTheme'; +export const description = 'Usage of theme properties.'; +export const examples = [ + { + title: 'Theme Aware Control', + render: function(): JSX.Element { + return ; + }, + } +]; \ No newline at end of file diff --git a/vnext/src/index.ts b/vnext/src/index.ts index 886a8328b49..973aafc28f9 100644 --- a/vnext/src/index.ts +++ b/vnext/src/index.ts @@ -11,4 +11,5 @@ export * from './Libraries/Components/Keyboard/KeyboardExt'; export * from './Libraries/Components/Keyboard/KeyboardExtProps'; export * from './Libraries/Components/View/ViewWindowsProps'; export * from './Libraries/Components/View/ViewWindows'; -export * from './Libraries/Modules/AppTheme/AppTheme'; +export * from './Libraries/AppTheme/AppTheme'; +export * from './Libraries/AppTheme/AppThemeTypes'; diff --git a/vnext/src/index.uwp.ts b/vnext/src/index.uwp.ts index 3cd3632a5b4..3c549d1d1e3 100644 --- a/vnext/src/index.uwp.ts +++ b/vnext/src/index.uwp.ts @@ -11,4 +11,5 @@ export * from './Libraries/Components/Keyboard/KeyboardExt.uwp'; export * from './Libraries/Components/Keyboard/KeyboardExtProps'; export * from './Libraries/Components/View/ViewWindowsProps'; export * from './Libraries/Components/View/ViewWindows.uwp'; -export * from './Libraries/Modules/AppTheme/AppTheme.uwp'; \ No newline at end of file +export * from './Libraries/AppTheme/AppTheme.uwp'; +export * from './Libraries/AppTheme/AppThemeTypes'; \ No newline at end of file