Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combobox rework #3168

Merged
merged 41 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
acb0b91
Commit yarn.lock
12joan May 1, 2024
869f868
Fix: www app uses separate slate-react to packages, breaking useSelected
12joan Apr 25, 2024
437738e
Prototype combobox input inside slash-input-element.tsx
12joan May 1, 2024
4aaf43c
Refactor combobox input hooks into package
12joan May 1, 2024
71bbfce
Refactor `createSlashPlugin` to use combobox helper
12joan May 1, 2024
2f486a2
Prototype InlineCombobox component
12joan May 1, 2024
7ed9cdb
Refactor out InlineCombobox component
12joan May 1, 2024
1783605
Add tests for matchWords
12joan May 1, 2024
f866183
Fully implement slash commands using new combobox
12joan May 1, 2024
2c98587
Fix error when focusing
12joan May 1, 2024
297657e
Fix CSS leaking into combobox popover
12joan May 1, 2024
f9f21b6
Fix: Aliases backwards for [un]ordered lists
12joan May 1, 2024
2890a67
Use cva for combobox item styles
12joan May 2, 2024
b6e1dab
Refactor aliases -> keywords
12joan May 2, 2024
4818f2d
Remove SlashPlugin type
12joan May 2, 2024
42f2421
Refactor query -> search
12joan May 2, 2024
766ab37
Drop label and make value user-facing
12joan May 2, 2024
878b1ca
Merge branch 'main' into feat/combobox
12joan May 2, 2024
e5034c8
Fix: SlashCommandRule doesn't need exporting
12joan May 2, 2024
a20be89
Refactor withInsertTextTriggerCombobox -> withTriggerCombobox
12joan May 2, 2024
f5e0553
Refactor TriggerComboboxPlugin options
12joan May 2, 2024
26e92a7
Revert "Fix: www app uses separate slate-react to packages, breaking …
12joan May 2, 2024
88f386c
Merge branch 'main' into feat/combobox
12joan May 2, 2024
7cca80c
Rename match -> filter
12joan May 2, 2024
13f4fa9
Refactor InlineCombobox into multiple components
12joan May 2, 2024
2c86fbf
Fix: InlineComboboxItem className should be merged
12joan May 2, 2024
538cc02
Remove unnecessary `?.`
12joan May 2, 2024
4a5c168
Use new combobox API for mentions
12joan May 3, 2024
d958373
Remove mention-combobox from registry.ts
12joan May 3, 2024
2aa6b5b
yarn build:registry
12joan May 3, 2024
f74f4f8
Merge branch 'refs/heads/main' into feat/combobox
May 6, 2024
d32625b
eslint
May 6, 2024
751a9bb
eslint
May 6, 2024
8cf1b3d
Use combobox with emoji picker
12joan Jun 3, 2024
27616fe
Delete old combobox code
12joan Jun 3, 2024
af87f80
Remove old combobox component
12joan Jun 3, 2024
035752f
Fix: Trigger is inserted in the wrong place on selection change
12joan Jun 3, 2024
9288232
Update registry and docs
12joan Jun 5, 2024
5424940
Linter and brl fixes
12joan Jun 5, 2024
6985cf6
Add package changesets
12joan Jun 5, 2024
4dac4cb
Merge branch 'main' into feat/combobox
12joan Jun 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"typecheck": "yarn prebuild && tsc --noEmit"
},
"dependencies": {
"@ariakit/react": "0.4.6",
"@faker-js/faker": "^8.4.1",
"@radix-ui/colors": "1.0.1",
"@radix-ui/react-accessible-icon": "^1.0.3",
Expand Down Expand Up @@ -135,7 +136,6 @@
"slate": "0.102.0",
"slate-history": "0.100.0",
"slate-hyperscript": "0.100.0",
"slate-react": "0.102.0",
"slate-test-utils": "1.3.2",
"sonner": "^1.4.32",
"tailwind-merge": "^2.2.2",
Expand Down
52 changes: 0 additions & 52 deletions apps/www/src/lib/plate/demo/values/slashRules.ts

This file was deleted.

10 changes: 1 addition & 9 deletions apps/www/src/registry/default/example/playground-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend';

import { ValueId } from '@/config/customizer-plugins';
import { captionPlugin } from '@/lib/plate/demo/plugins/captionPlugin';
import { SLASH_RULES } from '@/lib/plate/demo/values/slashRules';
import { settingsStore } from '@/components/context/settings-store';
import { PlaygroundFixedToolbarButtons } from '@/components/plate-ui/playground-fixed-toolbar-buttons';
import { PlaygroundFloatingToolbarButtons } from '@/components/plate-ui/playground-floating-toolbar-buttons';
Expand All @@ -130,7 +129,6 @@ import {
TodoMarker,
} from '@/registry/default/plate-ui/indent-todo-marker-component';
import { MentionCombobox } from '@/registry/default/plate-ui/mention-combobox';
import { SlashCombobox } from '@/registry/default/plate-ui/slash-combobox';

export const usePlaygroundPlugins = ({
id,
Expand Down Expand Up @@ -179,11 +177,7 @@ export const usePlaygroundPlugins = ({
triggerPreviousCharPattern: /^$|^[\s"']$/,
},
}),
createSlashPlugin({
options: {
rules: SLASH_RULES,
},
}),
createSlashPlugin(),
createTablePlugin({
enabled: !!enabled.table,
options: {
Expand Down Expand Up @@ -439,8 +433,6 @@ export default function PlaygroundDemo({ id }: { id?: ValueId }) {
<MentionCombobox items={MENTIONABLES} />
)}

<SlashCombobox items={SLASH_RULES} />

{isEnabled('cursoroverlay', id) && (
<CursorOverlay containerRef={containerRef} />
)}
Expand Down
132 changes: 132 additions & 0 deletions apps/www/src/registry/default/plate-ui/inline-combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { ReactNode, startTransition, useMemo, useState } from 'react';
import {
Combobox,
ComboboxItem,
ComboboxPopover,
ComboboxProvider,
Portal,
} from '@ariakit/react';
import { cn } from '@udecode/cn';
import {
BaseComboboxItemWithEditor,
matchWords,
useComboboxInput,
useHTMLInputCursorState,
} from '@udecode/plate-combobox';
import { insertText, moveSelection, useEditorRef } from '@udecode/plate-common';

const comboboxItemClassName =
12joan marked this conversation as resolved.
Show resolved Hide resolved
'relative flex h-9 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none';
const comboboxItemInteractiveClassName =
'cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground';

const defaultMatchItem = (
12joan marked this conversation as resolved.
Show resolved Hide resolved
{ label, aliases = [] }: BaseComboboxItemWithEditor,
query: string
12joan marked this conversation as resolved.
Show resolved Hide resolved
) => [label, ...aliases].some((alias) => matchWords(alias, query));

interface InlineComboboxProps<TItem extends BaseComboboxItemWithEditor> {
trigger: string;
items: TItem[];
matchItem?: (item: TItem, query: string) => boolean;
renderItem?: (item: TItem) => ReactNode;
renderEmpty?: ReactNode;
onSelectItem?: (item: TItem) => void;
}

export const InlineCombobox = <TItem extends BaseComboboxItemWithEditor>({
12joan marked this conversation as resolved.
Show resolved Hide resolved
trigger,
items,
matchItem = defaultMatchItem,
renderItem = ({ label }) => label,
renderEmpty,
onSelectItem,
}: InlineComboboxProps<TItem>) => {
const editor = useEditorRef();
const [value, setValue] = useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
const cursorState = useHTMLInputCursorState(inputRef);

const filteredItems = useMemo(
() => items.filter((item) => matchItem(item, value)),
[items, matchItem, value]
);

const { removeInput, props: inputProps } = useComboboxInput({
ref: inputRef,
cursorState,
onCancelInput: (cause) => {
if (cause !== 'backspace') {
insertText(editor, trigger + value);
}

if (cause === 'arrowLeft' || cause === 'arrowRight') {
moveSelection(editor, {
distance: 1,
reverse: cause === 'arrowLeft',
});
}
},
});

/**
* To create an auto-resizing input, we render a visually hidden span
* containing the input value and position the input element on top of it.
* This works well for all cases except when input exceeds the width of the
* container.
*/

return (
<span contentEditable={false}>
{trigger}

<ComboboxProvider
open={filteredItems.length > 0 || renderEmpty !== undefined}
setValue={(newValue) => startTransition(() => setValue(newValue))}
>
<span className="relative">
<span
aria-hidden="true"
className="invisible overflow-hidden text-nowrap"
>
{value}
</span>

<Combobox
ref={inputRef}
autoSelect
value={value}
className="absolute left-0 top-0 size-full bg-transparent outline-none"
{...inputProps}
/>
</span>

{/* Portal prevents CSS from leaking into popover */}
<Portal>
<ComboboxPopover className="z-[500] max-h-[288px] w-[300px] overflow-y-auto rounded-md bg-popover shadow-md">
12joan marked this conversation as resolved.
Show resolved Hide resolved
{filteredItems.map((item) => (
12joan marked this conversation as resolved.
Show resolved Hide resolved
<ComboboxItem
key={item.value}
className={cn(
comboboxItemClassName,
comboboxItemInteractiveClassName
)}
onClick={() => {
removeInput(true);
item.onSelect?.(editor);
onSelectItem?.(item);
}}
>
{renderItem(item)}
</ComboboxItem>
))}

{filteredItems.length === 0 && renderEmpty && (
<div className={comboboxItemClassName}>{renderEmpty}</div>
)}
</ComboboxPopover>
</Portal>
</ComboboxProvider>
</span>
);
};
36 changes: 0 additions & 36 deletions apps/www/src/registry/default/plate-ui/slash-combobox.tsx

This file was deleted.

117 changes: 88 additions & 29 deletions apps/www/src/registry/default/plate-ui/slash-input-element.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,92 @@
import React from 'react';
import { cn, withRef } from '@udecode/cn';
import { getHandler, PlateElement } from '@udecode/plate-common';
import { useFocused, useSelected } from 'slate-react';
import React, { ComponentType, SVGProps } from 'react';
import { withRef } from '@udecode/cn';
import { BaseComboboxItemWithEditor } from '@udecode/plate-combobox';
import { PlateElement, toggleNodeType } from '@udecode/plate-common';
import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading';
import { ListStyleType, toggleIndentList } from '@udecode/plate-indent-list';

export const SlashInputElement = withRef<
typeof PlateElement,
import { Icons } from '@/components/icons';

import { InlineCombobox } from './inline-combobox';

export type SlashCommandRule = BaseComboboxItemWithEditor & {
icon: ComponentType<SVGProps<SVGSVGElement>>;
};

const rules: SlashCommandRule[] = [
{
onClick?: (slashNode: any) => void;
}
>(({ className, onClick, ...props }, ref) => {
const { children, element } = props;
value: ELEMENT_H1,
label: 'Heading 1',
icon: Icons.h1,
onSelect: (editor) => {
toggleNodeType(editor, { activeType: ELEMENT_H1 });
},
},
{
value: ELEMENT_H2,
label: 'Heading 2',
icon: Icons.h2,
onSelect: (editor) => {
toggleNodeType(editor, { activeType: ELEMENT_H2 });
},
},
{
value: ELEMENT_H3,
label: 'Heading 3',
icon: Icons.h3,
onSelect: (editor) => {
toggleNodeType(editor, { activeType: ELEMENT_H3 });
},
},
{
value: ListStyleType.Disc,
label: 'Bulleted list',
icon: Icons.ul,
aliases: ['ul', 'ordered list'],
onSelect: (editor) => {
toggleIndentList(editor, {
listStyleType: ListStyleType.Disc,
});
},
},
{
value: ListStyleType.Decimal,
label: 'Numbered list',
icon: Icons.ol,
aliases: ['ol', 'unordered list'],
onSelect: (editor) => {
toggleIndentList(editor, {
listStyleType: ListStyleType.Decimal,
});
},
},
];

const selected = useSelected();
const focused = useFocused();
export const SlashInputElement = withRef<typeof PlateElement>(
({ className, ...props }, ref) => {
const { children, element } = props;

return (
<PlateElement
ref={ref}
asChild
data-slate-value={element.value}
className={cn(
'inline-block rounded-md px-1.5 py-0.5 align-baseline text-sm',
selected && focused && 'ring-2 ring-ring',
className
)}
onClick={getHandler(onClick, element)}
{...props}
>
<span>/{children}</span>
</PlateElement>
);
});
return (
<PlateElement
as="span"
ref={ref}
data-slate-value={element.value}
{...props}
>
<InlineCombobox
trigger="/"
items={rules}
renderItem={({ icon: Icon, label }) => (
<>
<Icon className="mr-2 size-4" aria-hidden />
{label}
</>
)}
renderEmpty="No matching commands found"
/>

{children}
</PlateElement>
);
}
);
Loading