From cafc159999fa6bfc00215bb5729bcb6af2379a5a Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 09:38:50 -0400 Subject: [PATCH 01/14] Add basic safe mode support. --- frontend/context_processors.py | 18 ++++++++++ frontend/safe_mode.py | 35 +++++++++++++++++++ frontend/safe_mode/safe-mode.js | 26 ++++++++++++++ frontend/sass/styles.scss | 32 +++++++++++++++++ frontend/templates/frontend/safe_mode_ui.html | 21 +++++++++++ project/settings.py | 1 + project/templates/index.html | 12 ++++--- project/tests/test_views.py | 16 ++++----- project/urls.py | 1 + project/util/js_snippet.py | 8 +---- 10 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 frontend/context_processors.py create mode 100644 frontend/safe_mode.py create mode 100644 frontend/safe_mode/safe-mode.js create mode 100644 frontend/templates/frontend/safe_mode_ui.html diff --git a/frontend/context_processors.py b/frontend/context_processors.py new file mode 100644 index 000000000..5a6377f5e --- /dev/null +++ b/frontend/context_processors.py @@ -0,0 +1,18 @@ +from project.util.js_snippet import JsSnippetContextProcessor +import frontend.safe_mode + + +class SafeModeJsSnippet(JsSnippetContextProcessor): + @property + def template(self) -> str: + return frontend.safe_mode.SAFE_MODE_JS.read_text() + + var_name = 'SAFE_MODE_SNIPPET' + + +def safe_mode(request): + is_enabled = frontend.safe_mode.is_enabled(request) + ctx = {'is_safe_mode_enabled': is_enabled} + if not is_enabled: + ctx.update(SafeModeJsSnippet()(request)) + return ctx diff --git a/frontend/safe_mode.py b/frontend/safe_mode.py new file mode 100644 index 000000000..e3277035d --- /dev/null +++ b/frontend/safe_mode.py @@ -0,0 +1,35 @@ +from pathlib import Path +from django.http import HttpResponseRedirect +from django.urls import path +from django.views.decorators.http import require_POST + + +MY_DIR = Path(__file__).parent.resolve() + +SESSION_KEY = 'enable_safe_mode' + +SAFE_MODE_JS = MY_DIR / 'safe_mode' / 'safe-mode.js' + + +def is_enabled(request): + return request.session.get(SESSION_KEY, False) + + +@require_POST +def enable(request): + request.session[SESSION_KEY] = True + return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) + + +@require_POST +def disable(request): + request.session[SESSION_KEY] = False + return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) + + +app_name = 'safe_mode' + +urlpatterns = [ + path('enable', enable, name='enable'), + path('disable', disable, name='disable'), +] diff --git a/frontend/safe_mode/safe-mode.js b/frontend/safe_mode/safe-mode.js new file mode 100644 index 000000000..09daf0ee4 --- /dev/null +++ b/frontend/safe_mode/safe-mode.js @@ -0,0 +1,26 @@ +// @ts-check + +var globalErrorCount = 0; + +function showEnableSafeModeUI() { + var el = document.getElementById('safe-mode-enable'); + + if (el && el.hasAttribute('hidden')) { + el.removeAttribute('hidden'); + el.focus(); + } +} + +window.addEventListener('error', function() { + globalErrorCount++; + // Enclose the following in a try/catch to avoid infinite recursion. + try { + showEnableSafeModeUI(); + } catch (e) {} +}); + +window.addEventListener('load', function() { + if (globalErrorCount) { + showEnableSafeModeUI(); + } +}); diff --git a/frontend/sass/styles.scss b/frontend/sass/styles.scss index 24d736984..399503778 100644 --- a/frontend/sass/styles.scss +++ b/frontend/sass/styles.scss @@ -196,3 +196,35 @@ .jf-autocomplete-field ul li { padding: 0.5rem; } + +aside.safe-mode { + position: fixed; + background-color: $danger; + color: white; + bottom: 0; + left: 0; + right: 0; + padding: 1em; + z-index: 1000; + + strong { + color: white; + } + + form { + margin-bottom: 0; + } + + button { + margin: 0 0 0 1em; + float: right; + } +} + +[hidden] { + display: none; +} + +aside.safe-mode.safe-mode-disable { + background-color: transparentize(black, 0.2); +} diff --git a/frontend/templates/frontend/safe_mode_ui.html b/frontend/templates/frontend/safe_mode_ui.html new file mode 100644 index 000000000..a4fa82420 --- /dev/null +++ b/frontend/templates/frontend/safe_mode_ui.html @@ -0,0 +1,21 @@ +{% if is_safe_mode_enabled %} + +{% else %} + +{% endif %} diff --git a/project/settings.py b/project/settings.py index 172ad15c9..55b37a9bd 100644 --- a/project/settings.py +++ b/project/settings.py @@ -82,6 +82,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'frontend.context_processors.safe_mode', 'project.context_processors.ga_snippet', 'project.context_processors.rollbar_snippet', ], diff --git a/project/templates/index.html b/project/templates/index.html index 4186d5df5..ad2de75b8 100644 --- a/project/templates/index.html +++ b/project/templates/index.html @@ -4,6 +4,7 @@ + {{ SAFE_MODE_SNIPPET }} {{ ROLLBAR_SNIPPET }} {{ GA_SNIPPET }} {{ title_tag }} @@ -11,9 +12,12 @@
{{ modal_html }}
{{ initial_render }}
- {{ initial_props|json_script:'initial-props' }} - {% for bundle_url in bundle_urls %} - - {% endfor %} + {% if not is_safe_mode_enabled %} + {{ initial_props|json_script:'initial-props' }} + {% for bundle_url in bundle_urls %} + + {% endfor %} + {% endif %} + {% include 'frontend/safe_mode_ui.html' %} \ No newline at end of file diff --git a/project/tests/test_views.py b/project/tests/test_views.py index 5367a8dd9..38906a193 100644 --- a/project/tests/test_views.py +++ b/project/tests/test_views.py @@ -98,7 +98,7 @@ def test_fix_newlines_works(): def test_form_submission_redirects_on_success(django_app): - form = django_app.get('/__example-form').form + form = django_app.get('/__example-form').forms[0] # Sometimes browsers will munge the newlines in our own # hidden inputs before submitting; let's make sure that @@ -117,12 +117,12 @@ def test_form_submission_shows_errors(django_app): response = django_app.get('/__example-form') assert response.status == '200 OK' - form = response.form + form = response.forms[0] form['exampleField'] = 'hello there buddy' response = form.submit() assert response.status == '200 OK' - form = response.form + form = response.forms[0] # Ensure the form preserves the input from our last submission. assert form['exampleField'].value == 'hello there buddy' @@ -131,28 +131,28 @@ def test_form_submission_shows_errors(django_app): def test_form_submission_preserves_boolean_fields(django_app): - form = django_app.get('/__example-form').form + form = django_app.get('/__example-form').forms[0] assert form['boolField'].value is None form['boolField'] = True response = form.submit() assert response.status == '200 OK' - form = response.form + form = response.forms[0] assert form['boolField'].value == 'on' form['boolField'] = False response = form.submit() assert response.status == '200 OK' - form = response.form + form = response.forms[0] assert form['boolField'].value is None @pytest.mark.django_db def test_successful_login_redirects_to_next(django_app): UserFactory(phone_number='5551234567', password='test123') - form = django_app.get('/login?next=/boop').form + form = django_app.get('/login?next=/boop').forms[0] form['phoneNumber'] = '5551234567' form['password'] = 'test123' @@ -164,7 +164,7 @@ def test_successful_login_redirects_to_next(django_app): @pytest.mark.django_db def test_unsuccessful_login_shows_error(django_app): - form = django_app.get('/login?next=/boop').form + form = django_app.get('/login?next=/boop').forms[0] form['phoneNumber'] = '5551234567' form['password'] = 'test123' diff --git a/project/urls.py b/project/urls.py index 71027157e..22b2dd761 100644 --- a/project/urls.py +++ b/project/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path('loc/', include('loc.urls')), + path('safe-mode/', include('frontend.safe_mode')), path('favicon.ico', redirect_favicon), path('__example-server-error/', example_server_error), path('graphql', GraphQLView.as_view(batch=True), name='batch-graphql'), diff --git a/project/util/js_snippet.py b/project/util/js_snippet.py index ceb468e3d..d63da0bbb 100644 --- a/project/util/js_snippet.py +++ b/project/util/js_snippet.py @@ -75,7 +75,7 @@ def get_html(self, request: HttpRequest) -> SafeString: This can be overridden to provide multiple HTML tags. ''' - inline_script = self._template % self.get_context() + inline_script = dedent(self.template).strip() % self.get_context() request.allow_inline_script(inline_script) request.csp_update(**self.csp_updates) return SafeString(f"") @@ -90,9 +90,3 @@ def __call__(self, request: HttpRequest) -> Dict[str, str]: return { self.var_name: SimpleLazyObject(partial(self.get_html, request)) } - - # A dedented version of our template. - _template: str - - def __init__(self): - self._template = dedent(self.template).strip() From 0e7a6f376348f088d42805fc1310737d0fbc0c2e Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 11:24:07 -0400 Subject: [PATCH 02/14] Don't show safe mode UI when prog enhancement catches errors. --- frontend/context_processors.py | 3 +- frontend/lib/progressive-enhancement.tsx | 1 + frontend/lib/tests/setup.ts | 4 ++ frontend/safe_mode/safe-mode.d.ts | 9 +++ frontend/safe_mode/safe-mode.js | 71 +++++++++++++++++------- jest.config.js | 1 + 6 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 frontend/safe_mode/safe-mode.d.ts diff --git a/frontend/context_processors.py b/frontend/context_processors.py index 5a6377f5e..b85b61ad1 100644 --- a/frontend/context_processors.py +++ b/frontend/context_processors.py @@ -13,6 +13,5 @@ def template(self) -> str: def safe_mode(request): is_enabled = frontend.safe_mode.is_enabled(request) ctx = {'is_safe_mode_enabled': is_enabled} - if not is_enabled: - ctx.update(SafeModeJsSnippet()(request)) + ctx.update(SafeModeJsSnippet()(request)) return ctx diff --git a/frontend/lib/progressive-enhancement.tsx b/frontend/lib/progressive-enhancement.tsx index c36b11cf2..8cb42390c 100644 --- a/frontend/lib/progressive-enhancement.tsx +++ b/frontend/lib/progressive-enhancement.tsx @@ -77,6 +77,7 @@ export class ProgressiveEnhancement extends React.Component 0; } -} - -window.addEventListener('error', function() { - globalErrorCount++; - // Enclose the following in a try/catch to avoid infinite recursion. - try { - showEnableSafeModeUI(); - } catch (e) {} -}); - -window.addEventListener('load', function() { - if (globalErrorCount) { - showEnableSafeModeUI(); + + function scheduleShowUICheck() { + if (showUiTimeout !== null) { + window.clearTimeout(showUiTimeout); + showUiTimeout = null; + } + showUiTimeout = window.setTimeout(function() { + var el = document.getElementById('safe-mode-enable'); + + if (checkValidErrors() && el && el.hasAttribute('hidden')) { + el.removeAttribute('hidden'); + el.focus(); + } + }, SHOW_UI_DELAY_MS); } -}); + + window.SafeMode = { + ignoreError: function(e) { + errorsToIgnore.push(e.toString()); + } + }; + + window.addEventListener('error', function(e) { + try { + errors.push(e.error.toString()); + } catch (e) { + errors.push('unknown error'); + } + scheduleShowUICheck(); + }); + + window.addEventListener('load', scheduleShowUICheck); +})(); diff --git a/jest.config.js b/jest.config.js index 612ef27e5..95c9aa2d4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { "frontend" ], "collectCoverage": true, + "coveragePathIgnorePatterns": ["/node_modules/", "safe-mode.d.ts"], "coverageReporters": [ "lcov", "html" From efd56b4522e2ddca42eb98e075570ecc4738ab5c Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 11:33:51 -0400 Subject: [PATCH 03/14] Make ProressiveEnhancement resilient to window.SafeMode not being present. --- frontend/lib/progressive-enhancement.tsx | 4 +++- frontend/lib/tests/setup.ts | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/lib/progressive-enhancement.tsx b/frontend/lib/progressive-enhancement.tsx index 8cb42390c..a5c4d1a61 100644 --- a/frontend/lib/progressive-enhancement.tsx +++ b/frontend/lib/progressive-enhancement.tsx @@ -77,7 +77,9 @@ export class ProgressiveEnhancement extends React.Component Date: Sun, 23 Sep 2018 11:58:38 -0400 Subject: [PATCH 04/14] Minify safe mode snippet. --- .dockerignore | 1 + .gitignore | 1 + frontend/safe_mode.py | 2 +- frontend/safe_mode/watcher.js | 18 ++++++++++++++++++ package.json | 6 ++++-- 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 frontend/safe_mode/watcher.js diff --git a/.dockerignore b/.dockerignore index ec502fa3c..84703a3f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,6 +17,7 @@ frontend/static/frontend/*.js frontend/static/frontend/*.css frontend/static/frontend/*.map frontend/static/frontend/report.html +frontend/safe_mode/safe-mode.min.js staticfiles/ frontend/lib/queries/__generated__/ querybuilder.js diff --git a/.gitignore b/.gitignore index ee5fd5386..0c3252a50 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ frontend/static/frontend/*.js frontend/static/frontend/*.css frontend/static/frontend/*.map frontend/static/frontend/report.html +frontend/safe_mode/safe-mode.min.js staticfiles/ frontend/lib/queries/__generated__/ querybuilder.js diff --git a/frontend/safe_mode.py b/frontend/safe_mode.py index e3277035d..d043f0d99 100644 --- a/frontend/safe_mode.py +++ b/frontend/safe_mode.py @@ -8,7 +8,7 @@ SESSION_KEY = 'enable_safe_mode' -SAFE_MODE_JS = MY_DIR / 'safe_mode' / 'safe-mode.js' +SAFE_MODE_JS = MY_DIR / 'safe_mode' / 'safe-mode.min.js' def is_enabled(request): diff --git a/frontend/safe_mode/watcher.js b/frontend/safe_mode/watcher.js new file mode 100644 index 000000000..9e2dd2ef1 --- /dev/null +++ b/frontend/safe_mode/watcher.js @@ -0,0 +1,18 @@ +// @ts-check + +const { execSync } = require('child_process'); +const chokidar = require('chokidar'); + +const FILENAME = 'safe-mode.js'; + +function uglify() { + console.log(`Uglifying ${FILENAME}.`); + execSync('npm run safe_mode_snippet', { stdio: 'inherit' }); +} + +if (!module.parent) { + console.log(`Waiting for changes to ${FILENAME}...`); + chokidar.watch(`${__dirname}/${FILENAME}`) + .on('ready', uglify) + .on('change', uglify); +} diff --git a/package.json b/package.json index cb4e043b4..8b275e986 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "webpack": "webpack", "webpack:watch": "webpack --watch", "webpack:querybuilder": "webpack --config frontend/webpack/querybuilder.config.js --silent", - "build": "npm run sass && npm run webpack", - "start": "npm run sass && npm run webpack:querybuilder && concurrently --kill-others \"npm run webpack:watch\" \"npm run sass:watch\" \"npm run querybuilder:watch\"" + "safe_mode_snippet": "uglifyjs frontend/safe_mode/safe-mode.js -o frontend/safe_mode/safe-mode.min.js", + "safe_mode_snippet:watch": "node frontend/safe_mode/watcher.js", + "build": "npm run sass && npm run webpack && npm run safe_mode_snippet", + "start": "npm run sass && npm run webpack:querybuilder && concurrently --kill-others \"npm run webpack:watch\" \"npm run sass:watch\" \"npm run querybuilder:watch\" \"npm run safe_mode_snippet:watch\"" }, "repository": { "type": "git", From 63475bc5d01ef8633bfea26767c4af4e5a601df7 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 12:13:42 -0400 Subject: [PATCH 05/14] Add docs to safe-mode.js. --- frontend/safe_mode/safe-mode.js | 65 ++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/frontend/safe_mode/safe-mode.js b/frontend/safe_mode/safe-mode.js index f418a3ca2..30dece9f1 100644 --- a/frontend/safe_mode/safe-mode.js +++ b/frontend/safe_mode/safe-mode.js @@ -1,28 +1,63 @@ // @ts-check + +/** + * This file contains ES3-compliant JavaScript that will be minified and inserted + * into the top of the page as an inline snippet. Its purpose is to listen for any + * uncaught errors that are raised on the page and present a UI to opt the user + * into "safe mode" (also known as "compatibility mode"), whereby we deliver + * nearly zero JavaScript to the client browser. + */ (function() { + /** + * The amount of time from when we receive an error to when we show the + * opt-in UI for activating safe mode. + * + * The reason there is any delay is because in some cases, an error + * event occurs and our own client-side code later deals with it + * gracefully on its own, obviating the need for the user to enter + * safe mode. We need to provide some leeway for that code to + * let us know that the error has been handled, hence this delay. + */ var SHOW_UI_DELAY_MS = 250; - /** @type {string[]} */ + /** + * A list of error messages that other client-side code has told + * us to ignore. + * + * @type {string[]} + */ var errorsToIgnore = []; - /** @type {string[]} */ + /** + * A list of error messages that we've received so far. + * + * @type {string[]} + */ var errors = []; - /** @type {number|null} */ + /** + * Book-keeping used to control the display of the UI. + * + * @type {number|null} + */ var showUiTimeout = null; - function checkValidErrors() { - var validErrors = 0; + /** + * Check to see if any valid errors have been logged and return + * true if so. + * + * @returns {boolean} + */ + function validErrorsExist() { for (var i = 0; i < errors.length; i++) { if (errorsToIgnore.indexOf(errors[i]) === -1) { - validErrors += 1; + return true; } } - errors = []; - errorsToIgnore = []; - return validErrors > 0; + return false; } + /** Shedule a check to see if we should display the opt-in UI. */ function scheduleShowUICheck() { if (showUiTimeout !== null) { window.clearTimeout(showUiTimeout); @@ -31,19 +66,23 @@ showUiTimeout = window.setTimeout(function() { var el = document.getElementById('safe-mode-enable'); - if (checkValidErrors() && el && el.hasAttribute('hidden')) { + if (el && el.hasAttribute('hidden') && validErrorsExist()) { el.removeAttribute('hidden'); el.focus(); + errors = []; + errorsToIgnore = []; } }, SHOW_UI_DELAY_MS); } + /** Our public API. See safe-mode.d.ts for more documentation. */ window.SafeMode = { ignoreError: function(e) { errorsToIgnore.push(e.toString()); } }; + /** Listen for any error events. */ window.addEventListener('error', function(e) { try { errors.push(e.error.toString()); @@ -53,5 +92,11 @@ scheduleShowUICheck(); }); + /** + * It's possible that some errors occurred while our page + * was loading, but the opt-in UI wasn't available yet. + * If that was the case, schedule another check to display + * the UI just in case. + */ window.addEventListener('load', scheduleShowUICheck); })(); From 2cc3ed9176f15e5b150ed55299c5e9441d60f107 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 12:19:20 -0400 Subject: [PATCH 06/14] improve safe mode watcher. --- frontend/safe_mode/watcher.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/safe_mode/watcher.js b/frontend/safe_mode/watcher.js index 9e2dd2ef1..32f83e519 100644 --- a/frontend/safe_mode/watcher.js +++ b/frontend/safe_mode/watcher.js @@ -2,17 +2,22 @@ const { execSync } = require('child_process'); const chokidar = require('chokidar'); +const chalk = require('chalk').default; const FILENAME = 'safe-mode.js'; function uglify() { console.log(`Uglifying ${FILENAME}.`); - execSync('npm run safe_mode_snippet', { stdio: 'inherit' }); + try { + execSync('npm run safe_mode_snippet', { stdio: 'inherit' }); + } catch (e) { + console.log(chalk.redBright(`Uglification failed!`)); + } } if (!module.parent) { console.log(`Waiting for changes to ${FILENAME}...`); - chokidar.watch(`${__dirname}/${FILENAME}`) + chokidar.watch(`${__dirname}/${FILENAME}`, { awaitWriteFinish: true }) .on('ready', uglify) .on('change', uglify); } From 276da9283d4a04cf0565afd4db26d6f0345bd635 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 13:17:14 -0400 Subject: [PATCH 07/14] Improve styling of safe mode UI. --- frontend/safe_mode/safe-mode.d.ts | 6 +++ frontend/safe_mode/safe-mode.js | 16 +++++- frontend/sass/styles.scss | 22 ++------ frontend/templates/frontend/safe_mode_ui.html | 51 +++++++++++++------ 4 files changed, 59 insertions(+), 36 deletions(-) diff --git a/frontend/safe_mode/safe-mode.d.ts b/frontend/safe_mode/safe-mode.d.ts index 8b62081b0..a5f339c24 100644 --- a/frontend/safe_mode/safe-mode.d.ts +++ b/frontend/safe_mode/safe-mode.d.ts @@ -5,5 +5,11 @@ interface Window { * shouldn't show its UI if it sees the error. */ ignoreError(e: Error): void; + + /** + * Show the safe-mode opt-in UI. This is intended primarily + * for manual testing, but client code can use it too. + */ + showUI(): void; } } diff --git a/frontend/safe_mode/safe-mode.js b/frontend/safe_mode/safe-mode.js index 30dece9f1..8879401ea 100644 --- a/frontend/safe_mode/safe-mode.js +++ b/frontend/safe_mode/safe-mode.js @@ -69,14 +69,26 @@ if (el && el.hasAttribute('hidden') && validErrorsExist()) { el.removeAttribute('hidden'); el.focus(); - errors = []; - errorsToIgnore = []; + + /** @type {HTMLButtonElement|null} */ + var deleteBtn = el.querySelector('button.delete'); + if (deleteBtn) { + deleteBtn.onclick = function() { + if (el) { + el.setAttribute('hidden', ''); + } + }; + } } }, SHOW_UI_DELAY_MS); } /** Our public API. See safe-mode.d.ts for more documentation. */ window.SafeMode = { + showUI: function() { + errors.push('showUI() called'); + scheduleShowUICheck(); + }, ignoreError: function(e) { errorsToIgnore.push(e.toString()); } diff --git a/frontend/sass/styles.scss b/frontend/sass/styles.scss index 399503778..3ebdde3d1 100644 --- a/frontend/sass/styles.scss +++ b/frontend/sass/styles.scss @@ -199,32 +199,18 @@ aside.safe-mode { position: fixed; - background-color: $danger; - color: white; bottom: 0; left: 0; right: 0; padding: 1em; z-index: 1000; +} - strong { - color: white; - } - - form { - margin-bottom: 0; - } - - button { - margin: 0 0 0 1em; - float: right; - } +.safe-mode-disable .notification { + background-color: $justfix-blue; + border-radius: 0; } [hidden] { display: none; } - -aside.safe-mode.safe-mode-disable { - background-color: transparentize(black, 0.2); -} diff --git a/frontend/templates/frontend/safe_mode_ui.html b/frontend/templates/frontend/safe_mode_ui.html index a4fa82420..d71cd8bac 100644 --- a/frontend/templates/frontend/safe_mode_ui.html +++ b/frontend/templates/frontend/safe_mode_ui.html @@ -1,21 +1,40 @@ {% if is_safe_mode_enabled %} - +
+
+
+ {% csrf_token %} +
+
+ This site is currently in compatibility mode. For an enhanced + experience, you can disable it, but this may cause compatibility issues + with your current browser. +
+
+ +
+
+
+
+
{% else %} {% endif %} From 8c539ec043a74a965aa0ea70bb4dd3fd5fddecad Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 14:17:32 -0400 Subject: [PATCH 08/14] Attempt to not overlap safe mode UI w/ dropdowns. --- frontend/sass/styles.scss | 12 +++++++- frontend/templates/frontend/safe_mode_ui.html | 30 ++++++++++--------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/frontend/sass/styles.scss b/frontend/sass/styles.scss index 3ebdde3d1..a1a7b91aa 100644 --- a/frontend/sass/styles.scss +++ b/frontend/sass/styles.scss @@ -206,9 +206,19 @@ aside.safe-mode { z-index: 1000; } +.safe-mode-disable { + background-color: $justfix-blue; +} + .safe-mode-disable .notification { + // This is a TERRIBLE hack to try to ensure that the + // safe mode UI isn't overlapped by any dropdowns in + // the perma-expanded navbar above it. + @media screen and (min-width: $desktop) { + padding-top: 200px; + } + background-color: $justfix-blue; - border-radius: 0; } [hidden] { diff --git a/frontend/templates/frontend/safe_mode_ui.html b/frontend/templates/frontend/safe_mode_ui.html index d71cd8bac..bd9ac77a2 100644 --- a/frontend/templates/frontend/safe_mode_ui.html +++ b/frontend/templates/frontend/safe_mode_ui.html @@ -1,19 +1,21 @@ {% if is_safe_mode_enabled %} -
-
-
- {% csrf_token %} -
-
- This site is currently in compatibility mode. For an enhanced - experience, you can disable it, but this may cause compatibility issues - with your current browser. -
-
- +
+
+
+ + {% csrf_token %} +
+
+ This site is currently in compatibility mode. For an enhanced + experience, you can disable it, but this may cause compatibility issues + with your current browser. +
+
+ +
-
- + +
{% else %} From 70bc317548018e429dc99e372ff48755ed2d87c9 Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sun, 23 Sep 2018 17:47:17 -0400 Subject: [PATCH 09/14] Replace .jf-no-js with [data-safe-mode-no-js]. --- frontend/lib/app.tsx | 4 - frontend/lib/main.ts | 2 - frontend/safe_mode/safe-mode.js | 21 ++- frontend/sass/_no-js.scss | 132 ------------------ frontend/sass/_safe-mode.scss | 24 ++++ frontend/sass/styles.scss | 30 +--- frontend/templates/frontend/safe_mode_ui.html | 2 +- project/templates/index.html | 2 +- 8 files changed, 45 insertions(+), 172 deletions(-) delete mode 100644 frontend/sass/_no-js.scss create mode 100644 frontend/sass/_safe-mode.scss diff --git a/frontend/lib/app.tsx b/frontend/lib/app.tsx index ee60e950e..0fbeca707 100644 --- a/frontend/lib/app.tsx +++ b/frontend/lib/app.tsx @@ -194,10 +194,6 @@ export class AppWithoutRouter extends React.Component
-
- {/* This is a no-JS fallback. */} - -
diff --git a/frontend/lib/main.ts b/frontend/lib/main.ts index 7997992ca..8b3bf584a 100644 --- a/frontend/lib/main.ts +++ b/frontend/lib/main.ts @@ -21,7 +21,5 @@ window.addEventListener('load', () => { // Since JS is now loaded, let's remove that restriction. div.removeAttribute('hidden'); - document.documentElement.classList.remove('jf-no-js'); - startApp(div, initialProps); }); diff --git a/frontend/safe_mode/safe-mode.js b/frontend/safe_mode/safe-mode.js index 8879401ea..9dc48455c 100644 --- a/frontend/safe_mode/safe-mode.js +++ b/frontend/safe_mode/safe-mode.js @@ -20,6 +20,18 @@ */ var SHOW_UI_DELAY_MS = 250; + /** + * The data attribute we're using to determine whether the + * safe mode opt-in UI is hidden or not. We're not using the + * standard 'hidden' attribute because under some situations, + * e.g. if JS in the client browser is completely disabled or + * this script fails to run, we actually want the UI to be + * visible even if it has this attribute, and we don't want + * e.g. assistive technologies to hide the element just because + * it has the 'hidden' attribute. + */ + var HIDDEN_ATTR = 'data-safe-mode-hidden'; + /** * A list of error messages that other client-side code has told * us to ignore. @@ -66,8 +78,8 @@ showUiTimeout = window.setTimeout(function() { var el = document.getElementById('safe-mode-enable'); - if (el && el.hasAttribute('hidden') && validErrorsExist()) { - el.removeAttribute('hidden'); + if (el && el.hasAttribute(HIDDEN_ATTR) && validErrorsExist()) { + el.removeAttribute(HIDDEN_ATTR); el.focus(); /** @type {HTMLButtonElement|null} */ @@ -75,7 +87,7 @@ if (deleteBtn) { deleteBtn.onclick = function() { if (el) { - el.setAttribute('hidden', ''); + el.setAttribute(HIDDEN_ATTR, ''); } }; } @@ -111,4 +123,7 @@ * the UI just in case. */ window.addEventListener('load', scheduleShowUICheck); + + var htmlEl = document.getElementsByTagName('html')[0]; + htmlEl.removeAttribute('data-safe-mode-no-js'); })(); diff --git a/frontend/sass/_no-js.scss b/frontend/sass/_no-js.scss deleted file mode 100644 index 6d9b0efe6..000000000 --- a/frontend/sass/_no-js.scss +++ /dev/null @@ -1,132 +0,0 @@ -// Because no-JS isn't a very common use case, but also because we don't want -// content that starts out collapsed to be inaccessible by clients without JS, -// we'll adopt a strategy whereby, through the use of CSS animations, content -// starts out the way it would on JS-enabled clients, but then transitions -// to its non-JS representation once a certain period of time has elapsed. -// -// We're going to call this period of time "long load time". - -$jf-long-load-time: 1.5s; -$jf-long-load-animation-time: $jf-long-load-time * 2; - -// This is an embarassingly large amount of code just to add support for non-JS -// clients in a non-janky way, but hopefully it will be reduced a lot by -// minification and compression. - -@keyframes jf-delayed-slidedown { - 0% { - max-height: 0; - overflow: hidden; - padding: 0 0; - } - - 50% { - max-height: 0; - overflow: hidden; - padding: 0 0; - } - - 100% { - max-height: 1000px; - padding: 0.5rem 0; - } -} - -@keyframes jf-delayed-fadein { - 0% { - opacity: 0; - } - - 50% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -// A mixin to automatically expand collapsed parts of -// a navbar for clients that have JS disabled. -@mixin autoexpand-fallback { - display: block; - .navbar-item, .navbar-link { - color: $justfix-blue; - } - .navbar-item:focus, .navbar-link:focus { - outline: 2px dashed $justfix-blue; - } - animation-duration: $jf-long-load-animation-time; - animation-name: jf-delayed-slidedown; -} - -html:not(.jf-no-js) { - // As soon as we have JS, hide the bottom navbar, since - // it's just there as a no-JS fallback. - .hero-foot nav.navbar { - // We just want to hide it rather than remove it - // entirely, to avoid jank. - opacity: 0; - } - - // We want to gently (but quickly) fade-in elements that - // were hidden while we were loading JS, to avoid - // jank. - - @media screen and (max-width: $desktop - 1) { - .navbar-burger { - animation-duration: 0.5s; - animation-name: jf-fadein; - } - } - - @media screen and (min-width: $desktop) { - .hero-head .navbar-link { - animation-duration: 0.5s; - animation-name: jf-fadein; - } - } -} - -// Avoid displaying the branding on the footer navbar, it's -// too repetitive. -.hero-foot .navbar-brand { - display: none; -} - -.hero-foot nav.navbar { - transition: opacity 1s; -} - -// Styles to display on systems that either have -// no JS, or which haven't yet loaded it. -html.jf-no-js { - .hero-foot nav.navbar { - animation-duration: $jf-long-load-animation-time; - animation-name: jf-delayed-fadein; - } - - @media screen and (max-width: $desktop - 1) { - // Don't show the hamburger at all, since people - // will just be confused by its lack of interactivity. - .navbar-burger { - visibility: hidden; - } - - .hero-foot .navbar-menu { - @include autoexpand-fallback(); - } - } - - @media screen and (min-width: $desktop) { - // Don't show any links with dropdowns in the top - // navbar, since they won't be interactive. - .hero-head .navbar-link { - visibility: hidden; - } - - .hero-foot .navbar-item .navbar-dropdown { - @include autoexpand-fallback(); - } - } -} diff --git a/frontend/sass/_safe-mode.scss b/frontend/sass/_safe-mode.scss new file mode 100644 index 000000000..f67849d2c --- /dev/null +++ b/frontend/sass/_safe-mode.scss @@ -0,0 +1,24 @@ +aside.safe-mode { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 1em; + z-index: 1000; +} + +.safe-mode-disable, .safe-mode-disable .notification { + background-color: $justfix-blue; +} + +.safe-mode[data-safe-mode-hidden] { + display: none; +} + +html[data-safe-mode-no-js] .safe-mode[data-safe-mode-hidden] { + display: block; + + button.delete { + display: none; + } +} diff --git a/frontend/sass/styles.scss b/frontend/sass/styles.scss index a1a7b91aa..b5d9d5afa 100644 --- a/frontend/sass/styles.scss +++ b/frontend/sass/styles.scss @@ -1,7 +1,7 @@ @charset "utf-8"; @import "../../node_modules/bulma/bulma.sass"; @import "./_colors.scss"; -@import "./_no-js.scss"; +@import "./_safe-mode.scss"; .hero nav.navbar { background: $justfix-blue; @@ -196,31 +196,3 @@ .jf-autocomplete-field ul li { padding: 0.5rem; } - -aside.safe-mode { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 1em; - z-index: 1000; -} - -.safe-mode-disable { - background-color: $justfix-blue; -} - -.safe-mode-disable .notification { - // This is a TERRIBLE hack to try to ensure that the - // safe mode UI isn't overlapped by any dropdowns in - // the perma-expanded navbar above it. - @media screen and (min-width: $desktop) { - padding-top: 200px; - } - - background-color: $justfix-blue; -} - -[hidden] { - display: none; -} diff --git a/frontend/templates/frontend/safe_mode_ui.html b/frontend/templates/frontend/safe_mode_ui.html index bd9ac77a2..4e0f4ecac 100644 --- a/frontend/templates/frontend/safe_mode_ui.html +++ b/frontend/templates/frontend/safe_mode_ui.html @@ -19,7 +19,7 @@
{% else %} -