Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
5b096a7c
Commit
5b096a7c
authored
Oct 06, 2023
by
Alejandro Celaya
Committed by
Alejandro Celaya
Oct 06, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use ToastMessages from frontend-shared
parent
64ff836d
Changes
8
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
8 additions
and
371 deletions
+8
-371
ToastMessages.tsx
src/annotator/components/ToastMessages.tsx
+2
-2
sidebar.tsx
src/annotator/sidebar.tsx
+1
-1
ToastMessages.tsx
src/shared/components/ToastMessages.tsx
+0
-205
ToastMessages-test.js
src/shared/components/test/ToastMessages-test.js
+0
-159
ToastMessages.tsx
src/sidebar/components/ToastMessages.tsx
+1
-1
frame-sync.ts
src/sidebar/services/frame-sync.ts
+1
-1
toast-messenger.ts
src/sidebar/services/toast-messenger.ts
+1
-1
toast-messages.ts
src/sidebar/store/modules/toast-messages.ts
+2
-1
No files found.
src/annotator/components/ToastMessages.tsx
View file @
5b096a7c
import
{
ToastMessages
as
BaseToastMessages
}
from
'@hypothesis/frontend-shared'
;
import
type
{
ToastMessage
}
from
'@hypothesis/frontend-shared'
;
import
{
useCallback
,
useEffect
,
useState
}
from
'preact/hooks'
;
import
BaseToastMessages
from
'../../shared/components/ToastMessages'
;
import
type
{
ToastMessage
}
from
'../../shared/components/ToastMessages'
;
import
type
{
Emitter
}
from
'../util/emitter'
;
export
type
ToastMessagesProps
=
{
...
...
src/annotator/sidebar.tsx
View file @
5b096a7c
import
type
{
ToastMessage
}
from
'@hypothesis/frontend-shared'
;
import
classnames
from
'classnames'
;
import
*
as
Hammer
from
'hammerjs'
;
import
{
render
}
from
'preact'
;
import
type
{
ToastMessage
}
from
'../shared/components/ToastMessages'
;
import
{
addConfigFragment
}
from
'../shared/config-fragment'
;
import
{
sendErrorsTo
}
from
'../shared/frame-error-capture'
;
import
{
ListenerCollection
}
from
'../shared/listener-collection'
;
...
...
src/shared/components/ToastMessages.tsx
deleted
100644 → 0
View file @
64ff836d
import
type
{
TransitionComponent
}
from
'@hypothesis/frontend-shared'
;
import
{
Callout
}
from
'@hypothesis/frontend-shared'
;
import
classnames
from
'classnames'
;
import
type
{
ComponentChildren
,
ComponentProps
,
FunctionComponent
,
}
from
'preact'
;
import
{
useCallback
,
useMemo
,
useRef
,
useState
}
from
'preact/hooks'
;
export
type
ToastMessage
=
{
id
:
string
;
type
:
'error'
|
'success'
|
'notice'
;
message
:
ComponentChildren
;
/**
* Visually hidden messages are announced to screen readers but not visible.
* Defaults to false.
*/
visuallyHidden
?:
boolean
;
/**
* Determines if the toast message should be auto-dismissed.
* Defaults to true.
*/
autoDismiss
?:
boolean
;
};
export
type
ToastMessageTransitionClasses
=
{
/** Classes to apply to a toast message when appended. Defaults to 'animate-fade-in' */
transitionIn
?:
string
;
/** Classes to apply to a toast message being dismissed. Defaults to 'animate-fade-out' */
transitionOut
?:
string
;
};
type
ToastMessageItemProps
=
{
message
:
ToastMessage
;
onDismiss
:
(
id
:
string
)
=>
void
;
};
/**
* An individual toast message: a brief and transient success or error message.
* The message may be dismissed by clicking on it. `visuallyHidden` toast
* messages will not be visible but are still available to screen readers.
*/
function
ToastMessageItem
({
message
,
onDismiss
}:
ToastMessageItemProps
)
{
// Capitalize the message type for prepending; Don't prepend a message
// type for "notice" messages
const
prefix
=
message
.
type
!==
'notice'
?
`
${
message
.
type
.
charAt
(
0
).
toUpperCase
()
+
message
.
type
.
slice
(
1
)}
: `
:
''
;
return
(
<
Callout
classes=
{
classnames
({
'sr-only'
:
message
.
visuallyHidden
,
})
}
status=
{
message
.
type
}
onClick=
{
()
=>
onDismiss
(
message
.
id
)
}
variant=
"raised"
>
<
strong
>
{
prefix
}
</
strong
>
{
message
.
message
}
</
Callout
>
);
}
type
BaseToastMessageTransitionType
=
FunctionComponent
<
ComponentProps
<
TransitionComponent
>
&
{
transitionClasses
?:
ToastMessageTransitionClasses
;
}
>
;
const
BaseToastMessageTransition
:
BaseToastMessageTransitionType
=
({
direction
,
onTransitionEnd
,
children
,
transitionClasses
=
{},
})
=>
{
const
isDismissed
=
direction
===
'out'
;
const
containerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
handleAnimation
=
(
e
:
AnimationEvent
)
=>
{
// Ignore animations happening on child elements
if
(
e
.
target
!==
containerRef
.
current
)
{
return
;
}
onTransitionEnd
?.(
direction
??
'in'
);
};
const
classes
=
useMemo
(()
=>
{
const
{
transitionIn
=
'animate-fade-in'
,
transitionOut
=
'animate-fade-out'
,
}
=
transitionClasses
;
return
{
[
transitionIn
]:
!
isDismissed
,
[
transitionOut
]:
isDismissed
,
};
},
[
isDismissed
,
transitionClasses
]);
return
(
<
div
data
-
testid=
"animation-container"
onAnimationEnd=
{
handleAnimation
}
ref=
{
containerRef
}
className=
{
classnames
(
'relative w-full container'
,
classes
)
}
>
{
children
}
</
div
>
);
};
export
type
ToastMessagesProps
=
{
messages
:
ToastMessage
[];
onMessageDismiss
:
(
id
:
string
)
=>
void
;
transitionClasses
?:
ToastMessageTransitionClasses
;
setTimeout_
?:
typeof
setTimeout
;
};
/**
* A collection of toast messages. These are rendered within an `aria-live`
* region for accessibility with screen readers.
*/
export
default
function
ToastMessages
({
messages
,
onMessageDismiss
,
transitionClasses
,
/* istanbul ignore next - test seam */
setTimeout_
=
setTimeout
,
}:
ToastMessagesProps
)
{
const
[
dismissedMessages
,
setDismissedMessages
]
=
useState
<
string
[]
>
([]);
const
scheduledMessages
=
useRef
(
new
Set
<
string
>
());
const
dismissMessage
=
useCallback
(
(
id
:
string
)
=>
setDismissedMessages
(
ids
=>
[...
ids
,
id
]),
[],
);
const
scheduleMessageDismiss
=
useCallback
(
(
id
:
string
)
=>
{
if
(
scheduledMessages
.
current
.
has
(
id
))
{
return
;
}
// Track that this message has been scheduled to be dismissed. After a
// period of time, actually dismiss it
scheduledMessages
.
current
.
add
(
id
);
setTimeout_
(()
=>
{
dismissMessage
(
id
);
scheduledMessages
.
current
.
delete
(
id
);
},
5000
);
},
[
dismissMessage
,
setTimeout_
],
);
const
onTransitionEnd
=
useCallback
(
(
direction
:
'in'
|
'out'
,
message
:
ToastMessage
)
=>
{
const
autoDismiss
=
message
.
autoDismiss
??
true
;
if
(
direction
===
'in'
&&
autoDismiss
)
{
scheduleMessageDismiss
(
message
.
id
);
}
if
(
direction
===
'out'
)
{
onMessageDismiss
(
message
.
id
);
setDismissedMessages
(
ids
=>
ids
.
filter
(
id
=>
id
!==
message
.
id
));
}
},
[
scheduleMessageDismiss
,
onMessageDismiss
],
);
return
(
<
ul
aria
-
live=
"polite"
aria
-
relevant=
"additions"
className=
"w-full space-y-2"
>
{
messages
.
map
(
message
=>
{
const
isDismissed
=
dismissedMessages
.
includes
(
message
.
id
);
return
(
<
li
className=
{
classnames
({
// Add a bottom margin to visible messages only. Typically, we'd
// use a `space-y-2` class on the parent to space children.
// Doing that here could cause an undesired top margin on
// the first visible message in a list that contains (only)
// visually-hidden messages before it.
// See https://tailwindcss.com/docs/space#limitations
'mb-2'
:
!
message
.
visuallyHidden
,
})
}
key=
{
message
.
id
}
>
<
BaseToastMessageTransition
direction=
{
isDismissed
?
'out'
:
'in'
}
onTransitionEnd=
{
direction
=>
onTransitionEnd
(
direction
,
message
)
}
transitionClasses=
{
transitionClasses
}
>
<
ToastMessageItem
message=
{
message
}
onDismiss=
{
dismissMessage
}
/>
</
BaseToastMessageTransition
>
</
li
>
);
})
}
</
ul
>
);
}
src/shared/components/test/ToastMessages-test.js
deleted
100644 → 0
View file @
64ff836d
import
{
mount
}
from
'enzyme'
;
import
ToastMessages
from
'../ToastMessages'
;
describe
(
'ToastMessages'
,
()
=>
{
const
toastMessages
=
[
{
id
:
'1'
,
type
:
'success'
,
message
:
'Hello world'
,
},
{
id
:
'2'
,
type
:
'success'
,
message
:
'Foobar'
,
},
{
id
:
'3'
,
type
:
'error'
,
message
:
'Something failed'
,
},
];
let
fakeOnMessageDismiss
;
beforeEach
(()
=>
{
fakeOnMessageDismiss
=
sinon
.
stub
();
});
function
createToastMessages
(
toastMessages
,
setTimeout
)
{
const
container
=
document
.
createElement
(
'div'
);
document
.
body
.
appendChild
(
container
);
return
mount
(
<
ToastMessages
messages
=
{
toastMessages
}
onMessageDismiss
=
{
fakeOnMessageDismiss
}
setTimeout_
=
{
setTimeout
}
/>
,
{
attachTo
:
container
},
);
}
function
triggerAnimationEnd
(
wrapper
,
index
,
direction
=
'out'
)
{
wrapper
.
find
(
'BaseToastMessageTransition'
)
.
at
(
index
)
.
props
()
.
onTransitionEnd
(
direction
);
wrapper
.
update
();
}
it
(
'renders a list of toast messages'
,
()
=>
{
const
wrapper
=
createToastMessages
(
toastMessages
);
assert
.
equal
(
wrapper
.
find
(
'ToastMessageItem'
).
length
,
toastMessages
.
length
);
});
toastMessages
.
forEach
((
message
,
index
)
=>
{
it
(
'dismisses messages when clicked'
,
()
=>
{
const
wrapper
=
createToastMessages
(
toastMessages
);
wrapper
.
find
(
'Callout'
).
at
(
index
).
props
().
onClick
();
// onMessageDismiss is not immediately called. Transition has to finish
assert
.
notCalled
(
fakeOnMessageDismiss
);
// Once dismiss animation has finished, onMessageDismiss is called
triggerAnimationEnd
(
wrapper
,
index
);
assert
.
calledWith
(
fakeOnMessageDismiss
,
message
.
id
);
});
});
it
(
'dismisses messages automatically unless instructed otherwise'
,
()
=>
{
const
messages
=
[
...
toastMessages
,
{
id
:
'foo'
,
type
:
'success'
,
message
:
'Not to be dismissed'
,
autoDismiss
:
false
,
},
];
const
wrapper
=
createToastMessages
(
messages
,
// Fake internal setTimeout, to immediately call its callback
callback
=>
callback
(),
);
// Trigger "in" animation for all messages, which will schedule dismiss for
// appropriate messages
messages
.
forEach
((
_
,
index
)
=>
{
triggerAnimationEnd
(
wrapper
,
index
,
'in'
);
});
// Trigger "out" animation on components which "direction" prop is currently
// "out". That means they were scheduled for dismiss
wrapper
.
find
(
'BaseToastMessageTransition'
)
.
forEach
((
transitionComponent
,
index
)
=>
{
if
(
transitionComponent
.
prop
(
'direction'
)
===
'out'
)
{
triggerAnimationEnd
(
wrapper
,
index
);
}
});
// Only one toast message will remain, as it was marked as `autoDismiss: false`
assert
.
equal
(
fakeOnMessageDismiss
.
callCount
,
3
);
});
it
(
'schedules dismiss only once per message'
,
async
()
=>
{
const
wrapper
=
createToastMessages
(
toastMessages
,
// Fake an immediate setTimeout which does not slow down the test, but
// keeps the async behavior
callback
=>
setTimeout
(
callback
,
0
),
);
const
scheduleFirstMessageDismiss
=
()
=>
triggerAnimationEnd
(
wrapper
,
0
,
'in'
);
scheduleFirstMessageDismiss
();
scheduleFirstMessageDismiss
();
scheduleFirstMessageDismiss
();
// Once dismiss animation has finished, onMessageDismiss is called
triggerAnimationEnd
(
wrapper
,
0
);
assert
.
equal
(
fakeOnMessageDismiss
.
callCount
,
1
);
});
it
(
'invokes onTransitionEnd when animation happens on container'
,
()
=>
{
const
wrapper
=
createToastMessages
(
toastMessages
,
callback
=>
callback
());
const
animationContainer
=
wrapper
.
find
(
'[data-testid="animation-container"]'
)
.
first
();
// Trigger "in" animation for all messages, which will schedule dismiss
toastMessages
.
forEach
((
_
,
index
)
=>
{
triggerAnimationEnd
(
wrapper
,
index
,
'in'
);
});
animationContainer
.
getDOMNode
()
.
dispatchEvent
(
new
AnimationEvent
(
'animationend'
));
assert
.
called
(
fakeOnMessageDismiss
);
});
it
(
'does not invoke onTransitionEnd for animation events bubbling from children'
,
()
=>
{
const
wrapper
=
createToastMessages
(
toastMessages
,
callback
=>
callback
());
const
invalidAnimationContainer
=
wrapper
.
find
(
'Callout'
).
first
();
// Trigger "in" animation for all messages, which will schedule dismiss
toastMessages
.
forEach
((
_
,
index
)
=>
{
triggerAnimationEnd
(
wrapper
,
index
,
'in'
);
});
invalidAnimationContainer
.
getDOMNode
()
.
dispatchEvent
(
new
AnimationEvent
(
'animationend'
,
{
bubbles
:
true
}));
assert
.
notCalled
(
fakeOnMessageDismiss
);
});
});
src/sidebar/components/ToastMessages.tsx
View file @
5b096a7c
import
{
ToastMessages
as
BaseToastMessages
}
from
'@hypothesis/frontend-shared'
;
import
classnames
from
'classnames'
;
import
BaseToastMessages
from
'../../shared/components/ToastMessages'
;
import
{
withServices
}
from
'../service-context'
;
import
type
{
ToastMessengerService
}
from
'../services/toast-messenger'
;
import
{
useSidebarStore
}
from
'../store'
;
...
...
src/sidebar/services/frame-sync.ts
View file @
5b096a7c
import
type
{
ToastMessage
}
from
'@hypothesis/frontend-shared'
;
import
debounce
from
'lodash.debounce'
;
import
type
{
DebouncedFunction
}
from
'lodash.debounce'
;
import
shallowEqual
from
'shallowequal'
;
import
type
{
ToastMessage
}
from
'../../shared/components/ToastMessages'
;
import
{
ListenerCollection
}
from
'../../shared/listener-collection'
;
import
{
PortFinder
,
...
...
src/sidebar/services/toast-messenger.ts
View file @
5b096a7c
import
type
{
ToastMessage
}
from
'@hypothesis/frontend-shared'
;
import
{
TinyEmitter
}
from
'tiny-emitter'
;
import
type
{
ToastMessage
}
from
'../../shared/components/ToastMessages'
;
import
{
generateHexString
}
from
'../../shared/random'
;
import
type
{
SidebarStore
}
from
'../store'
;
...
...
src/sidebar/store/modules/toast-messages.ts
View file @
5b096a7c
import
type
{
ToastMessage
}
from
'../../../shared/components/ToastMessages'
;
import
type
{
ToastMessage
}
from
'@hypothesis/frontend-shared'
;
import
{
createStoreModule
,
makeAction
}
from
'../create-store'
;
/**
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment