-
Notifications
You must be signed in to change notification settings - Fork 93
a11y reading options bar #5340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
StephDriver
wants to merge
56
commits into
r-v1.9.x
Choose a base branch
from
b-5337-a11y-user-text-format
base: r-v1.9.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
a11y reading options bar #5340
Changes from 55 commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
73b33d1
a11y: #5337 add opendyslexic font
StephDriver 255a5ec
a11y: #5337 add user reading options bar
StephDriver dfe9545
a11y: #5337 add reading options bar to articles
StephDriver a104406
a11y: #5337 reading options bar styling
StephDriver 18d5abe
a11y: #5337 remove missing css link
StephDriver 52297fa
a11y #5337 make reading bar generic
StephDriver ede9a0b
a11y: #5337 add reading bar to cms pages
StephDriver f8c741e
a11y: #5337 add reading bar to a11y info page
StephDriver b251240
a11y: #5337 extend reading bar for multiple regions
StephDriver 0f294f6
a11y: #5337 add reading bar to news items
StephDriver 65b7bf5
a11y: #5337 add reading bar to preprints
StephDriver 8508928
fix: preprint html not displaying in OLH
StephDriver 151a379
a11y: #5337 text-formats get colour padding
StephDriver 5402b69
a11y: #5337 create reading options bar dev doc
StephDriver dafca6f
a11y: #5337 smooth text format colour transitions
StephDriver e332f4f
a11y #5337 layer reading bar behind nav dropdowns
StephDriver 9a4c295
a11y: #5337 combine with a11y-mode for generic persistence
StephDriver 10c1024
a11y: #5337 single source for text format options
StephDriver 3d4aad3
a11y:#5337 paint custom text colours on page load
StephDriver 947f73a
a11y: #5337 toggle reading bar visibility
StephDriver efcfdbd
a11y #5337 fix select widths to avoid reflow on change
StephDriver 43eae57
fix: consistent focus visible style on a11y menu
StephDriver b71942a
a11y: #5337 reading bar focus visible
StephDriver 3f0a61b
a11y: #5337 reading opt drop-down above cms toc
StephDriver 54e77ea
a11y: #5337 show and hide the reading options
StephDriver 6a8ab05
a11y: #5337 hide custom colour picker until future work
StephDriver 72a8177
a11y: #5337 aria to reflect reading options current state
StephDriver 8625192
a11y: #5337 handle focus on show and hide of reading options
StephDriver c770033
a11y: #5337 reading options show hide on mobile
StephDriver 53e44f8
fix: #5337 olh mobile reading options overlaped other content
StephDriver 069d6e6
a11y: #5337 reading bar dropdowns keyboard accessible
StephDriver aba1d3b
a11y: #5337 reading bar changes announced to screen reader
StephDriver 1fe38d0
a11y: #5337 reading bar update colours and announcements
StephDriver b028fce
fix: #5337 reading bar olh mobile style like other themes
StephDriver 48731a6
a11y: #5337 user toggle for draw attention highlighting
StephDriver 0613895
minor: #5337 ruff format
StephDriver 262a2ae
a11y: #5337 replace email logic from before branch
StephDriver 0814d21
a11y: #5337 style tables for reading colour options
StephDriver c2bf4bb
a11y: #5337 style blockquotes for reading colour options
StephDriver 3f072df
a11y: #5337 style olh mobile reading bar
StephDriver c1e607c
a11y: #5337 reading options tests to use registry values
StephDriver b749e27
a11y: #5337 add migration to store text format preferences
StephDriver 07a6b61
a11y: #5337 tidy reading options bar doc
StephDriver 7f91c43
fix: make webkit scrollbars visible
StephDriver 74c71eb
a11y: #5337 readingbar article table styles
StephDriver e62bf8c
a11y: #5337 table-modal added to readingbar
StephDriver 6cbf468
a11y #5337 wrap clean mobile doi-links
StephDriver 58f5263
a11y #5337 clean mobile text resizing styles
StephDriver 9ed7b69
a11y: #5337 tidy doc at end of readingbar work
StephDriver 6d93c04
a11y: #5337 remove z-index stack notes from during development
StephDriver 0d3fd67
a11y: #5337 consolidate reading bar tests
StephDriver 00fce79
a11y: #5337 resolve migration conflict
StephDriver 1c648d7
Revert "a11y #5337 wrap clean mobile doi-links"
StephDriver 9ed6641
a11y: #5337 wrap long links when reading options cause overflow
StephDriver b67f290
doc: #5337 user preference storage across login and out
StephDriver 29fb80c
a11y: #5337 set reading-options-bar default to hidden
StephDriver File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| # Reader Preference Storage | ||
|
|
||
| Janeway persists small, reader-chosen preferences (e.g. **accessibility mode**, | ||
| the **reading options bar**'s font/colour/size) the same way: on the | ||
| `Account` for logged-in readers, in the session for anonymous ones, with the | ||
| anonymous choice carried onto the account on login and carried back into the | ||
| session on logout. This generic store lives in `src/core/logic.py` and was | ||
| generalised from the original accessibility-mode work so a new preference | ||
| doesn't have to reinvent that persistence. | ||
|
|
||
| > [!WARNING] | ||
| > This store is for small, low-stakes reader-facing choices only — never | ||
| > secrets. Values are plainly loaded: the reading-options bar template seeds | ||
| > its stored value straight into the page as visible JSON (`json_script`), | ||
| > and even a preference that's never rendered into a page still sits in | ||
| > Django's session and in a plain `Account` field, readable by anyone with | ||
| > session or admin/model access. Don't put tokens, keys, or anything | ||
| > sensitive through this mechanism. | ||
|
|
||
| ## How it works | ||
|
|
||
| A preference is one `PreferenceDescriptor`: a session key, the matching | ||
| `Account` field, the "unset" default, and a `clean` function that sanitises a | ||
| raw value (identity if you don't pass one). | ||
|
|
||
| ```python | ||
| ACCESSIBILITY_MODE_DESCRIPTOR = PreferenceDescriptor( | ||
| session_key="accessibility_mode", | ||
| account_field="accessibility_mode", | ||
| default=False, | ||
| clean=bool, | ||
| ) | ||
| ``` | ||
|
|
||
| ## Login and logout behaviour | ||
|
|
||
| ### Principles | ||
| - **Anonymous default is off** (or whatever `default` is registered as). | ||
| - **On login**, if the reader explicitly changed the preference during the | ||
| anonymous session, that choice wins and updates the account. If they never | ||
| touched it, the account's existing preference is applied instead. This | ||
| matters so a reader who switches a preference on to use the site doesn't | ||
| lose it just by logging in. | ||
| - **The preference is sticky across logout** — logging out never changes the | ||
| current value; the account simply keeps whatever it last had saved. That | ||
| isn't necessarily the same value the anonymous session held before they | ||
| first logged in, since it could have been changed again while logged in. | ||
|
|
||
| ### State Storage | ||
| | Reader state | Storage location | Lifetime | | ||
| | --- | --- | --- | | ||
| | Anonymous, never touched it | nowhere — reads fall back to `default` | n/a | | ||
| | Anonymous, explicitly changed it | `request.session[session_key]` | Until the session expires or the browser is closed, or until the reader logs in (then migrated onto the account and cleared) | | ||
| | Logged in | `Account.<account_field>` | Indefinite — follows the reader across devices and sessions until changed again | | ||
| | After logout | re-seeded into the fresh session from the account's last value, only if it isn't `default` | Until the session expires/browser closes, or until the reader logs in again | | ||
|
|
||
|
|
||
|
|
||
| ### Login: value changes | ||
|
|
||
| | Anonymous session | Account before | Value after login | Account after login | Rule | | ||
| | --- | --- | --- | --- | --- | | ||
| | Untouched | Off | Off | Off | Nothing to apply | | ||
| | Untouched | On | On | On | Account preference applied | | ||
| | Explicit On | Off | On | On ← updated | Explicit action wins and writes back | | ||
| | Explicit On | On | On | On | Consistent | | ||
| | Explicit Off | Off | Off | Off | Consistent | | ||
| | Explicit Off | On | Off | Off ← updated | Explicit Off wins and writes back, even though it *disables* a previously-enabled account preference — this is the case naive "value equals default, so treat it as untouched" logic would get wrong; what matters is that the session *key is present*, not what it's set to | | ||
|
|
||
| ### Logout: value changes | ||
|
|
||
| Simpler than login — no explicit/untouched distinction, since there's no new | ||
| choice being made. Whatever the account holds is just carried forward. | ||
|
|
||
| | Mode at logout | Account | Value after logout | Account after logout | Rule | | ||
| | --- | --- | --- | --- | --- | | ||
| | On | On | On | On | Stays on — sticky | | ||
| | Off | Off | Off | Off | Stays off | | ||
|
|
||
| ## Adding a new preference | ||
|
|
||
| 1. **Add the field** to `Account` (e.g. a `BooleanField`, `CharField`, or | ||
| `JSONField`) and its migration. | ||
| 2. **Write a `clean` function** if the value needs sanitising beyond a plain | ||
| type coercion — see `clean_text_format_preferences()` for a dict example | ||
| that drops anything unrecognised. A bare bool can just pass `clean=bool`. | ||
| 3. **Register a descriptor** in `src/core/logic.py` and add it to | ||
| `PREFERENCE_DESCRIPTORS`: | ||
|
|
||
| ```python | ||
| MY_PREFERENCE_DESCRIPTOR = PreferenceDescriptor( | ||
| session_key="my_preference", | ||
| account_field="my_preference", | ||
| default=False, # must match the value logout treats as "nothing to reseed" | ||
| clean=my_clean_function, | ||
| ) | ||
| PREFERENCE_DESCRIPTORS = [ | ||
| ACCESSIBILITY_MODE_DESCRIPTOR, | ||
| TEXT_FORMAT_PREFERENCES_DESCRIPTOR, | ||
| MY_PREFERENCE_DESCRIPTOR, | ||
| ] | ||
| ``` | ||
|
|
||
| 4. **Read it** with `get_preference(request, MY_PREFERENCE_DESCRIPTOR)` | ||
| 5. **Write your own save view.** | ||
| 6. **Tests.** The full login/logout truth table (untouched vs. explicit | ||
| on/off/reset) is already exercised once, generically, against the bool | ||
| case in `AccessibilityModePersistenceTests`. Add tests only for what's | ||
| actually specific to your value | ||
|
|
||
| ### Reserved consideration: `default` | ||
|
|
||
| `default` isn't just a read fallback — `reseed_session_preferences` only | ||
| re-seeds a session key when the account value is `!= default`, so it also | ||
| defines what "nothing set" means for logout. | ||
|
|
||
| ## See also | ||
|
|
||
| `docs/md/reading-options-bar.md` is a worked example of a preference (a dict | ||
| of font/colour/size choices) built on top of this store, including its own | ||
| save endpoint and JS-side seeding of the stored value into the page. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # Reading Options Bar | ||
|
|
||
| The **Reading Options bar** is a control bar that lets a reader adjust how | ||
| text is displayed — e.g. **font**, **text size**, **colour scheme**. | ||
| On article pages it also carries the article-only options (e.g email author). | ||
|
|
||
| ## Adding the *reading options bar* to a page template | ||
| This bar may be added to pages which extend a `base.html` template. | ||
|
|
||
| 1. **Include the bar** where you want the controls to appear. | ||
|
|
||
| ```django | ||
| {% block reading_options_bar %} | ||
| {% include "common/elements/journal/reading_options_bar.html" %} | ||
| {% endblock reading_options_bar %} | ||
| ``` | ||
|
|
||
| 2. **Mark the elements** you want it to affect with the class `.text-format-region`. | ||
| This will affect all child-elements of the marked element. | ||
| You may markup multiple elements on the same page. | ||
|
|
||
| The reading options are intended to assist with focusing on the main text on a page. | ||
| Do not markup the side content, `nav` or other *distractions* without good reason. | ||
|
|
||
| ## Extending the reading text-format options | ||
|
|
||
| ### Adding a new font | ||
|
|
||
| If the font is a web-safe stack (e.g. system serif/sans), only step 3 is required. | ||
|
|
||
| 1. Put the font files in `src/static/common/fonts/<family>/` (`.woff2`/`.woff` | ||
| preferred; `.otf`/`.ttf` work too). Include the font's licence file. | ||
| 2. Add an `@font-face` block to `src/static/common/css/text_readability.css`: | ||
|
|
||
| ```css | ||
| @font-face { | ||
| font-family: "MyFont"; | ||
| src: url("../fonts/myfont/MyFont-Regular.woff2") format("woff2"); | ||
| font-weight: normal; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } | ||
| ``` | ||
|
|
||
| Add a second block with `font-weight: bold` for the bold face if you have one. | ||
|
|
||
| 3. Add an entry to in `src/core/text_format.py`. The key is an arbitrary slug | ||
|
|
||
| ```python | ||
| FONTS = { | ||
| # ...existing entries... | ||
| "myfont": {"label": _("My Font"), "value": '"MyFont", Verdana, sans-serif'}, | ||
| } | ||
| ``` | ||
|
|
||
| ### Adding a new colour scheme | ||
|
|
||
| A scheme is a **pair of two colours**. The Light/Dark toggle decides which is | ||
| background and which is foreground. So pick two colours with **good contrast against each | ||
| other** or they will not be readable! | ||
|
|
||
| **For WCAG 2.2AA compliance go for 1:4.5 minimum.** | ||
|
|
||
| ```python | ||
| COLOUR_SCHEMES = { | ||
| # ...existing entries... | ||
| "pink": {"label": _("Pink"), "light": "#ffd6e8", "dark": "#3a0b22"}, | ||
|
|
||
| # ... customise should remain as the final entry | ||
| "customise": {"label": _("Customise"), "light": None, "dark": None}, | ||
| } | ||
| ``` | ||
|
|
||
| #### Reserved colour scheme keys | ||
|
|
||
| - **`default`** is special-cased in `applyPreferences()`: in **Light** it applies | ||
| *no* override (the genuine theme colours show); in **Dark** it applies a dark | ||
| mode (`#1a1a1a` background / `#ffffff` text). Selecting `default` also resets | ||
| the mode to Light and re-shows italics. Your new schemes do **not** get this | ||
| special-casing — they always apply their pair. | ||
| - **`customise`** reads the two `<input type="color">` values via | ||
| `setCustomLight` / `setCustomDark`; the Light/Dark swap applies to it too. | ||
|
|
||
| ### Adding a new setting | ||
| Adding a brand-new *kind* of setting (not just another font or colour) is three | ||
| matching edits: | ||
| - an update to the `reading_options_bar.html` template | ||
| - a field on the JS `state` object, | ||
| - a validation branch in`clean_text_format_preferences()` | ||
| - tests to cover the new setting. If it's a toggle then it only needs to be added to the `TOGGLE_FLAGS` in `src/core/logic.py` and it will be tested with the other toggles. | ||
|
|
||
| ## How preferences persist | ||
|
|
||
| A reader's choices are stored **server-side** and restored on the next page load | ||
| as per the accessibility mode preference: | ||
|
|
||
| - **Logged-in readers** — on the `Account.text_format_preferences` JSON field. | ||
| - **Anonymous readers** — in the session. On login, a choice the reader made | ||
| while anonymous is written back to their account (an untouched session leaves | ||
| the account value standing). | ||
|
|
||
| The bar template seeds the stored values into the page as JSON | ||
| (`{{ text_format_preferences|json_script:"tf-preferences" }}`). The JS reads them | ||
| in `loadPreferences()` before the first apply, and `savePreferences()` POSTs the | ||
| whole `state` object back with a short wait so that rapid changes collapse into a single request, and failing silently if it doesn't succeed to the `save_text_format_preferences` endpoint. | ||
|
|
||
| ### Validation | ||
| The save endpoint runs every value through | ||
| `clean_text_format_preferences()` in `src/core/logic.py`, which **drops anything | ||
| it doesn't recognise**. Validation derives its allow-lists from the | ||
| `src/core/text_format.py` registry — the single source of truth. | ||
|
|
||
| Note: `serialiseState()` already sends every non-function property of `state` | ||
| automatically. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this file exist?