Add some whimsical fun with an easter egg - #13152
adamsilverstein wants to merge 26 commits into
Conversation
The revisions easter egg was removed in [24820] because the revisions UI it hooked into was rewritten and its trigger disappeared with it, not because the egg was unwanted. This gives the payload a new home: a command palette loader that returns no commands, and so stays invisible, until its phrase is typed. The 2013 original shipped as a Dean Edwards packed blob. It is unpacked here into readable source and no longer depends on jQuery, since obfuscated code is not GPL source. See #15262. The trigger phrase is still compared through the original Dvorak/QWERTY cipher so that it is not a readable string in the admin bundle; that is the only obfuscation retained, and it is aimed at users rather than developers. The payload is fetched only once the egg has been invoked, so a normal admin page load carries nothing but the loader. It renders into a self-contained overlay instead of taking over the document as the original did. See #24852.
… admin easter egg. Browser testing surfaced three problems with the overlay. The page behind it stayed scrollable, so the overlay measured 15px narrower than the viewport and left a visible scrollbar strip down the side. Scrolling is now locked on the document element while the scene plays and restored when it is dismissed. The scene inserts text one character at a time, and each character landed in the accessibility tree as its own node. The overlay is purely decorative, so it is now marked aria-hidden. Act two ended on an empty black screen with no indication of how to leave it. It now closes on the line the original used as its noscript fallback, which both ends the scene deliberately and hints that the page is still there.
JSHint enforces single-quoted strings in core JS, and jshint:core failed on this one double-quoted literal. It was written that way only because the line contains an apostrophe, so escape the apostrophe instead.
A registered command appears in the palette as soon as its phrase matches, so anyone typing near the phrase is shown the egg rather than discovering it. Nothing is registered now. A keydown handler watches for Enter while the palette is open and acts on the phrase itself, which leaves the palette showing its ordinary "No results found." state throughout, indistinguishable from a search that missed. The handler runs in the capture phase, so it is guarded on the palette being open and the phrase matching before it touches the event. Verified that ordinary palette commands still run and that Enter on a genuinely empty search does nothing. Also drops the now-unused label constant, since no label is rendered.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
|
Calling the files easter-egg-* is probably a bit too obvious, maybe we can come up with something a bit less so. |
The loader is enqueued by name on every admin screen that loads the command palette, so its src appears in the markup of an ordinary page load. A file called easter-egg-loader.js announces that something is hidden before anyone goes looking, which is most of what the egg has to offer. Name both files for what they do instead. The payload types lines onto a fullscreen overlay, so teletype.js and teletype-loader.js describe the mechanism while giving away nothing about the surprise. The script handle, the config global, and the overlay class follow. The docblocks still say plainly what this is. Minification strips them, so they never reach a production page, and a core developer reading the source should not have to guess.
|
Agreed. Renamed to |
…logue. The doc block credited WordPress 3.6, but 3.6 is where the egg was removed. r8306 added it on July 10, 2008, when trunk was 2.6-beta3, so it first shipped in 2.6 and survived through 3.5.x before r24820 dropped it. Store the dialogue in its Dvorak/QWERTY-substituted form, as the original did, so the payoff does not turn up in a plain-text search of wp-admin/js. The decoder ships alongside it and all of the logic stays readable.
Cut the provenance narrative and the rationale walkthroughs from the file headers, leaving the facts a reader of the code needs. The history belongs on the ticket.
…s own. The self-comparison line no longer describes how the egg is reached, and a self-destruct countdown risks reading as a compromised site. Announce a routine maintenance cycle instead, and drop the closing line: the scene now clears itself and hands the admin page back. See #65907.
The loader swallowed every Enter whose search text matched the phrase, so a post titled with it could never be opened from the palette. Defer to the active option whenever the palette has one. See #65907.
…act. The act was green text on black and nothing more, which is the part of the reference everyone actually pictures. Draw the rain on a canvas behind the dialogue, and sit it out when reduced motion is preferred. See #65907.
The scene was aria-hidden end to end, so a blind user got forty-five seconds of nothing while focus sat on a page they could no longer see. Make it a modal dialog that takes focus and gives it back, narrate whole lines through a live region rather than the character-by-character surface, describe the rain once, and add the visible Exit control that was missing for everyone. See #65907.
The dialogue is part of the joke and stays English, but the dialog label and Exit button are interface chrome that a non-English screen reader user meets first. Pass them through `__()` in the settings object the loader already receives, rather than giving the payload a `wp-i18n` dependency it cannot resolve when loaded by a bare script tag. Move `lang="en"` from the dialog onto the typed line and the narration region so translated chrome is not mis-tagged as English. See #65907. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A9dxZ6esbyQHgK7LaegHuS
Comments are not stripped from shipped PHP files, so the comment above the loader should not announce what it loads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXPKxNJV7v3s7b33QYVBM9
The label ships as a translatable string, so keep it to the instruction the user needs instead of describing what the dialog is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXPKxNJV7v3s7b33QYVBM9
The unminified admin scripts ship with their comments intact for SCRIPT_DEBUG, so the comments should not describe what the teletype is, how it is triggered, or how its strings are encoded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXPKxNJV7v3s7b33QYVBM9
|
I did some cleanup on the doc blocks to make the easter egg less obvious to casual observers, instead linking to the trac ticket where any sleuth can readily uncover the full details. |
|
Love this. Just tested it out and it works great! I think the command palette is an excellent spot for it too. |
|
Happy if you want to craft the commit message @aaronjorbin ;) |
peterwilsoncc
left a comment
There was a problem hiding this comment.
I've added a couple of notes inline.
I might quickly push a few things but I won't change the strings as I think that will need further thought.
| return; | ||
| } | ||
|
|
||
| var KEY = 'ishdg;rsdkot', |
There was a problem hiding this comment.
To allow for translations this will need a translator comment linking to something like this https://jsbin.com/yiraraf/edit?html,js,console,output
Given there is going to need to be a link to a spoiler anyway, I think it best to put the keyword and strings inline and allow for the fact that nerds will go searching for spoilers.
For a nerd, it's a pretty low barrier for spoiling as is so I don't think it will lose a great deal.
| narration.textContent = text; | ||
| } | ||
|
|
||
| function wait( ms, next ) { |
There was a problem hiding this comment.
Docblock.
🔢 This applies for a few functions so I won't repeat myself. A few re missing @param annotations etc too,
peterwilsoncc
left a comment
There was a problem hiding this comment.
I've pushed the following:
- Moved CSS to the script loader to allow for code highlighting via HEREDOC
- Write up a bunch of docblocks. @adamsilverstein Are you able to do a logic check in case I've got some function purposes incorrect?
I didn't push any changes for i18n because I think we'll need some solutions figured out for that but I do think it's a blocker.
According to this completely random page on the internet, the Matrix has a lot of translation so there are canonical strings available for act two in lot of languages.
As with Howdy, %s some locales may wish to translate the trigger string to another phrase more popular in their local community. As a random idea, the Japanese community is quite proud of being the birthplace of wapuu so they may wish to use that.
As mentioned inline, I think it's fine to keep the strings in plain text and accept the low risk of spoilers that comes with.
I guess it looses a little of its geeky coded text appeal, but I'm fine just using the plain strings across the board and maybe even encouraging translators to have fun with it - give it their own local flavor as you suggested. I agree searching the codebase is geeky enough for this easter egg. Maybe in a future egg we can get a bit more creative with the trigger (the original - comparing a revision to itself - was smart) and the egg itself to avoid needing translations. |
Description
Adds an admin easter egg behind the command palette, picking up the Matrix scene WordPress used to play when you compared a revision to itself. That one shipped for about a decade and came out in [24820] (see #24852) when the revisions UI was rewritten and its doorway disappeared.
The command palette is the new doorway. No command is registered: a registered command shows up in the results the moment its phrase matches, which hands the egg to anyone who happens to type near it. Instead a keydown handler watches for Enter while the palette is open and acts on the phrase itself. The palette shows its ordinary "No results found." state throughout, so typing the phrase looks exactly like a search that missed.
That empty state is also the condition. The palette's search field is a combobox, so while any command matches, one of them is the active option and Enter belongs to it - the handler checks
aria-activedescendantand stands down whenever the palette has something to run. Without that check, a post that happened to be titled with the phrase could never be opened from the palette, which @sabernhardt hit in https://core.trac.wordpress.org/ticket/65907#comment:5.The phrase is compared through the same Dvorak/QWERTY substitution cipher the original used, so it is not a readable string in the admin bundle. It can be worked out from the source with a little effort, which seems like about the right amount.
The two files are named for what they do -
teletype.jsandteletype-loader.js- rather than for what they are. The loader is enqueued by name on every admin screen with the palette, so its src is right there in the markup, and a filename with "easter egg" in it gives away most of the fun before anyone goes looking. The docblocks still say plainly what this is; minification strips them, so they never reach a production page.Trac ticket: https://core.trac.wordpress.org/ticket/65907
What changed after feedback on the ticket
@sabernhardt made a strong case that a faithful restoration lands badly, and most of it is addressed here:
prefers-reduced-motion: reducepreference.lang="en", since the dialogue is English whatever the profile language is set to.Accessibility
The first version was
aria-hiddenend to end. That made the typing bearable, but it also meant a blind user pressed Enter and got nothing at all, while focus stayed on an admin page they could no longer see. Reworked:role="dialog"witharia-modal="true", labelled "A WordPress easter egg. Press Escape to leave." It takes focus when it opens and hands focus back to wherever it came from on the way out.aria-hidden, since a character at a time is not something anyone wants read to them. Each line is instead handed whole to a polite live region as it begins typing, so the narration keeps pace with the screen rather than trailing it.aria-hidden.Screenshots
Act one, before the lights go out:
The Matrix act, with the rain running behind the dialogue:
Notes on the payload
Staging and timings follow the original. A few things needed updating after twelve years:
<blink>andtext-decoration: blinkare both gone from browsers. Replaced with a CSS@keyframesblink, plus aprefers-reduced-motionguard.html/bodyCSS and stripping nodes out of the page's first<p>. The scene now renders into its own overlay and touches nothing on the admin page.aria-hidden. Without that, text typed one character at a time lands in the accessibility tree one node at a time. The live region described above carries the dialogue instead.Only a small loader is added to admin pages. The scene itself is fetched at the moment it is invoked, so a normal admin load carries nothing else.
How has this been tested
Worth a reviewer's eye, since the handler runs in capture phase:
The scene itself was driven in a standalone harness rather than in wp-admin, which is where the screenshots above come from. Confirmed there:
prefers-reduced-motion: reducematches.The palette handler still wants a pass on a real admin screen, and the narration is worth hearing through an actual screen reader rather than trusting the live region contract.
grunt jshint:corejs,grunt typecheck:js, andphpcsonscript-loader.phpall pass on the current branch.Types of changes
src/js/_enqueues/admin/teletype.js, fetched on demand.src/js/_enqueues/admin/teletype-loader.js, enqueued with the command palette.wp_enqueue_command_palette_assets().admin-jsbuild map.Suggested commit message
AI Use
🤖 Claude Code wrote the code here and drafted this description and the suggested commit message, working from the discussion on the ticket. I will review and test.