Skip to content

Commit

Permalink
fix(js): Popover focus trap and dismissal(novuhq#6049)
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Jul 12, 2024
1 parent 7a65b6f commit 5ba5b0a
Show file tree
Hide file tree
Showing 34 changed files with 929 additions and 830 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@
"suported",
"subresource",
"htmlonly",
"apidevtools"
"apidevtools",
"contenteditable"
],
"flagWords": [],
"patterns": [
Expand Down
9 changes: 9 additions & 0 deletions packages/js/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.preferences.autoImportFileExcludePatterns": ["**/node_modules/**", "**/dist/**"],
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSave": true
}
9 changes: 6 additions & 3 deletions packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@
"private": true,
"scripts": {
"start": "pnpm run build -- --watch --sourcemap",
"build": "tsup && pnpm run post:build",
"pre:build": "cp ./src/ui/index.css ./src/ui/index.directcss",
"build": "pnpm run pre:build && tsup && pnpm run post:build",
"build:umd": "webpack --config webpack.config.cjs",
"build:watch": "tsup --watch",
"post:build": "node scripts/size-limit.mjs",
"post:build": "rm ./src/ui/index.directcss && node scripts/size-limit.mjs",
"lint": "eslint --ext .ts,.tsx src",
"test": "jest"
},
Expand Down Expand Up @@ -83,10 +84,12 @@
"dependencies": {
"@floating-ui/dom": "^1.6.7",
"@novu/client": "workspace:*",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"mitt": "^3.0.1",
"socket.io-client": "4.7.2",
"solid-floating-ui": "^0.3.1",
"solid-js": "^1.8.11"
"solid-js": "^1.8.11",
"tailwind-merge": "^2.4.0"
}
}
10 changes: 0 additions & 10 deletions packages/js/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,5 @@ module.exports = {
plugins: {
autoprefixer: {},
tailwindcss: {},
'postcss-prefix-selector': {
transform: function (_, selector) {
// Prefix each class selector with :where(.class)
if (selector.startsWith('.')) {
return `:where(${selector})`;
}

return selector; // Return other selectors unchanged
},
},
},
};
12 changes: 2 additions & 10 deletions packages/js/src/ui/components/Bell/DefaultBellContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Show } from 'solid-js';
import { useAppearance } from '../../context';
import { cn, useStyle } from '../../helpers';
import { useStyle } from '../../helpers';
import { BellIcon } from '../../icons';

type DefaultBellContainerProps = {
Expand All @@ -9,19 +8,12 @@ type DefaultBellContainerProps = {

export const BellContainer = (props: DefaultBellContainerProps) => {
const style = useStyle();
const { id } = useAppearance();

return (
<span
class={style(
'bellContainer',
cn(
id,
`nt-h-6 nt-w-6 nt-flex nt-justify-center
nt-items-center nt-rounded-md nt-relative
hover:nt-bg-foreground-alpha-50
nt-text-foreground-alpha-600 nt-cursor-pointer`
)
`nt-h-6 nt-w-6 nt-flex nt-justify-center nt-items-center nt-rounded-md nt-relative hover:nt-bg-foreground-alpha-50 nt-text-foreground-alpha-600 nt-cursor-pointer`
)}
>
<BellIcon />
Expand Down
16 changes: 16 additions & 0 deletions packages/js/src/ui/components/Dropdown/DropdownContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ComponentProps, splitProps } from 'solid-js';
import { AppearanceKey } from '../../context';
import { useStyle } from '../../helpers';
import { Popover } from '../Popover';

export const dropdownContentVariants = () =>
'nt-w-max nt-rounded-lg nt-overflow-hidden nt-flex nt-flex-col nt-min-w-52 nt-shadow-[0_5px_20px_0_rgba(0,0,0,0.20)] nt-z-10 nt-bg-background';

export const DropdownContent = (props: ComponentProps<typeof Popover.Content> & { appearanceKey: AppearanceKey }) => {
const style = useStyle();
const [local, rest] = splitProps(props, ['appearanceKey']);

return (
<Popover.Content class={style(local.appearanceKey || 'dropdownContent', dropdownContentVariants())} {...rest} />
);
};
28 changes: 28 additions & 0 deletions packages/js/src/ui/components/Dropdown/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { splitProps } from 'solid-js';
import { JSX } from 'solid-js/jsx-runtime';
import { AppearanceKey } from '../../context';
import { useStyle } from '../../helpers';
import { usePopover } from '../Popover';

export const dropdownItemVariants = () =>
'focus:nt-outline-none nt-items-center hover:nt-bg-neutral-alpha-100 focus:nt-bg-neutral-alpha-100 nt-py-1 nt-px-3';

type DropdownItemProps = JSX.IntrinsicElements['button'] & { appearanceKey?: AppearanceKey };
export const DropdownItem = (props: DropdownItemProps) => {
const style = useStyle();
const [local, rest] = splitProps(props, ['appearanceKey', 'onClick']);
const { onClose } = usePopover();

return (
<button
class={style(local.appearanceKey || 'dropdownItem', dropdownItemVariants())}
onClick={(e) => {
if (typeof local.onClick === 'function') {
local.onClick(e);
}
onClose();
}}
{...rest}
/>
);
};
17 changes: 17 additions & 0 deletions packages/js/src/ui/components/Dropdown/DropdownTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ComponentProps, splitProps } from 'solid-js';
import { useStyle } from '../../helpers';
import { AppearanceKey } from '../../context';
import { Popover } from '../Popover';

//TODO: Extend from buttonVariants() once added.
export const dropdownTriggerVariants = () =>
'nt-flex nt-justify-center nt-items-center nt-rounded-md nt-relative hover:nt-bg-foreground-alpha-50 focus:nt-bg-foreground-alpha-50 nt-text-foreground-alpha-600 nt-px-2';

export const DropdownTrigger = (props: ComponentProps<typeof Popover.Trigger> & { appearanceKey?: AppearanceKey }) => {
const style = useStyle();
const [local, rest] = splitProps(props, ['appearanceKey']);

return (
<Popover.Trigger class={style(local.appearanceKey || 'dropdownTrigger', dropdownTriggerVariants())} {...rest} />
);
};
16 changes: 16 additions & 0 deletions packages/js/src/ui/components/Dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Popover } from '../Popover';
import { DropdownContent } from './DropdownContent';
import { DropdownItem } from './DropdownItem';
import { DropdownTrigger } from './DropdownTrigger';

export { dropdownTriggerVariants } from './DropdownTrigger';
export { dropdownContentVariants } from './DropdownContent';
export { dropdownItemVariants } from './DropdownItem';

export const Dropdown = {
Root: Popover.Root,
Trigger: DropdownTrigger,
Content: DropdownContent,
Close: Popover.Close,
Item: DropdownItem,
};
5 changes: 2 additions & 3 deletions packages/js/src/ui/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useAppearance } from 'src/ui/context';
import { cn, useStyle } from 'src/ui/helpers';
import { cn, useStyle } from '../../helpers';
import { ActionsContainer } from './ActionsContainer';
import { StatusDropdown } from './StatusDropdown';

export const Header = () => {
const style = useStyle();

return (
<div class={style('inboxHeader', cn('nt-flex nt-justify-between nt-items-center nt-self-stretch nt-py-5 nt-px-6'))}>
<div class={style('inboxHeader', cn('nt-flex nt-justify-between nt-items-center nt-w-full nt-py-5 nt-px-6'))}>
<StatusDropdown />
<ActionsContainer />
</div>
Expand Down
27 changes: 17 additions & 10 deletions packages/js/src/ui/components/Header/MoreActionsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { useStyle } from '../../helpers';
import { cn, useStyle } from '../../helpers';
import { DotsMenu } from '../../icons';
import { dropdownContentClasses, moreActionsDropdownTriggerClasses, Popover } from '../Popover';
import { Dropdown, dropdownTriggerVariants } from '../Dropdown';
import { popoverTriggerVariants } from '../Popover';
import { MoreActionsOptions } from './MoreActionsOptions';

const APPEARANCE_KEY_PREFIX = 'moreActions';

export const MoreActionsDropdown = () => {
const style = useStyle();

return (
<Popover fallbackPlacements={['bottom', 'top']} placement="bottom">
<Popover.Trigger classes={style('moreActions__dropdownTrigger', moreActionsDropdownTriggerClasses())}>
<Dropdown.Root fallbackPlacements={['bottom', 'top']} placement="bottom">
<Dropdown.Trigger
class={style(
'moreActions__dropdownTrigger',
cn(
dropdownTriggerVariants(),
'nt-rounded-md nt-px-0 hover:nt-bg-foreground-alpha-50 focus:nt-bg-foreground-alpha-50 nt-text-foreground-alpha-600'
)
)}
>
<DotsMenu />
</Popover.Trigger>
<Popover.Content classes={style('moreActions__dropdownContent', dropdownContentClasses())}>
</Dropdown.Trigger>
<Dropdown.Content appearanceKey="moreActions__dropdownContent">
<MoreActionsOptions />
</Popover.Content>
</Popover>
</Dropdown.Content>
</Dropdown.Root>
);
};
25 changes: 11 additions & 14 deletions packages/js/src/ui/components/Header/MoreActionsOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { JSX } from 'solid-js';
import { useReadAll } from '../../api';
import { useStyle } from '../../helpers';
import { cn, useStyle } from '../../helpers';
import { Archived, ArchiveRead, ReadAll } from '../../icons';
import { Popover } from '../Popover';
import { PopoverClose } from '../Popover/PopoverClose';
import { dropdownItemClasses, dropdownItemLabelClasses, dropdownItemLabelContainerClasses } from './common';
import { Dropdown, dropdownItemVariants } from '../Dropdown';

export const MoreActionsOptions = () => {
const { markAllAsRead } = useReadAll();

return (
<PopoverClose>
<>
<ActionsItem
label="Mark all as read"
/**
Expand All @@ -35,21 +33,20 @@ export const MoreActionsOptions = () => {
onClick={() => {}}
icon={ArchiveRead}
/>
</PopoverClose>
</>
);
};

export const ActionsItem = (props: { label: string; onClick: () => void; icon: () => JSX.Element }) => {
const style = useStyle();

return (
<Popover.Close onClick={props.onClick}>
<button class={style('moreActions__dropdownItem', dropdownItemClasses())}>
<span class={style('moreActions__dropdownItemLabelContainer', dropdownItemLabelContainerClasses())}>
<span class={style('moreActions__dropdownItemLeftIcon', '')}>{props.icon()}</span>
<span class={style('moreActions__dropdownItemLabel', dropdownItemLabelClasses())}>{props.label}</span>
</span>
</button>
</Popover.Close>
<Dropdown.Item
class={style('moreActions__dropdownItem', cn(dropdownItemVariants(), 'nt-flex nt-gap-2'))}
onClick={props.onClick}
>
<span class={style('moreActions__dropdownItemLeftIcon')}>{props.icon()}</span>
<span class={style('moreActions__dropdownItemLabel')}>{props.label}</span>
</Dropdown.Item>
);
};
19 changes: 9 additions & 10 deletions packages/js/src/ui/components/Header/StatusDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NotificationStatus } from '../../../types';
import { useAppearance, useInboxStatusContext } from '../../context';
import { useInboxStatusContext } from '../../context';
import { cn, useStyle } from '../../helpers';
import { ArrowDropDown } from '../../icons';
import { dropdownContentClasses, inboxStatusDropdownTriggerClasses, Popover } from '../Popover';
import { Dropdown, dropdownTriggerVariants } from '../Dropdown';
import { StatusOptions } from './StatusOptions';

/**
Expand All @@ -25,22 +25,21 @@ const getStatusLabel = (status?: NotificationStatus) => {

export const StatusDropdown = () => {
const style = useStyle();
const { id } = useAppearance();
const { setFeedOptions, feedOptions } = useInboxStatusContext();

return (
<Popover fallbackPlacements={['bottom', 'top']} placement="bottom">
<Popover.Trigger classes={style('inboxStatus__dropdownTrigger', inboxStatusDropdownTriggerClasses())}>
<span class={style('inboxStatus__title', cn('nt-text-xl nt-font-semibold nt-text-foreground'))}>
<Dropdown.Root fallbackPlacements={['bottom', 'top']} placement="bottom">
<Dropdown.Trigger class={style('inboxStatus__dropdownTrigger', dropdownTriggerVariants())}>
<span class={style('inboxStatus__title', 'nt-text-xl nt-font-semibold nt-text-foreground')}>
{getStatusLabel(feedOptions.status)}
</span>
<span>
<ArrowDropDown />
</span>
</Popover.Trigger>
<Popover.Content classes={style('inboxStatus__dropdownContent', dropdownContentClasses())}>
</Dropdown.Trigger>
<Dropdown.Content appearanceKey="inboxStatus__dropdownContent">
<StatusOptions setFeedOptions={setFeedOptions} />
</Popover.Content>
</Popover>
</Dropdown.Content>
</Dropdown.Root>
);
};
30 changes: 15 additions & 15 deletions packages/js/src/ui/components/Header/StatusOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { JSX, Show } from 'solid-js';
import { FetchFeedArgs } from '../../../feeds';
import { NotificationStatus } from '../../../types';
import { useStyle } from '../../helpers';
import { cn, useStyle } from '../../helpers';
import { Archived, Check, Inbox, Unread } from '../../icons';
import { Popover } from '../Popover';
import { dropdownItemClasses, dropdownItemLabelClasses, dropdownItemLabelContainerClasses } from './common';
import { Dropdown, dropdownItemVariants } from '../Dropdown';

const DropdownStatus = {
UnreadRead: 'Unread & read',
Expand Down Expand Up @@ -61,18 +60,19 @@ export const StatusItem = (props: {
const style = useStyle();

return (
<Popover.Close onClick={props.onClick}>
<button class={style('inboxStatus__dropdownItem', dropdownItemClasses())}>
<span class={style('inboxStatus__dropdownItemLabelContainer', dropdownItemLabelContainerClasses())}>
<span class={style('inboxStatus__dropdownItemLeftIcon', '')}>{props.icon()}</span>
<span class={style('inboxStatus__dropdownItemLabel', dropdownItemLabelClasses())}>{props.label}</span>
<Dropdown.Item
class={style('inboxStatus__dropdownItem', cn(dropdownItemVariants(), 'nt-flex nt-gap-8'))}
onClick={props.onClick}
>
<span class={style('inboxStatus__dropdownItemLabelContainer', 'nt-flex nt-gap-2 nt-items-center')}>
<span class={style('inboxStatus__dropdownItemLeftIcon')}>{props.icon()}</span>
<span class={style('inboxStatus__dropdownItemLabel')}>{props.label}</span>
</span>
<Show when={props.isSelected}>
<span class={style('inboxStatus__dropdownItemRightIcon', 'nt-justify-self-end')}>
<Check />
</span>
<Show when={props.isSelected}>
<span class={style('inboxStatus__dropdownItemRightIcon', '')}>
<Check />
</span>
</Show>
</button>
</Popover.Close>
</Show>
</Dropdown.Item>
);
};
6 changes: 0 additions & 6 deletions packages/js/src/ui/components/Header/common/index.ts

This file was deleted.

Loading

0 comments on commit 5ba5b0a

Please sign in to comment.