Skip to content

Commit

Permalink
refactor(Device): use internal getters to support SSR (#6421)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcusNotheis authored Feb 8, 2023
1 parent 78bd237 commit 2830c67
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ root = true
[*]
charset = utf-8

[*.{css,html,java,js,json,less,txt}]
[*.{css,html,java,js,json,less,txt,ts}]
trim_trailing_whitespace = true
end_of_line = lf
tab_width = 4
Expand Down
6 changes: 5 additions & 1 deletion packages/base/package-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ const scripts = {
styles: 'chokidar "src/css/*.css" -c "nps generateStyles"'
},
start: "nps prepare watch.withBundle",
test: `node "${LIB}/test-runner/test-runner.js"`,
test: {
default: 'concurrently "nps test.wdio"',
ssr: `mocha test/ssr`,
wdio: `node "${LIB}/test-runner/test-runner.js"`
},
};


Expand Down
4 changes: 4 additions & 0 deletions packages/base/src/Boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const boot = async (): Promise<void> => {
}

const bootExecutor = async (resolve: PromiseResolve) => {
if (typeof document === "undefined") {
resolve();
return;
}
registerCurrentRuntime();

const openUI5Support = getFeature<typeof OpenUI5Support>("OpenUI5Support");
Expand Down
147 changes: 113 additions & 34 deletions packages/base/src/Device.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,178 @@
const ua = navigator.userAgent;
const touch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
const ie = /(msie|trident)/i.test(ua);
const chrome = !ie && /(Chrome|CriOS)/.test(ua);
const firefox = /Firefox/.test(ua);
const safari = !ie && !chrome && /(Version|PhantomJS)\/(\d+\.\d+).*Safari/.test(ua);
const webkit = !ie && /webkit/.test(ua);
const windows = navigator.platform.indexOf("Win") !== -1;
const iOS = !!(navigator.platform.match(/iPhone|iPad|iPod/)) || !!(navigator.userAgent.match(/Mac/) && "ontouchend" in document);
const android = !windows && /Android/.test(ua);
const androidPhone = android && /(?=android)(?=.*mobile)/i.test(ua);
const ipad = /ipad/i.test(ua) || (/Macintosh/i.test(ua) && "ontouchend" in document);
// With iOS 13 the string 'iPad' was removed from the user agent string through a browser setting, which is applied on all sites by default:
// "Request Desktop Website -> All websites" (for more infos see: https://forums.developer.apple.com/thread/119186).
// Therefore the OS is detected as MACINTOSH instead of iOS and the device is a tablet if the Device.support.touch is true.
const isSSR = typeof document === "undefined";

const internals = {
get userAgent() {
if (isSSR) {
return "";
}
return navigator.userAgent;
},
get touch() {
if (isSSR) {
return false;
}
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
},
get ie() {
if (isSSR) {
return false;
}
return /(msie|trident)/i.test(internals.userAgent);
},
get chrome() {
if (isSSR) {
return false;
}
return !internals.ie && /(Chrome|CriOS)/.test(internals.userAgent);
},
get firefox() {
if (isSSR) {
return false;
}
return /Firefox/.test(internals.userAgent);
},
get safari() {
if (isSSR) {
return false;
}
return !internals.ie && !internals.chrome && /(Version|PhantomJS)\/(\d+\.\d+).*Safari/.test(internals.userAgent);
},
get webkit() {
if (isSSR) {
return false;
}
return !internals.ie && /webkit/.test(internals.userAgent);
},
get windows() {
if (isSSR) {
return false;
}
return navigator.platform.indexOf("Win") !== -1;
},
get iOS() {
if (isSSR) {
return false;
}
return !!(navigator.platform.match(/iPhone|iPad|iPod/)) || !!(internals.userAgent.match(/Mac/) && "ontouchend" in document);
},
get android() {
if (isSSR) {
return false;
}
return !internals.windows && /Android/.test(internals.userAgent);
},
get androidPhone() {
if (isSSR) {
return false;
}
return internals.android && /(?=android)(?=.*mobile)/i.test(internals.userAgent);
},
get ipad() {
if (isSSR) {
return false;
}
// With iOS 13 the string 'iPad' was removed from the user agent string through a browser setting, which is applied on all sites by default:
// "Request Desktop Website -> All websites" (for more infos see: https://forums.developer.apple.com/thread/119186).
// Therefore the OS is detected as MACINTOSH instead of iOS and the device is a tablet if the Device.support.touch is true.
return /ipad/i.test(internals.userAgent) || (/Macintosh/i.test(internals.userAgent) && "ontouchend" in document);
},
};

let windowsVersion: number;
let webkitVersion: number;
let tablet: boolean;

const isWindows8OrAbove = () => {
if (!windows) {
if (isSSR) {
return false;
}

if (!internals.windows) {
return false;
}

if (windowsVersion === undefined) {
const matches = ua.match(/Windows NT (\d+).(\d)/);
const matches = internals.userAgent.match(/Windows NT (\d+).(\d)/);
windowsVersion = matches ? parseFloat(matches[1]) : 0;
}

return windowsVersion >= 8;
};

const isWebkit537OrAbove = () => {
if (!webkit) {
if (isSSR) {
return false;
}

if (!internals.webkit) {
return false;
}

if (webkitVersion === undefined) {
const matches = ua.match(/(webkit)[ /]([\w.]+)/);
const matches = internals.userAgent.match(/(webkit)[ /]([\w.]+)/);
webkitVersion = matches ? parseFloat(matches[1]) : 0;
}

return webkitVersion >= 537.10;
};

const detectTablet = () => {
if (isSSR) {
return false;
}

if (tablet !== undefined) {
return;
}

if (ipad) {
if (internals.ipad) {
tablet = true;
return;
}

if (touch) {
if (internals.touch) {
if (isWindows8OrAbove()) {
tablet = true;
return;
}

if (chrome && android) {
tablet = !/Mobile Safari\/[.0-9]+/.test(ua);
if (internals.chrome && internals.android) {
tablet = !/Mobile Safari\/[.0-9]+/.test(internals.userAgent);
return;
}

let densityFactor = window.devicePixelRatio ? window.devicePixelRatio : 1; // may be undefined in Windows Phone devices
if (android && isWebkit537OrAbove()) {
if (internals.android && isWebkit537OrAbove()) {
densityFactor = 1;
}

tablet = (Math.min(window.screen.width / densityFactor, window.screen.height / densityFactor) >= 600);
return;
}

tablet = (ie && ua.indexOf("Touch") !== -1) || (android && !androidPhone);
tablet = (internals.ie && internals.userAgent.indexOf("Touch") !== -1) || (internals.android && !internals.androidPhone);
};

const supportsTouch = (): boolean => touch;
const isIE = (): boolean => ie;
const isSafari = (): boolean => safari;
const isChrome = (): boolean => chrome;
const isFirefox = (): boolean => firefox;
const supportsTouch = (): boolean => internals.touch;
const isIE = (): boolean => internals.ie;
const isSafari = (): boolean => internals.safari;
const isChrome = (): boolean => internals.chrome;
const isFirefox = (): boolean => internals.firefox;

const isTablet = (): boolean => {
detectTablet();
return (touch || isWindows8OrAbove()) && tablet;
return (internals.touch || isWindows8OrAbove()) && tablet;
};

const isPhone = (): boolean => {
detectTablet();
return touch && !tablet;
return internals.touch && !tablet;
};

const isDesktop = (): boolean => {
if (isSSR) {
return false;
}
return (!isTablet() && !isPhone()) || isWindows8OrAbove();
};

Expand All @@ -102,11 +181,11 @@ const isCombi = (): boolean => {
};

const isIOS = (): boolean => {
return iOS;
return internals.iOS;
};

const isAndroid = (): boolean => {
return android || androidPhone;
return internals.android || internals.androidPhone;
};

export {
Expand Down
2 changes: 1 addition & 1 deletion packages/base/src/InitialConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const applyOpenUI5Configuration = () => {
};

const initConfiguration = () => {
if (initialized) {
if (typeof document === "undefined" || initialized) {
return;
}

Expand Down
11 changes: 10 additions & 1 deletion packages/base/src/getSharedResource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import getSingletonElementInstance from "./util/getSingletonElementInstance.js";

const getSharedResourcesInstance = () => getSingletonElementInstance("ui5-shared-resources", document.head);
const getSharedResourcesInstance = (): Record<string, unknown> | null => {
if (typeof document === "undefined") {
return null;
}
return getSingletonElementInstance("ui5-shared-resources", document.head) as unknown as Record<string, unknown>;
};

/**
* Use this method to initialize/get resources that you would like to be shared among UI5 Web Components runtime instances.
Expand All @@ -15,6 +20,10 @@ const getSharedResource = <T>(namespace: string, initialValue: T): T => {
const parts = namespace.split(".");
let current = getSharedResourcesInstance() as Record<string, any>;

if (!current) {
return initialValue;
}

for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const lastPart = i === parts.length - 1;
Expand Down
12 changes: 7 additions & 5 deletions packages/base/src/theming/CustomStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import EventProvider from "../EventProvider.js";

type CustomCSSChangeCallback = (tag: string) => void;

const eventProvider = getSharedResource("CustomStyle.eventProvider", new EventProvider<string, void>());
const getEventProvider = () => getSharedResource("CustomStyle.eventProvider", new EventProvider<string, void>());
const CUSTOM_CSS_CHANGE = "CustomCSSChange";

const attachCustomCSSChange = (listener: CustomCSSChangeCallback) => {
eventProvider.attachEvent(CUSTOM_CSS_CHANGE, listener);
getEventProvider().attachEvent(CUSTOM_CSS_CHANGE, listener);
};

const detachCustomCSSChange = (listener: CustomCSSChangeCallback) => {
eventProvider.detachEvent(CUSTOM_CSS_CHANGE, listener);
getEventProvider().detachEvent(CUSTOM_CSS_CHANGE, listener);
};

const fireCustomCSSChange = (tag: string) => {
return eventProvider.fireEvent(CUSTOM_CSS_CHANGE, tag);
return getEventProvider().fireEvent(CUSTOM_CSS_CHANGE, tag);
};

const customCSSFor = getSharedResource<Record<string, Array<string>>>("CustomStyle.customCSSFor", {});
const getCustomCSSFor = () => getSharedResource<Record<string, Array<string>>>("CustomStyle.customCSSFor", {});

// Listen to the eventProvider, in case other copies of this CustomStyle module fire this
// event, and this copy would therefore need to reRender the ui5 webcomponents; but
Expand All @@ -32,6 +32,7 @@ attachCustomCSSChange((tag: string) => {
});

const addCustomCSS = (tag: string, css: string) => {
const customCSSFor = getCustomCSSFor();
if (!customCSSFor[tag]) {
customCSSFor[tag] = [];
}
Expand All @@ -51,6 +52,7 @@ const addCustomCSS = (tag: string, css: string) => {
};

const getCustomCSS = (tag: string) => {
const customCSSFor = getCustomCSSFor();
return customCSSFor[tag] ? customCSSFor[tag].join("") : "";
};

Expand Down
21 changes: 21 additions & 0 deletions packages/base/test/ssr/Device.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {assert} from "chai";
import * as Device from "../../dist/Device.js";

describe('SSR / Device', () => {

it('all detections should return false', () => {
assert.strictEqual(Device.supportsTouch(), false, `'supportsTouch' should be false`);
assert.strictEqual(Device.isIE(), false, `'isIE' should be false`);
assert.strictEqual(Device.isSafari(), false, `'isSafari' should be false`);
assert.strictEqual(Device.isChrome(), false, `'isChrome' should be false`);
assert.strictEqual(Device.isFirefox(), false, `'isFirefox' should be false`);
assert.strictEqual(Device.isPhone(), false, `'isPhone' should be false`);
assert.strictEqual(Device.isTablet(), false, `'isTablet' should be false`);
assert.strictEqual(Device.isDesktop(), false, `'isDesktop' should be false`);
assert.strictEqual(Device.isCombi(), false, `'isCombi' should be false`);
assert.strictEqual(Device.isIOS(), false, `'isIOS' should be false`);
assert.strictEqual(Device.isAndroid(), false, `'isAndroid' should be false`);
})
})


0 comments on commit 2830c67

Please sign in to comment.