Skip to content

Commit

Permalink
feat: dropdown component support subMenu; add common pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
F-star committed Jan 13, 2024
1 parent 3c0a5d2 commit e1ad736
Show file tree
Hide file tree
Showing 30 changed files with 253 additions and 101 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"packages/*"
],
"scripts": {
"dev": "run-p \"watch icons\" \"watch components\" \"watch geo\" app:dev",
"dev": "run-p \"watch common\" \"watch icons\" \"watch components\" \"watch geo\" app:dev",
"watch": "node scripts/dev.js",
"app:dev": "pnpm --filter @suika/suika dev",
"app:build": "pnpm --filter @suika/suika build",
Expand Down
1 change: 1 addition & 0 deletions packages/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Common
16 changes: 16 additions & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@suika/common",
"private": true,
"version": "0.0.1",
"type": "module",
"sideEffect": false,
"main": "dist/common.es.js",
"module": "dist/common.es.js",
"types": "dist/common.d.ts",
"scripts": {
"build": "tsc && vite build"
},
"devDependencies": {
"vite": "^4.2.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Publish-Subscribe Design Pattern
*/

class EventEmitter<T extends Record<string | symbol, any>> {
export class EventEmitter<T extends Record<string | symbol, any>> {
private eventMap: Record<keyof T, Array<(...args: any[]) => void>> =
{} as any;

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './event_emitter';
1 change: 1 addition & 0 deletions packages/common/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
21 changes: 21 additions & 0 deletions packages/common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2015",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,

"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
11 changes: 11 additions & 0 deletions packages/common/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite';

export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
name: 'geo',
fileName: (format) => `common.${format}.js`,
},
},
});
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@floating-ui/react": "^0.22.3",
"@suika/common": "workspace:^",
"@suika/icons": "workspace:^",
"classnames": "^2.3.2"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

cursor: pointer;

&:hover {
&:hover,
&.active {
color: #fff;
background-color: #0f8fff;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,85 @@
import { FC, PropsWithChildren } from 'react';
import { FC, useEffect, useState } from 'react';
import './dropdown-item.scss';
import { CheckOutlined } from '@suika/icons';
import { Dropdown } from '../dropdown';
import { DropdownEvents, Item } from '../type';
import { EventEmitter } from '@suika/common';
import classNames from 'classnames';

interface IProps extends PropsWithChildren {
interface IProps {
itemKey: string;
label: string;
suffix?: string;
onClick: () => void;
check?: boolean;
subItems?: Item[];

onClick: (params: { key: string }) => void;

emitter: EventEmitter<DropdownEvents>;
}

export const DropdownItem: FC<IProps> = ({
onClick,
children,
suffix,
check,
}) => {
return (
<div className="sk-dropdown-item-wrap" onClick={onClick}>
export const DropdownItem: FC<IProps> = (props) => {
const { onClick, label, suffix, check, subItems, emitter } = props;

useEffect(() => {
const handleOpenSubMenu = () => {
setOpen(false);
};

emitter.on('openSubMenu', handleOpenSubMenu);
return () => {
emitter.off('openSubMenu', handleOpenSubMenu);
};
}, [emitter, props.itemKey]);

const item = (
<>
<div className="sk-dropdown-item">
<div className="sk-dropdown-item-icon-box">
{check && <CheckOutlined />}
</div>
{children}
{label}
</div>
{suffix && <span>{suffix}</span>}
</div>
</>
);

const [open, setOpen] = useState(false);

return (
<>
{subItems ? (
<Dropdown
items={subItems}
onClick={onClick}
open={open}
placement="right-start"
offset={{
mainAxis: 0,
crossAxis: -8,
}}
>
<div
className={classNames('sk-dropdown-item-wrap', { active: open })}
onMouseEnter={() => {
emitter.emit('openSubMenu', props.itemKey);
setOpen(true);
}}
>
{item}
</div>
</Dropdown>
) : (
<div
className="sk-dropdown-item-wrap"
onClick={() => onClick({ key: props.itemKey })}
onMouseEnter={() => {
emitter.emit('openSubMenu', props.itemKey);
}}
>
{item}
</div>
)}
</>
);
};
26 changes: 25 additions & 1 deletion packages/components/src/components/dropdown/dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,39 @@ export const Default: Story = {
{
label: 'item one',
check: true,
suffix: 'Ctrl+1',
key: 'aa',
},
{
type: 'divider',
},
{
label: 'item two',
suffix: 'Ctrl+1',
key: 'bb',
children: [
{
label: 'item two one',
key: 'bb-1',
},
{
label: 'item two two',
key: 'bb-2',
},
],
},
{
label: 'item three',
key: 'cc',
children: [
{
label: 'item three one',
key: 'cc-1',
},
{
label: 'item three two',
key: 'cc-2',
},
],
},
],
onClick: (params) => {
Expand Down
58 changes: 33 additions & 25 deletions packages/components/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,49 @@
import React, { FC, useState } from 'react';
import React, { FC, useRef, useState } from 'react';
import { Popover } from '../popover';
import { DropdownItem } from './dropdown-item';
import './dropdown.scss';
import { DropdownDivider, DropdownEvents, Item } from './type';
import { OffsetOptions, Placement } from '@floating-ui/react';

interface DropdownDivider {
type: 'divider';
}

interface DropDownItem {
key: string;
label: string;
suffix?: string;
check?: boolean;
children?: DropDownItem[];
}

type Item = DropDownItem | DropdownDivider;
import { EventEmitter } from '@suika/common';

interface IProps {
items: Item[];
onClick?: (params: { key: string }) => void;
children: React.ReactNode;

placement?: Placement;
trigger?: 'click' | 'hover';
offset?: OffsetOptions;

open?: boolean;
onOpenChange?: (open: boolean) => void;
}

const isDivider = (item: Item): item is DropdownDivider => {
return (item as DropdownDivider).type === 'divider';
};

export const Dropdown: FC<IProps> = (props) => {
const { items, children } = props;
const { items, children, placement = 'bottom-start' } = props;
const [open, setOpen] = useState(false);

const mixedOpen = props.open === undefined ? open : props.open;

const onOpenChange = (visible: boolean) => {
setOpen(visible);
props.onOpenChange?.(visible);
};

const emitter = useRef(new EventEmitter<DropdownEvents>());

return (
<Popover
open={open}
onOpenChange={(val) => {
setOpen(val);
}}
open={mixedOpen}
onOpenChange={onOpenChange}
placement={placement}
offset={props.offset}
trigger={props.trigger}
content={
<div className="sk-dropdown-content">
{items.map((item, index) => {
Expand All @@ -45,20 +52,21 @@ export const Dropdown: FC<IProps> = (props) => {
) : (
<DropdownItem
key={item.key}
itemKey={item.key}
label={item.label}
suffix={item.suffix}
check={item.check}
onClick={() => {
subItems={item.children}
emitter={emitter.current}
onClick={(params) => {
setOpen(false);
props.onClick?.({ key: item.key });
props.onClick?.(params);
}}
>
{item.label}
</DropdownItem>
/>
);
})}
</div>
}
placement="bottom-start"
>
{React.cloneElement(children as React.ReactElement)}
</Popover>
Expand Down
17 changes: 17 additions & 0 deletions packages/components/src/components/dropdown/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface DropdownDivider {
type: 'divider';
}

export interface DropDownItemType {
key: string;
label: string;
suffix?: string;
check?: boolean;
children?: DropDownItemType[];
}

export type Item = DropDownItemType | DropdownDivider;

export interface DropdownEvents {
openSubMenu(key: string): void;
}
Loading

0 comments on commit e1ad736

Please sign in to comment.