From 6dedba403ad4a425d4894aa6dd18175c3d87173b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Sat, 19 Aug 2023 12:57:36 +0300 Subject: [PATCH 1/8] feat(clerk-js): Add NotificationBadge --- .../OrganizationSwitcher.tsx | 8 +- .../OrganizationSwitcherTrigger.tsx | 84 +++++++++++++++++-- .../clerk-js/src/ui/customizables/index.ts | 4 + .../src/ui/primitives/NotificationBadge.tsx | 47 +++++++++++ packages/clerk-js/src/ui/primitives/index.ts | 1 + .../src/ui/styledSystem/animations.ts | 21 +++++ packages/types/src/appearance.ts | 1 + 7 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 packages/clerk-js/src/ui/primitives/NotificationBadge.tsx diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx index c0b08ad817..f2dc5c28a2 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -1,11 +1,10 @@ import { withOrganizationsEnabledGuard } from '../../common'; -import { useCoreOrganizationList, withCoreUserGuard } from '../../contexts'; +import { withCoreUserGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Popover, withCardStateProvider, withFloatingTree } from '../../elements'; import { usePopover } from '../../hooks'; import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover'; import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger'; -import { organizationListParams } from './utils'; const _OrganizationSwitcher = withFloatingTree(() => { const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({ @@ -13,11 +12,6 @@ const _OrganizationSwitcher = withFloatingTree(() => { offset: 8, }); - /** - * Prefetch user invitations and suggestions - */ - useCoreOrganizationList(organizationListParams); - return ( & { isOpen: boolean; }; +function useEnterAnimation() { + const prefersReducedMotion = usePrefersReducedMotion(); + + const getFormTextAnimation = useCallback( + (enterAnimation: boolean): ThemableCssProp => { + if (prefersReducedMotion) { + return { + animation: 'none', + }; + } + return t => ({ + animation: `${enterAnimation ? animations.notificationAnimation : animations.outAnimation} ${ + t.transitionDuration.$textField + } ${t.transitionTiming.$slowBezier} 0s 1 normal forwards`, + }); + }, + [prefersReducedMotion], + ); + + return { + getFormTextAnimation, + }; +} + export const OrganizationSwitcherTrigger = withAvatarShimmer( forwardRef((props, ref) => { + const { getFormTextAnimation } = useEnterAnimation(); const { sx, ...rest } = props; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { username, primaryEmailAddress, primaryPhoneNumber, ...userWithoutIdentifiers } = useCoreUser(); const { organization } = useCoreOrganization(); const { hidePersonal } = useOrganizationSwitcherContext(); + /** + * Prefetch user invitations and suggestions + */ + const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams); + + const notificationCount = (userInvitations.count ?? 0) + (userSuggestions.count ?? 0); + + const notificationCountAnimated = useDelayedVisibility(notificationCount, 100); + return ( ); }), diff --git a/packages/clerk-js/src/ui/customizables/index.ts b/packages/clerk-js/src/ui/customizables/index.ts index 353afc3b2a..44c9620c0c 100644 --- a/packages/clerk-js/src/ui/customizables/index.ts +++ b/packages/clerk-js/src/ui/customizables/index.ts @@ -44,6 +44,10 @@ export const Badge = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitive defaultDescriptor: descriptors.badge, }); +export const NotificationBadge = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.NotificationBadge)), { + defaultDescriptor: descriptors.notificationBadge, +}); + export const Table = makeCustomizable(sanitizeDomProps(Primitives.Table)); export const Thead = makeCustomizable(sanitizeDomProps(Primitives.Thead)); export const Tbody = makeCustomizable(sanitizeDomProps(Primitives.Tbody)); diff --git a/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx b/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx new file mode 100644 index 0000000000..7e08f8d6dc --- /dev/null +++ b/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx @@ -0,0 +1,47 @@ +import type { PropsOfComponent, StyleVariants } from '../styledSystem'; +import { common, createCssVariables, createVariants } from '../styledSystem'; +import { Flex } from './Flex'; + +const vars = createCssVariables('accent', 'bg'); + +const { applyVariants, filterProps } = createVariants(theme => ({ + base: { + color: vars.accent, + backgroundColor: vars.bg, + borderRadius: theme.radii.$sm, + height: theme.space.$4, + minWidth: theme.space.$4, + padding: `${theme.space.$0x5}`, + display: 'inline-flex', + }, + variants: { + textVariant: { ...common.textVariants(theme) }, + colorScheme: { + primary: { + // [vars.accent]: theme.colors.$primary500, + // [vars.bg]: colors.setAlpha(theme.colors.$primary400, 0.2), + + [vars.accent]: theme.colors.$colorTextOnPrimaryBackground, + [vars.bg]: theme.colors.$primary500, + }, + }, + }, + defaultVariants: { + colorScheme: 'primary', + textVariant: 'extraSmallRegular', + }, +})); + +// @ts-ignore +export type NotificationBadgeProps = PropsOfComponent & StyleVariants; + +export const NotificationBadge = (props: NotificationBadgeProps) => { + return ( + + ); +}; diff --git a/packages/clerk-js/src/ui/primitives/index.ts b/packages/clerk-js/src/ui/primitives/index.ts index 9efed6bf07..fa81bf923f 100644 --- a/packages/clerk-js/src/ui/primitives/index.ts +++ b/packages/clerk-js/src/ui/primitives/index.ts @@ -26,3 +26,4 @@ export * from './Tbody'; export * from './Tr'; export * from './Th'; export * from './Td'; +export * from './NotificationBadge'; diff --git a/packages/clerk-js/src/ui/styledSystem/animations.ts b/packages/clerk-js/src/ui/styledSystem/animations.ts index 51e3074599..a32b2494e8 100644 --- a/packages/clerk-js/src/ui/styledSystem/animations.ts +++ b/packages/clerk-js/src/ui/styledSystem/animations.ts @@ -45,6 +45,26 @@ const inAnimation = keyframes` } `; +const notificationAnimation = keyframes` + 0% { + opacity: 0; + transform: translateX(-5px) translateY(5px) scale(.5); + max-height: 0; + } + + 50% { + opacity: 1; + transform: translateX(0px) translateY(0px) scale(1.2); + max-height: 6rem; + } + + 100% { + opacity: 1; + transform: translateX(0px) translateY(0px) scale(1); + max-height: 6rem; + } +`; + const outAnimation = keyframes` 20% { opacity: 1; @@ -102,4 +122,5 @@ export const animations = { navbarSlideIn, inAnimation, outAnimation, + notificationAnimation, }; diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index d3a1074e65..7e259e651d 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -418,6 +418,7 @@ export type ElementsConfig = { // default descriptors badge: WithOptions<'primary' | 'actionRequired', never, never>; + notificationBadge: WithOptions; button: WithOptions; providerIcon: WithOptions; }; From 342cf6bae894217e259897af42e31c55db12e123 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 21 Aug 2023 16:44:41 +0300 Subject: [PATCH 2/8] chore(clerk-js): Add notification badge descriptor --- packages/clerk-js/src/ui/customizables/elementDescriptors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index 27bef4e412..fbd12897e5 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -262,6 +262,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'qrCodeContainer', 'badge', + 'notificationBadge', 'button', 'providerIcon', // Decide if we want to keep the keys as camel cased in HTML as well, From c02e83e164fe0199efa6b5c3451e383ad2402113 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 23 Aug 2023 13:40:15 +0300 Subject: [PATCH 3/8] chore(clerk-js): Show/hide selector icon when notification badge is visible --- .../OrganizationSwitcherTrigger.tsx | 33 ++++++++++++------- .../src/ui/styledSystem/animations.ts | 10 +++--- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx index eed88a50fc..26a4a1fdc8 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx @@ -22,16 +22,16 @@ function useEnterAnimation() { const prefersReducedMotion = usePrefersReducedMotion(); const getFormTextAnimation = useCallback( - (enterAnimation: boolean): ThemableCssProp => { + (enterAnimation: boolean, delay?: number): ThemableCssProp => { if (prefersReducedMotion) { return { animation: 'none', }; } return t => ({ - animation: `${enterAnimation ? animations.notificationAnimation : animations.outAnimation} ${ - t.transitionDuration.$textField - } ${t.transitionTiming.$slowBezier} 0s 1 normal forwards`, + animation: `${animations.notificationAnimation} ${t.transitionDuration.$textField} ${ + t.transitionTiming.$slowBezier + } ${delay ?? 0}ms 1 ${enterAnimation ? 'normal' : 'reverse'} forwards`, }); }, [prefersReducedMotion], @@ -91,11 +91,6 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer( } /> )} - ({ opacity: t.opacity.$sm, marginLeft: `${t.space.$2}` })} - /> {(notificationCountAnimated ?? 0) > 0 ? ( {notificationCountAnimated} ) : null} + + ({ + marginLeft: `${t.space.$2}`, + '--organization-switcher-icon-opacity': t.opacity.$sm, + opacity: 'var(--organization-switcher-icon-opacity)', + }), + !!notificationCountAnimated && getFormTextAnimation(!notificationCountAnimated), + ]} + /> + {/*{(notificationCountAnimated ?? 0) > 0 ? (*/} {/* Date: Thu, 24 Aug 2023 12:42:34 +0300 Subject: [PATCH 4/8] Revert "chore(clerk-js): Show/hide selector icon when notification badge is visible" This reverts commit a5cbdc923a87bf1adba4fdaccb6335ca2c3587dd. --- .../OrganizationSwitcherTrigger.tsx | 33 +++++++------------ .../src/ui/styledSystem/animations.ts | 10 +++--- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx index 26a4a1fdc8..eed88a50fc 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx @@ -22,16 +22,16 @@ function useEnterAnimation() { const prefersReducedMotion = usePrefersReducedMotion(); const getFormTextAnimation = useCallback( - (enterAnimation: boolean, delay?: number): ThemableCssProp => { + (enterAnimation: boolean): ThemableCssProp => { if (prefersReducedMotion) { return { animation: 'none', }; } return t => ({ - animation: `${animations.notificationAnimation} ${t.transitionDuration.$textField} ${ - t.transitionTiming.$slowBezier - } ${delay ?? 0}ms 1 ${enterAnimation ? 'normal' : 'reverse'} forwards`, + animation: `${enterAnimation ? animations.notificationAnimation : animations.outAnimation} ${ + t.transitionDuration.$textField + } ${t.transitionTiming.$slowBezier} 0s 1 normal forwards`, }); }, [prefersReducedMotion], @@ -91,6 +91,11 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer( } /> )} + ({ opacity: t.opacity.$sm, marginLeft: `${t.space.$2}` })} + /> {(notificationCountAnimated ?? 0) > 0 ? ( {notificationCountAnimated} ) : null} - - ({ - marginLeft: `${t.space.$2}`, - '--organization-switcher-icon-opacity': t.opacity.$sm, - opacity: 'var(--organization-switcher-icon-opacity)', - }), - !!notificationCountAnimated && getFormTextAnimation(!notificationCountAnimated), - ]} - /> - {/*{(notificationCountAnimated ?? 0) > 0 ? (*/} {/* Date: Thu, 24 Aug 2023 13:02:44 +0300 Subject: [PATCH 5/8] feat(clerk-js): Notification Badge in OrganizationSwitcher --- .../OrganizationSwitcherTrigger.tsx | 52 +++++++------------ .../src/ui/primitives/NotificationBadge.tsx | 10 ++-- .../src/ui/styledSystem/animations.ts | 9 ++-- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx index eed88a50fc..a828be44e0 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx @@ -6,7 +6,7 @@ import { useCoreUser, useOrganizationSwitcherContext, } from '../../contexts'; -import { Button, descriptors, Icon, localizationKeys, NotificationBadge } from '../../customizables'; +import { Box, Button, descriptors, Icon, localizationKeys, NotificationBadge } from '../../customizables'; import { OrganizationPreview, PersonalWorkspacePreview, withAvatarShimmer } from '../../elements'; import { useDelayedVisibility, usePrefersReducedMotion } from '../../hooks'; import { Selector } from '../../icons'; @@ -58,7 +58,7 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer( const notificationCount = (userInvitations.count ?? 0) + (userSuggestions.count ?? 0); - const notificationCountAnimated = useDelayedVisibility(notificationCount, 100); + const notificationCountAnimated = useDelayedVisibility(notificationCount, 350); return ( ); }), diff --git a/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx b/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx index 7e08f8d6dc..265e1e71df 100644 --- a/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx +++ b/packages/clerk-js/src/ui/primitives/NotificationBadge.tsx @@ -18,9 +18,6 @@ const { applyVariants, filterProps } = createVariants(theme => ({ textVariant: { ...common.textVariants(theme) }, colorScheme: { primary: { - // [vars.accent]: theme.colors.$primary500, - // [vars.bg]: colors.setAlpha(theme.colors.$primary400, 0.2), - [vars.accent]: theme.colors.$colorTextOnPrimaryBackground, [vars.bg]: theme.colors.$primary500, }, @@ -41,7 +38,12 @@ export const NotificationBadge = (props: NotificationBadgeProps) => { {...filterProps(props)} center as='span' - css={applyVariants(props)} + css={[ + applyVariants(props), + { + lineHeight: 0, + }, + ]} /> ); }; diff --git a/packages/clerk-js/src/ui/styledSystem/animations.ts b/packages/clerk-js/src/ui/styledSystem/animations.ts index a32b2494e8..01f2f5e422 100644 --- a/packages/clerk-js/src/ui/styledSystem/animations.ts +++ b/packages/clerk-js/src/ui/styledSystem/animations.ts @@ -48,20 +48,17 @@ const inAnimation = keyframes` const notificationAnimation = keyframes` 0% { opacity: 0; - transform: translateX(-5px) translateY(5px) scale(.5); - max-height: 0; + transform: translateY(5px) scale(.5); } 50% { opacity: 1; - transform: translateX(0px) translateY(0px) scale(1.2); - max-height: 6rem; + transform: translateY(0px) scale(1.2); } 100% { opacity: 1; - transform: translateX(0px) translateY(0px) scale(1); - max-height: 6rem; + transform: translateY(0px) scale(1); } `; From 17eae2eba77d60af21adc48fa4b11c4962743468 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 24 Aug 2023 13:11:53 +0300 Subject: [PATCH 6/8] chore(clerk-js): Add changeset --- .changeset/healthy-carrots-turn.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/healthy-carrots-turn.md diff --git a/.changeset/healthy-carrots-turn.md b/.changeset/healthy-carrots-turn.md new file mode 100644 index 0000000000..68cee6e66c --- /dev/null +++ b/.changeset/healthy-carrots-turn.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Display a notification counter for organization invitations in OrganizationSwitcher From 21e6b1fb9f9815375b95488c10e0761dd693ea7c Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 24 Aug 2023 13:25:49 +0300 Subject: [PATCH 7/8] test(clerk-js): Add a case for the notification counter to appear in OrganizationSwitcher --- .../__tests__/OrganizationSwitcher.test.tsx | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx index 3b008495b8..ded0d57ea9 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx @@ -1,7 +1,7 @@ import type { MembershipRole } from '@clerk/types'; import { describe } from '@jest/globals'; -import { render } from '../../../../testUtils'; +import { render, runFakeTimers, waitFor } from '../../../../testUtils'; import { bindCreateFixtures } from '../../../utils/test/createFixtures'; import { OrganizationSwitcher } from '../OrganizationSwitcher'; import { createFakeUserOrganizationInvitation, createFakeUserOrganizationSuggestion } from './utlis'; @@ -42,6 +42,37 @@ describe('OrganizationSwitcher', () => { }); }); + describe('OrganizationSwitcherTrigger', () => { + it('shows the counter for pending suggestions and invitations', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ email_addresses: ['test@clerk.dev'] }); + }); + + fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce( + Promise.resolve({ + data: [], + total_count: 2, + }), + ); + + fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce( + Promise.resolve({ + data: [], + total_count: 3, + }), + ); + + await runFakeTimers(async () => { + const { getByText } = render(, { wrapper }); + + await waitFor(() => { + expect(getByText('5')).toBeInTheDocument(); + }); + }); + }); + }); + describe('OrganizationSwitcherPopover', () => { it('opens the organization switcher popover when clicked', async () => { const { wrapper, props } = await createFixtures(f => { From 51dcfc26d7cf5c6ada51db4b6d296dc642f1aa86 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 25 Aug 2023 01:05:55 +0300 Subject: [PATCH 8/8] chore(clerk-js): Abstract badge in orgswitcher to a component --- .../OrganizationSwitcherTrigger.tsx | 83 ++++++++----------- .../src/ui/hooks/useDelayedVisibility.ts | 5 ++ 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx index a828be44e0..b954d8ee54 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from 'react'; +import { forwardRef } from 'react'; import { useCoreOrganization, @@ -18,48 +18,14 @@ type OrganizationSwitcherTriggerProps = PropsOfComponent & { isOpen: boolean; }; -function useEnterAnimation() { - const prefersReducedMotion = usePrefersReducedMotion(); - - const getFormTextAnimation = useCallback( - (enterAnimation: boolean): ThemableCssProp => { - if (prefersReducedMotion) { - return { - animation: 'none', - }; - } - return t => ({ - animation: `${enterAnimation ? animations.notificationAnimation : animations.outAnimation} ${ - t.transitionDuration.$textField - } ${t.transitionTiming.$slowBezier} 0s 1 normal forwards`, - }); - }, - [prefersReducedMotion], - ); - - return { - getFormTextAnimation, - }; -} - export const OrganizationSwitcherTrigger = withAvatarShimmer( forwardRef((props, ref) => { - const { getFormTextAnimation } = useEnterAnimation(); const { sx, ...rest } = props; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { username, primaryEmailAddress, primaryPhoneNumber, ...userWithoutIdentifiers } = useCoreUser(); const { organization } = useCoreOrganization(); const { hidePersonal } = useOrganizationSwitcherContext(); - /** - * Prefetch user invitations and suggestions - */ - const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams); - - const notificationCount = (userInvitations.count ?? 0) + (userSuggestions.count ?? 0); - - const notificationCountAnimated = useDelayedVisibility(notificationCount, 350); - return (