-
Notifications
You must be signed in to change notification settings - Fork 888
fix: improve click UX and styling for Auth Token page #11863
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
Conversation
const { | ||
text, | ||
ctaCopy, | ||
wrapperStyles, | ||
buttonStyles, | ||
onClick: outsideOnClick, | ||
tooltipTitle = Language.tooltipTitle, | ||
} = props; |
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.
Generally try to keep prop definitions inlined, but Prettier's auto-formatting made the code super ugly to look at
onClick={(event) => { | ||
void copyToClipboard(); | ||
outsideOnClick?.(event); | ||
}} |
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.
This is the only major change for the component. The rest of the diff is just from the forwardRef
call increasing the indentation
site/src/hooks/useClipboard.ts
Outdated
// Absolutely cartoonish logic, but it's how you do things with the exec API | ||
const previousFocusTarget = document.activeElement; | ||
const dummyInput = document.createElement("input"); | ||
dummyInput.style.visibility = "hidden"; | ||
document.body.appendChild(dummyInput); | ||
dummyInput.focus(); | ||
|
||
// Confusingly, you want to use the command opposite of what you actually want | ||
// to do to interact with the execCommand method | ||
const command = operation === "read" ? "paste" : "copy"; | ||
const isExecSupported = document.execCommand(command); | ||
const value = dummyInput.value; | ||
dummyInput.remove(); | ||
|
||
if (previousFocusTarget instanceof HTMLElement) { | ||
previousFocusTarget.focus(); | ||
} |
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.
All of these changes together might be a bit overkill, but I didn't want to leave things to chance. The biggest issue before was that when the dummy input received focus, there was nothing in place to put it back on the original element once the function was done
Copy the session token below and | ||
{/* | ||
* This looks silly, but it's a case where you want to hide the space | ||
* visually because it messes up the centering, but you want the space | ||
* to still be available to screen readers | ||
*/} | ||
<span css={{ ...visuallyHidden }}>{VISUALLY_HIDDEN_SPACE}</span> | ||
<strong css={{ display: "block" }}>paste it in your terminal.</strong> |
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.
Changed the previous markup because the nowrap
styling had a risk of breaking the text rendering at smaller viewport sizes
🚀 Deploying PR 11863 ... |
On Safari, the button doesn't revert to the clipboard icon (the copy still works) Screen.Recording.2024-01-28.at.6.35.03.PM.movI haven't seen a copy UX like this (that is clipboard-aware and maintains confirmation state), but I see the immediate feedback being quite common. Here is Iconfinder, for example: https://medium.com/iconfinder/copy-to-clipboard-b4e37b6fe20b I could be wrong and this is a more common paradigm, but as much as possible I'd love to avoid complexity on this page (or manually test with various browsers) as it's a critical CLI flow |
variant="text" | ||
onClick={(event) => { | ||
void copyToClipboard(); | ||
outsideOnClick?.(event); |
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.
Why do we need this?
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.
More just for flexibility. It might be because I've been in a Backstage mindset, where we need a lot more API flexibility, but my thinking was this would let someone attach an onClick
handler without overriding the main copy functionality
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.
In the Coder app context + this component context, what would be a common use case for this?
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.
For this context, it is over-engineering, yeah. I'll remove it
site/src/hooks/useClipboard.ts
Outdated
|
||
type Result<T = unknown> = void extends T ? VoidResult : ResultWithData<T>; | ||
|
||
async function readFromClipboard(): Promise<Result<string>> { |
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.
I did not understand this part. Why do we need to read something from the clipboard? I would expect to just write things in it.
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.
Maybe listing the flow using bullet points would make things easier to understand for me.
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.
It's mainly for setting up a way to watch the clipboard, since there isn't an observer-based API for it, like there is for a lot of other browser APIs
The main idea is that isCopied
is always a derived value that comes from checking whether the text passed into the hook matches the text currently in the clipboard. And the only way to check the current clipboard is by reading from it – if we only write to it, then we can't make any assumptions about what's actually in the clipboard, because it can also change from sources outside React
So basically, there's three main "sync cues":
- The user copies our text to the clipboard
- The user changes their clipboard text while in the same page
- The user changes to a different tab, and then comes back
In all of the cases, we don't have a direct, event-based way to check the clipboard contents to verify that the content matches. You would think that writing to the clipboard would expose that information, but the event objects don't actually contain the new clipboard text. So in all three cases, the only choice is reading from it
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.
Why do we need to check if the content in the clipboard matches the one passed into the hook? Is there a use case in the app where this can happen?
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.
So, this is splitting semantic hairs, but I guess the question boils down to, "What do we want isCopied
to represent?
- Do we want it to indicate that we've just finished handling a copy operation, and this value represents how long we want to tell the user that the text is copied?
- Or do we want this property to act as a source of truth, where
isCopied
answers the question, "Each time the component renders, is the text we provided into the hook copied into the clipboard?"- If we don't want to do that, maybe that's a sign we should rename the property, but because the clipboard is one of the most mutable values in web development, the only way to have 100% confidence to that question is by checking the clipboard
That mismatch between the property name and actual functionality is what led me to consider a more synchronization-based solution, but maybe this did end up putting me on a weird, wild goose chase
site/src/hooks/useClipboard.ts
Outdated
|
||
// Comments for this function mirror the ones for readFromClipboard | ||
async function writeToClipboard(textToCopy: string): Promise<Result<void>> { | ||
if (!document.hasFocus()) { |
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.
Hmm... why is this necessary?
Thinking about the most common use case, like the user clicking the button and the value being copied, I don't see how this could happen if the document is not in focus 🤔
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.
I had a comment in readToClipboard
, but it's mainly to suppress errors when trying to do stuff in development mode
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.
Do you think is there a way to improve readability without adding a comment mentioning another comment? Thinking on a named variable or function like isDevMode
?
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.
Yeah, now that you mention it, I can use an assertion function. I'll get that swapped in
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.
I think it's always good to share screenshots and demos when making UI changes or features. Some design improvements I see we can make:
- The space between the title, description text, and code example looks strange
- I would make the "Go to workspaces" link a normal link. I think it's not that important to be considered a secondary action like a button.
- The code example looks bigger than normal input sizes to me... I think it would be nice to have it the same height.
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.
I'm a little concerned about the added complexity and some things I didn't understand, but after getting some explanation I can quickly come back to this PR and review it 👍
@bpmct @BrunoQuaresma So, Safari support is something I forgot to test for, and the more I look into things, it looks like their handling of the clipboard/navigator APIs is a little wonkier than most browsers. I could see this becoming a rabbit hole, and I think making more sweeping changes with Backstage launch coming up was a mistake Now that you mention, Ben, I also agree that reducing complexity is even more important in core user flows This is what I'm going to do:
|
@BrunoQuaresma |
@Parkreiner I liked the design changes 👍 Wondering if we should use some package for the clipboard that would take care of the complexity for us. 33K starts on GitHub https://github.com/zenorocha/clipboard.js |
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.
I'm going to leave the final decision to you 🤗 so we don't block work for Backstage integration.
@BrunoQuaresma Hopefully my tone didn't come across as bad in my responses, but I want to clear: I really appreciate your questions, and the pushback I've gotten. You're making me double-check my own assumptions, and pushing me to make the code as good as it can be |
@Parkreiner 100% |
@BrunoQuaresma Yeah, I was going to ask about that at tomorrow's FE Variety before I had to change my flight plans. I feel like the stance we've taken before is "don't bring in a package if it's easy enough to do things yourself", but just because it feels like clipboards are so surprisingly complicated, maybe bringing something in would be for the best |
No issue to link – ended up adding this to the workpile, since Blueberry decided that we'd be launching with token auth for Backstage's alpha release
Changes made
useClipboard
hook to be based on a more "synchronization-based" APIuseClipboard
CodeExample
component to have a much larger clickable area (underlying HTML semantics have not been changed)secret
prop is true (and also makes sure that the value defaults totrue
)useQuery
is integrated into the page to follow newer Coder conventionsNotes
useClipboard
because even thoughuserEvent.setup
sets up a clipboard mock automatically (it normally doesn't exist in JSDOM), I couldn't get it to trigger events predictably. If anybody has suggestions for how to get rid of my home-spun mock, I have a bunch of tacos waiting for you