Skip to content

Commit

Permalink
Use index to reference popup configuration option
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan committed Feb 2, 2025
1 parent fb8e71d commit 53b8ca5
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type ComposerBoxPopupPreviewProps = ComposerBoxPopupProps<ComposerBoxPopupPrevie
title?: ReactNode;
rid: string;
tmid?: string;
suspended?: boolean;
suspended: boolean;
};

const ComposerBoxPopupPreview = forwardRef(function ComposerBoxPopupPreview(
Expand Down
144 changes: 71 additions & 73 deletions apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { ComposerPopupOption } from '../../contexts/ComposerPopupContext';

type ComposerBoxPopupImperativeCommands<T> = MutableRefObject<
| {
getFilter?: () => unknown;
getFilter?: () => string;
select?: (s: T) => void;
}
| undefined
Expand All @@ -19,66 +19,60 @@ type ComposerBoxPopupOptions<T extends { _id: string; sort?: number | undefined

type ComposerBoxPopupResult<T extends { _id: string; sort?: number }> =
| {
popup: ComposerPopupOption<T>;
option: ComposerPopupOption<T>;
items: UseQueryResult<T[]>[];
focused: T | undefined;
ariaActiveDescendant: string | undefined;
select: (item: T) => void;
callbackRef: (node: HTMLElement) => void;
commandsRef: ComposerBoxPopupImperativeCommands<T>;
suspended: boolean;
filter: unknown;
clearPopup: () => void;
clear: () => void;
}
| {
popup: undefined;
option: undefined;
items: undefined;
focused: undefined;
ariaActiveDescendant: undefined;
callbackRef: (node: HTMLElement) => void;
select: undefined;
commandsRef: ComposerBoxPopupImperativeCommands<T>;
suspended: boolean;
suspended: undefined;
filter: unknown;
clearPopup: () => void;
clear: () => void;
};

const keys = {
TAB: 9,
ENTER: 13,
ESC: 27,
ARROW_LEFT: 37,
ARROW_UP: 38,
ARROW_RIGHT: 39,
ARROW_DOWN: 40,
};
} as const;

export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>({
configurations,
}: {
configurations: ComposerBoxPopupOptions<T>[];
}): ComposerBoxPopupResult<T> => {
const [popup, setPopup] = useState<ComposerBoxPopupOptions<T> | undefined>(undefined);
export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>(
options: ComposerBoxPopupOptions<T>[],
): ComposerBoxPopupResult<T> => {
const [optionIndex, setOptionIndex] = useState<number>(-1);
const [focused, setFocused] = useState<T | undefined>(undefined);
const [filter, setFilter] = useState<unknown>('');
const [filter, setFilter] = useState('');

const option = options[optionIndex];

const commandsRef: ComposerBoxPopupImperativeCommands<T> = useRef();

const { queries: items, suspended } = useComposerBoxPopupQueries(filter, popup) as {
const { queries: items, suspended } = useComposerBoxPopupQueries(filter, option) as {
queries: UseQueryResult<T[]>[];
suspended: boolean;
};

const chat = useChat();

const ariaActiveDescendant = focused ? `popup-item-${focused._id}` : undefined;

useEffect(() => {
if (!popup) {
if (!option) {
return;
}

if (popup?.preview && suspended) {
if (option?.preview && suspended) {
setFocused(undefined);
return;
}
Expand All @@ -89,10 +83,10 @@ export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>({
.sort((a, b) => (('sort' in a && a.sort) || 0) - (('sort' in b && b.sort) || 0));
return sortedItems.find((item) => item._id === focused?._id) ?? sortedItems[0];
});
}, [items, popup, suspended]);
}, [items, option, suspended]);

const select = useEffectEvent((item: T) => {
if (!popup) {
if (!option) {
throw new Error('No popup is open');
}

Expand All @@ -101,33 +95,33 @@ export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>({
} else {
const value = chat?.composer?.substring(0, chat?.composer?.selection.start);
const selector =
popup.matchSelectorRegex ??
(popup.triggerAnywhere ? new RegExp(`(?:^| |\n)(${popup.trigger})([^\\s]*$)`) : new RegExp(`(?:^)(${popup.trigger})([^\\s]*$)`));
option.matchSelectorRegex ??
(option.triggerAnywhere ? new RegExp(`(?:^| |\n)(${option.trigger})([^\\s]*$)`) : new RegExp(`(?:^)(${option.trigger})([^\\s]*$)`));

const result = value?.match(selector);
if (!result || !value) {
return;
}

chat?.composer?.replaceText((popup.prefix ?? popup.trigger ?? '') + popup.getValue(item) + (popup.suffix ?? ''), {
chat?.composer?.replaceText((option.prefix ?? option.trigger ?? '') + option.getValue(item) + (option.suffix ?? ''), {
start: value.lastIndexOf(result[1] + result[2]),
end: chat?.composer?.selection.start,
});
}
setPopup(undefined);
setOptionIndex(-1);
setFocused(undefined);
});

const setConfigByInput = useEffectEvent((): ComposerBoxPopupOptions<T> | undefined => {
const setOptionByInput = useEffectEvent((): ComposerBoxPopupOptions<T> | undefined => {
const value = chat?.composer?.substring(0, chat?.composer?.selection.start);

if (!value) {
setPopup(undefined);
setOptionIndex(-1);
setFocused(undefined);
return;
}

const configuration = configurations.find(({ trigger, matchSelectorRegex, triggerAnywhere, triggerLength }) => {
const optionIndex = options.findIndex(({ trigger, matchSelectorRegex, triggerAnywhere, triggerLength }) => {
const selector =
matchSelectorRegex ?? (triggerAnywhere ? new RegExp(`(?:^| |\n)(${trigger})[^\\s]*$`) : new RegExp(`(?:^)(${trigger})[^\\s]*$`));
const result = selector.test(value);
Expand All @@ -137,50 +131,49 @@ export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>({
const filter = value.match(selector);
return filter && triggerLength < filter[0].length;
});
setPopup(configuration);
if (!configuration) {
setOptionIndex(optionIndex);
const option = options[optionIndex];
if (!option) {
setFocused(undefined);
setFilter('');
}

if (configuration) {
if (option) {
const selector =
configuration.matchSelectorRegex ??
(configuration.triggerAnywhere
? new RegExp(`(?:^| |\n)(${configuration.trigger})([^\\s]*$)`)
: new RegExp(`(?:^)(${configuration.trigger})([^\\s]*$)`));
option.matchSelectorRegex ??
(option.triggerAnywhere ? new RegExp(`(?:^| |\n)(${option.trigger})([^\\s]*$)`) : new RegExp(`(?:^)(${option.trigger})([^\\s]*$)`));
const result = value.match(selector);
setFilter(commandsRef.current?.getFilter?.() ?? (result ? result[2] : ''));
}
return configuration;
return option;
});

const onFocus = useEffectEvent(() => {
if (popup) {
const handleFocus = useEffectEvent(() => {
if (option) {
return;
}
setConfigByInput();
setOptionByInput();
});

const keyup = useEffectEvent((event: KeyboardEvent) => {
if (!setConfigByInput()) {
const handleKeyUp = useEffectEvent((event: KeyboardEvent) => {
if (!setOptionByInput()) {
return;
}

if (!popup) {
if (!option) {
return;
}

if (popup.closeOnEsc === true && event.which === keys.ESC) {
setPopup(undefined);
if (option.closeOnEsc === true && event.which === keys.ESC) {
setOptionIndex(-1);
setFocused(undefined);
event.preventDefault();
event.stopImmediatePropagation();
}
});

const keydown = useEffectEvent((event: KeyboardEvent) => {
if (!popup) {
const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
if (!option) {
return;
}

Expand Down Expand Up @@ -235,54 +228,59 @@ export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>({
}
});

const clearPopup = useEffectEvent(() => {
if (!popup) {
const clear = useEffectEvent(() => {
if (!option) {
return;
}

setPopup(undefined);
setOptionIndex(-1);
setFocused(undefined);
setFilter('');
});

const ref = useRef<HTMLElement | null>(null);
const callbackRef = useCallback(
(node: HTMLElement | null) => {
if (!node) {
return;
if (ref.current) {
ref.current.removeEventListener('keyup', handleKeyUp);
ref.current.removeEventListener('keydown', handleKeyDown);
ref.current.removeEventListener('focus', handleFocus);
ref.current = null;
}

node.addEventListener('keyup', keyup);
node.addEventListener('keydown', keydown);
node.addEventListener('focus', onFocus);
if (node) {
ref.current = node;
node.addEventListener('keyup', handleKeyUp);
node.addEventListener('keydown', handleKeyDown);
node.addEventListener('focus', handleFocus);
}
},
[keyup, keydown, onFocus],
[handleKeyUp, handleKeyDown, handleFocus],
);

if (!popup) {
if (!option) {
return {
callbackRef,
focused: undefined,
option: undefined,
items: undefined,
ariaActiveDescendant: undefined,
popup: undefined,
focused: undefined,
select: undefined,
suspended: true,
callbackRef,
commandsRef,
suspended: undefined,
filter: undefined,
clearPopup,
clear,
};
}

return {
focused,
option,
items,
ariaActiveDescendant,
popup,
focused,
select,
filter,
suspended,
commandsRef,
callbackRef,
clearPopup,
commandsRef,
suspended,
filter,
clear,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const isMutableRefObject = <T>(x: unknown): x is MutableRefObject<T> => typeof x
* @param refs The refs to merge.
* @returns The merged ref callback.
*/
export const useMessageComposerMergedRefs = <T>(...refs: Ref<T>[]): RefCallback<T> => {
export const useMessageComposerMergedRefs = <T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> => {
return useCallback((refValue: T) => {
refs.filter(Boolean).forEach((ref) => {
refs.forEach((ref) => {
if (isRefCallback<T>(ref)) {
ref(refValue);
return;
Expand Down
Loading

0 comments on commit 53b8ca5

Please sign in to comment.