Skip to content

Commit

Permalink
feat(js): Popover and collapse animations (novuhq#6506)
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Sep 18, 2024
1 parent 4bc709b commit 334f300
Show file tree
Hide file tree
Showing 13 changed files with 1,521 additions and 1,028 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,8 @@
"xcodebuild",
"xkeysib",
"zulip",
"zwnj"
"zwnj",
"motionone"
],
"flagWords": [],
"patterns": [
Expand Down
1 change: 1 addition & 0 deletions packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"socket.io-client": "4.7.2",
"solid-floating-ui": "^0.3.1",
"solid-js": "^1.8.11",
"solid-motionone": "^1.0.1",
"tailwind-merge": "^2.4.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ChannelRow = (props: ChannelRowProps) => {
<div
class={style(
'channelContainer',
'nt-flex nt-justify-between nt-items-center nt-h-11 data-[disabled=true]:nt-text-foreground-alpha-600'
'nt-flex nt-justify-between nt-items-center nt-h-11 nt-gap-2 data-[disabled=true]:nt-text-foreground-alpha-600'
)}
data-disabled={props.isCritical}
>
Expand Down
65 changes: 38 additions & 27 deletions packages/js/src/ui/components/elements/Preferences/Preferences.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js';
import { setDynamicLocalization } from '../../../config';
import { Presence } from 'solid-motionone';
import { Preference } from '../../../../preferences/preference';
import { ChannelPreference, ChannelType, PreferenceLevel } from '../../../../types';
import { usePreferences } from '../../../api';
import { setDynamicLocalization } from '../../../config';
import { StringLocalizationKey, useLocalization } from '../../../context';
import { useStyle } from '../../../helpers';
import { ArrowDropDown, Lock } from '../../../icons';
import { Motion } from '../../primitives';
import { Tooltip } from '../../primitives/Tooltip';
import { ChannelRow, getLabel } from './ChannelRow';
import { LoadingScreen } from './LoadingScreen';
Expand Down Expand Up @@ -198,32 +200,41 @@ const PreferencesRow = (props: {
<ArrowDropDown class={style('workflowArrow__icon', 'nt-text-foreground-alpha-600')} />
</span>
</div>
<Show when={isOpen()}>
<div class={style('channelsContainer', 'nt-flex nt-flex-col nt-gap-1 nt-self-stretch')}>
<Show when={props.isCritical}>
<span
class={style(
'workflowContainerDisabledNotice',
'nt-text-sm nt-text-foreground-alpha-600 nt-text-start'
)}
data-localization="preferences.workflow.disabled.notice"
>
{t('preferences.workflow.disabled.notice')}
</span>
</Show>
<For each={channels()}>
{(channel) => (
<ChannelRow
channel={channel as ChannelType}
enabled={!!props.channels[channel as keyof ChannelPreference]}
workflowId={props.workflowId}
onChange={props.onChange}
isCritical={props.isCritical}
/>
)}
</For>
</div>
</Show>
<Presence exitBeforeEnter>
<Show when={isOpen()}>
<Motion.div
animate={{ gridTemplateRows: ['0fr', '1fr'] }}
exit={{ gridTemplateRows: ['1fr', '0fr'] }}
transition={{ duration: 0.2, easing: 'ease-out' }}
class={style('channelsContainerCollapsible', 'nt-grid')}
>
<div class={style('channelsContainer', 'nt-overflow-hidden nt-flex-col nt-gap-1')}>
<Show when={props.isCritical}>
<span
class={style(
'workflowContainerDisabledNotice',
'nt-text-sm nt-text-foreground-alpha-600 nt-text-start'
)}
data-localization="preferences.workflow.disabled.notice"
>
{t('preferences.workflow.disabled.notice')}
</span>
</Show>
<For each={channels()}>
{(channel) => (
<ChannelRow
channel={channel as ChannelType}
enabled={!!props.channels[channel as keyof ChannelPreference]}
workflowId={props.workflowId}
onChange={props.onChange}
isCritical={props.isCritical}
/>
)}
</For>
</div>
</Motion.div>
</Show>
</Presence>
</div>
</Show>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/ui/components/elements/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const Root = (props: RootProps) => {
const style = useStyle();

return (
<div id={`novu-root-${id}`} class={(style('root'), cn('novu', id, 'nt-text-foreground nt-h-full'))} {...rest} />
<div id={`novu-root-${id()}`} class={(style('root'), cn('novu', id, 'nt-text-foreground nt-h-full'))} {...rest} />
);
};
12 changes: 12 additions & 0 deletions packages/js/src/ui/components/primitives/Motion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Motion as MotionPrimitive, MotionProxy, MotionProxyComponent } from 'solid-motionone';
import { useAppearance } from '../../context';

export const Motion = new Proxy(MotionPrimitive, {
get:
(_, tag: string): MotionProxyComponent<any> =>
(props) => {
const { animations } = useAppearance();

return <MotionPrimitive {...props} tag={tag} transition={animations() ? props.transition : { duration: 0 }} />;
},
}) as MotionProxy;
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { JSX, onCleanup, onMount, Show, splitProps } from 'solid-js';
import { Portal } from 'solid-js/web';
import { useFocusManager } from '../../../context';
import type { AppearanceKey } from '../../../types';
import { cn, useStyle } from '../../../helpers';
import type { AppearanceKey } from '../../../types';
import { Root } from '../../elements';
import { Motion } from '../Motion';
import { usePopover } from './PopoverRoot';

export const popoverContentVariants = () =>
Expand All @@ -28,11 +29,13 @@ const PopoverContentBody = (props: PopoverContentProps) => {
});

return (
<div
<Motion.div
ref={setFloating}
class={local.class ? local.class : style(local.appearanceKey || 'popoverContent', popoverContentVariants())}
style={floatingStyles()}
data-open={open()}
animate={{ opacity: [0, 1], y: [-6, 0] }}
transition={{ duration: 0.1, easing: 'ease-out' }}
{...rest}
/>
);
Expand Down
1 change: 1 addition & 0 deletions packages/js/src/ui/components/primitives/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Button';
export * from './Dropdown';
export * from './Motion';
export * from './Popover';
export * from './Tabs';
1 change: 1 addition & 0 deletions packages/js/src/ui/config/appearanceKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const appearanceKeys = [

// channel
'channelContainer',
'channelsContainerCollapsible',
'channelsContainer',
'channelLabel',
'channelLabelContainer',
Expand Down
22 changes: 15 additions & 7 deletions packages/js/src/ui/context/AppearanceContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Accessor,
createContext,
createEffect,
createMemo,
Expand All @@ -14,10 +15,11 @@ import { parseElements, parseVariables } from '../helpers';
import type { Appearance, Elements, Variables } from '../types';

type AppearanceContextType = {
variables?: Variables;
elements?: Elements;
variables: Accessor<Variables>;
elements: Accessor<Elements>;
animations: Accessor<boolean>;
appearanceKeyToCssInJsClass: Record<string, string>;
id: string;
id: Accessor<string>;
};

const AppearanceContext = createContext<AppearanceContextType | undefined>(undefined);
Expand All @@ -34,6 +36,10 @@ export const AppearanceProvider = (props: AppearanceProviderProps) => {
const themes = createMemo(() =>
Array.isArray(props.appearance?.baseTheme) ? props.appearance?.baseTheme || [] : [props.appearance?.baseTheme || {}]
);
const id = () => props.id;
const variables = () => props.appearance?.variables || {};
const elements = () => props.appearance?.elements || {};
const animations = () => props.appearance?.animations ?? true;

onMount(() => {
const el = document.getElementById(props.id);
Expand Down Expand Up @@ -85,7 +91,7 @@ export const AppearanceProvider = (props: AppearanceProviderProps) => {

const baseElements = themes().reduce<Elements>((acc, obj) => ({ ...acc, ...(obj.elements || {}) }), {});

const elementsStyleData = parseElements({ ...baseElements, ...(props.appearance?.elements || {}) });
const elementsStyleData = parseElements({ ...baseElements, ...elements() });
setStore('appearanceKeyToCssInJsClass', (obj) => ({
...obj,
...elementsStyleData.reduce<Record<string, string>>((acc, item) => {
Expand All @@ -110,9 +116,11 @@ export const AppearanceProvider = (props: AppearanceProviderProps) => {
return (
<AppearanceContext.Provider
value={{
elements: props.appearance?.elements || {},
appearanceKeyToCssInJsClass: store.appearanceKeyToCssInJsClass,
id: props.id,
elements,
variables,
appearanceKeyToCssInJsClass: store.appearanceKeyToCssInJsClass, // stores are reactive
animations,
id,
}}
>
{props.children}
Expand Down
4 changes: 2 additions & 2 deletions packages/js/src/ui/helpers/useStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export const useStyle = () => {

const appearanceClassnames = [];
for (let i = 0; i < finalAppearanceKeys.length; i += 1) {
if (typeof appearance.elements?.[finalAppearanceKeys[i]] === 'string') {
appearanceClassnames.push(appearance.elements?.[finalAppearanceKeys[i]]);
if (typeof appearance.elements()?.[finalAppearanceKeys[i]] === 'string') {
appearanceClassnames.push(appearance.elements()?.[finalAppearanceKeys[i]]);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/js/src/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type Elements = Partial<Record<AppearanceKey, ElementStyles>>;
export type Theme = {
variables?: Variables;
elements?: Elements;
animations?: boolean;
};
export type Appearance = Theme & { baseTheme?: Theme | Theme[] };

Expand Down
Loading

0 comments on commit 334f300

Please sign in to comment.