@@ -59,6 +59,9 @@ export class MastodonModalElement extends LitElement {
5959 open : { type : Boolean , reflect : true } ,
6060 shareText : { type : String , attribute : false } ,
6161 instanceValue : { type : String , attribute : false } ,
62+ shareTextFocusVisible : { type : Boolean , attribute : false } ,
63+ suppressInitialInstanceFocusVisible : { type : Boolean , attribute : false } ,
64+ instanceInputFocusVisible : { type : Boolean , attribute : false } ,
6265 rememberInstance : { type : Boolean , attribute : false } ,
6366 savedInstances : { attribute : false } ,
6467 statusMessage : { type : String , attribute : false } ,
@@ -70,6 +73,9 @@ export class MastodonModalElement extends LitElement {
7073 declare open : boolean
7174 declare shareText : string
7275 declare instanceValue : string
76+ declare shareTextFocusVisible : boolean
77+ declare suppressInitialInstanceFocusVisible : boolean
78+ declare instanceInputFocusVisible : boolean
7379 declare rememberInstance : boolean
7480 declare statusMessage : string
7581 declare statusType : 'error' | 'success' | ''
@@ -86,6 +92,9 @@ export class MastodonModalElement extends LitElement {
8692 this . open = false
8793 this . shareText = ''
8894 this . instanceValue = ''
95+ this . shareTextFocusVisible = false
96+ this . suppressInitialInstanceFocusVisible = false
97+ this . instanceInputFocusVisible = false
8998 this . rememberInstance = false
9099 this . savedInstances = [ ]
91100 this . statusMessage = ''
@@ -183,6 +192,9 @@ export class MastodonModalElement extends LitElement {
183192 this . statusMessage = ''
184193 this . statusType = ''
185194 this . isSubmitting = false
195+ this . shareTextFocusVisible = false
196+ this . suppressInitialInstanceFocusVisible = true
197+ this . instanceInputFocusVisible = false
186198 this . rememberInstance = false
187199
188200 const savedInstance = getCurrentMastodonInstance ( ) ?? this . savedInstances . at ( 0 ) ?? ''
@@ -229,6 +241,9 @@ export class MastodonModalElement extends LitElement {
229241 this . statusMessage = ''
230242 this . statusType = ''
231243 this . isSubmitting = false
244+ this . shareTextFocusVisible = false
245+ this . suppressInitialInstanceFocusVisible = false
246+ this . instanceInputFocusVisible = false
232247 this . rememberInstance = false
233248
234249 if ( typeof window !== 'undefined' ) {
@@ -313,6 +328,56 @@ export class MastodonModalElement extends LitElement {
313328 }
314329 }
315330
331+ private updateFocusVisibleState (
332+ element : HTMLInputElement | HTMLTextAreaElement ,
333+ applyState : ( _isFocusVisible : boolean ) => void
334+ ) : void {
335+ const scheduleUpdate =
336+ typeof window !== 'undefined' && typeof window . requestAnimationFrame === 'function'
337+ ? window . requestAnimationFrame . bind ( window )
338+ : ( callback : FrameRequestCallback ) => setTimeout ( ( ) => callback ( 0 ) , 0 )
339+
340+ scheduleUpdate ( ( ) => {
341+ if ( typeof document === 'undefined' || document . activeElement !== element ) {
342+ return
343+ }
344+
345+ applyState ( element . matches ( ':focus-visible' ) )
346+ } )
347+ }
348+
349+ private handleShareTextFocus ( event : FocusEvent ) : void {
350+ const target = event . target
351+ if ( target instanceof HTMLTextAreaElement ) {
352+ this . updateFocusVisibleState ( target , isFocusVisible => {
353+ this . shareTextFocusVisible = isFocusVisible
354+ } )
355+ }
356+ }
357+
358+ private handleShareTextBlur ( ) : void {
359+ this . shareTextFocusVisible = false
360+ }
361+
362+ private handleInstanceFocus ( event : FocusEvent ) : void {
363+ const target = event . target
364+ if ( target instanceof HTMLInputElement ) {
365+ if ( this . suppressInitialInstanceFocusVisible ) {
366+ this . suppressInitialInstanceFocusVisible = false
367+ this . instanceInputFocusVisible = false
368+ return
369+ }
370+
371+ this . updateFocusVisibleState ( target , isFocusVisible => {
372+ this . instanceInputFocusVisible = isFocusVisible
373+ } )
374+ }
375+ }
376+
377+ private handleInstanceBlur ( ) : void {
378+ this . instanceInputFocusVisible = false
379+ }
380+
316381 protected override render ( ) {
317382 const modalTitleId = `${ this . modalId } -title`
318383 const instanceHintId = `${ this . modalId } -instance-hint`
@@ -324,6 +389,14 @@ export class MastodonModalElement extends LitElement {
324389 root : document ,
325390 } )
326391 : null
392+ const mastodonIconMarkup =
393+ typeof document !== 'undefined'
394+ ? queryMastodonIconMarkup ( {
395+ iconBankId : ICON_BANK_ID ,
396+ iconName : 'mastodon' ,
397+ root : document ,
398+ } )
399+ : null
327400
328401 return html `
329402 < div
@@ -338,14 +411,17 @@ export class MastodonModalElement extends LitElement {
338411 class ="modal-backdrop absolute inset-0 bg-black/50 backdrop-blur-sm "
339412 @click =${ ( event : Event ) => this . handleBackdropClick ( event ) }
340413 > </ div >
341- < div
342- class ="modal-content relative w-full max-w-2xl max-h-[90vh] overflow-auto bg-content-inverse rounded-lg shadow-2xl "
343- >
344- < div class ="flex items-center justify-between p-6 border-b border-trim ">
345- < h2 id =${ modalTitleId } class ="m-0 text-xl font-semibold"> Share to Mastodon</ h2 >
414+ < div class ="modal-content relative w-full max-w-2xl max-h-[90vh] overflow-auto rounded-xl bg-page-base shadow-2xl ">
415+ < div class ="bg-page-inverse px-6 py-5 flex items-center justify-between ">
416+ < div class ="flex items-center gap-3 text-content-inverse ">
417+ ${ mastodonIconMarkup ? unsafeHTML ( mastodonIconMarkup ) : null }
418+ < h2 id =${ modalTitleId } class ="m-0 text-lg font-bold text-content-inverse">
419+ Share to Mastodon
420+ </ h2 >
421+ </ div >
346422 < button
347423 type ="button "
348- class ="modal-close flex items-center justify-center w-8 h-8 p-0 border-0 bg-transparent rounded cursor-pointer transition-all duration-150 hover:bg-content-inverse-muted hover:text-content "
424+ class ="modal-close relative flex items-center justify-center w-8 h-8 rounded-full bg-white/20 border-0 cursor-pointer text-primary-inverse transition-colors hover: bg-white/30 focus-visible:outline-none after:pointer-events-none after:absolute after:content-[''] after:inset-0 after: rounded-none after:border-2 after:border-transparent after:opacity-0 after: transition-opacity after: duration-150 after:ease-out focus-visible:after:opacity-100 focus-visible:after:-inset-1 focus-visible:after:border-spotlight "
349425 aria-label ="Close modal "
350426 @click =${ ( ) => this . closeModal ( ) }
351427 >
@@ -355,32 +431,42 @@ export class MastodonModalElement extends LitElement {
355431
356432 < form
357433 id ="mastodon-share-form "
358- class ="flex flex-col gap-6 p-6 "
434+ class ="flex flex-col gap-5 p-6 bg-page-base "
359435 @submit =${ ( event : Event ) => this . handleSubmit ( event ) }
360436 >
361437 < div class ="flex flex-col gap-2 ">
362- < label for ="share-text " class ="font-medium "> Text to share</ label >
363- < textarea
364- id ="share-text "
365- name ="text "
366- rows ="4 "
367- readonly
368- class ="w-full p-3 border border-trim rounded-md bg-content-inverse-muted font-[inherit] text-sm leading-6 resize-y focus:outline-2 focus:outline-primary focus:outline-offset-2 "
369- .value =${ this . shareText }
370- > </ textarea >
438+ < label for ="share-text " class ="text-sm font-medium text-content-offset uppercase tracking-wider ">
439+ Text to share
440+ </ label >
441+ < div
442+ class =${ `relative after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent ${ this . shareTextFocusVisible ? 'after:-inset-1 after:border-spotlight' : '' } ` }
443+ >
444+ < textarea
445+ id ="share-text "
446+ name ="text "
447+ rows ="4 "
448+ readonly
449+ class ="w-full p-3 border border-trim-offset rounded-lg bg-page-offset font-[inherit] text-sm leading-6 resize-y outline-none focus:border-trim-offset focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none focus-visible:border-trim-offset focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:shadow-none "
450+ .value =${ this . shareText }
451+ @focus =${ ( event : FocusEvent ) => this . handleShareTextFocus ( event ) }
452+ @blur=${ ( ) => this . handleShareTextBlur ( ) }
453+ > </ textarea >
454+ </ div >
371455 </ div >
372456
373457 < div class ="flex flex-col gap-4 ">
374458 < label for ="mastodon-instance " class ="flex flex-col gap-2 font-medium ">
375- < span > Mastodon Instance</ span >
459+ < span class ="text-sm font-medium text-content-offset uppercase tracking-wider ">
460+ Instance
461+ </ span >
376462 < span id =${ instanceHintId } class ="sr-only"
377463 > Enter only the domain, without https://</ span
378464 >
379465 < div
380- class =" flex items-stretch border border-trim rounded-md overflow-hidden bg-content-inverse-input focus-within:outline-2 focus-within:outline-primary focus-within:outline-offset-2 "
466+ class =${ `relative flex items-stretch border border-trim-offset rounded-lg overflow-hidden bg-page-base after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent ${ this . instanceInputFocusVisible ? 'after:-inset-1 after:border-spotlight' : '' } ` }
381467 >
382468 < span
383- class ="flex items-center px-3 py-2 bg-content-inverse-muted text-sm select-none "
469+ class ="flex items-center px-3 py-2 bg-page-offset text-sm text-content-offset select-none "
384470 aria-hidden ="true "
385471 >
386472 https://
@@ -392,8 +478,10 @@ export class MastodonModalElement extends LitElement {
392478 placeholder ="mastodon.social "
393479 required
394480 aria-describedby =${ instanceHintId }
395- class ="flex-1 min-w-0 px-3 py-2 border-0 bg-transparent text-base text-content focus:outline-none"
481+ class ="flex-1 min-w-0 px-3 py-2 border-0 bg-transparent text-base text-content outline-none focus:border-0 focus: outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none focus-visible:border-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:shadow -none"
396482 .value=${ this . instanceValue }
483+ @focus=${ ( event : FocusEvent ) => this . handleInstanceFocus ( event ) }
484+ @blur=${ ( ) => this . handleInstanceBlur ( ) }
397485 @input=${ ( event : Event ) => this . handleInstanceInput ( event ) }
398486 />
399487 </ div >
@@ -417,30 +505,30 @@ export class MastodonModalElement extends LitElement {
417505 </ div > `
418506 : null }
419507
420- < label for ="remember-instance " class ="flex items-center gap-2 text-sm cursor-pointer ">
508+ < label for ="remember-instance " class ="flex items-center gap-2 text-sm cursor-pointer text-content-offset ">
421509 < input
422510 type ="checkbox "
423511 id ="remember-instance "
424512 name ="remember "
425- class ="w-4 h-4 cursor-pointer "
513+ class ="relative w-4 h-4 cursor-pointer accent-accent focus-visible:outline-none after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent focus-visible:after:-inset-1 focus-visible:after:border-spotlight "
426514 .checked =${ this . rememberInstance }
427515 @change =${ ( event : Event ) => this . handleRememberChange ( event ) }
428516 />
429517 < span > Remember this instance</ span >
430518 </ label >
431519 </ div >
432520
433- < div class ="flex gap-3 justify-end ">
521+ < div class ="flex gap-3 justify-end pt-2 ">
434522 < button
435523 type ="button "
436- class ="btn-secondary modal-cancel px-4 py-2 border border-trim rounded-md font-medium cursor-pointer transition-all duration-150 bg-transparent text-content hover:bg-content-inverse-muted "
524+ class ="btn-secondary modal-cancel relative px-5 py-2.5 border border-trim-offset rounded-lg font-medium cursor-pointer bg-transparent text-content hover:bg-page-offset transition-colors focus-visible:outline-none after:pointer-events-none after:absolute after: content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent after:opacity-0 after:transition-opacity after:duration-150 after:ease-out focus-visible:after:opacity-100 focus-visible:after:-inset-1 focus-visible:after:border-spotlight "
437525 @click =${ ( ) => this . closeModal ( ) }
438526 >
439527 Cancel
440528 </ button >
441529 < button
442530 type ="submit "
443- class ="btn-primary px-4 py-2 border-0 rounded-md font-medium cursor-pointer transition-all duration-150 bg-spotlight text-white hover:bg-primary-offset disabled:opacity-50 disabled:cursor-not-allowed "
531+ class ="btn-primary relative px-5 py-2.5 border-0 rounded-lg font-medium cursor-pointer bg-page-inverse text-content-inverse hover:bg-content-active transition-colors focus-visible:outline-none after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent after:opacity-0 after:transition-opacity after: duration-150 after:ease-out focus-visible:after:opacity-100 focus-visible:after:-inset-1 focus-visible:after:border-spotlight disabled:opacity-50 disabled:cursor-not-allowed "
444532 ?disabled =${ this . isSubmitting }
445533 >
446534 Share
@@ -467,14 +555,36 @@ export class MastodonModalElement extends LitElement {
467555 cursor : pointer;
468556 font-size : 0.875rem ;
469557 padding : 0.25rem 0.5rem ;
558+ position : relative;
470559 transition : all 0.15s ease;
471560 }
472561
562+ .saved-list : global (.saved-instance )::after {
563+ border : 2px solid transparent;
564+ border-radius : 0 ;
565+ content : '' ;
566+ inset : 0 ;
567+ opacity : 0 ;
568+ pointer-events : none;
569+ position : absolute;
570+ transition : opacity 0.15s ease;
571+ }
572+
473573 .saved-list : global (.saved-instance : hover ) {
474574 background : var (--color-primary );
475575 color : var (--color-page-base );
476576 }
477577
578+ .saved-list : global (.saved-instance : focus-visible ) {
579+ outline : none;
580+ }
581+
582+ .saved-list : global (.saved-instance : focus-visible )::after {
583+ border-color : var (--color-spotlight );
584+ inset : -0.25rem ;
585+ opacity : 1 ;
586+ }
587+
478588 .modal-status .error {
479589 color : var (--color-danger );
480590 }
0 commit comments