Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/fuzzy-bars-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/fuselage': minor
---

feat(fuselage): Remove deprecated `Options.*` component namespace
2 changes: 1 addition & 1 deletion packages/fuselage/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRef, useCallback, useEffect } from 'react';

import type Box from '../Box';
import { IconButton } from '../Button';
import Options, { useCursor, type OptionType } from '../Options';
import { Options, useCursor, type OptionType } from '../Options';
import PositionAnimated from '../PositionAnimated';

type MenuProps = Omit<ComponentProps<typeof IconButton>, 'icon'> & {
Expand Down
31 changes: 31 additions & 0 deletions packages/fuselage/src/components/Options/OptionContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { forwardRef } from 'react';

import Box, { type BoxProps } from '../Box';
import Scrollable from '../Scrollable';
import Tile from '../Tile';

export type OptionContainerProps = BoxProps;

const OptionContainer = forwardRef<HTMLElement, OptionContainerProps>(
function OptionContainer({ children, ...props }, ref) {
return (
<Box rcx-options>
<Tile padding={0} paddingBlock={'x12'} paddingInline={0}>
<Scrollable vertical smooth>
<Tile
ref={ref}
elevation='0'
padding='none'
maxHeight='x240'
{...props}
>
{children}
</Tile>
</Scrollable>
</Tile>
</Box>
);
},
);

export default OptionContainer;
10 changes: 10 additions & 0 deletions packages/fuselage/src/components/Options/OptionType.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';

export type OptionType = [
value: string | number,
label: ReactNode,
selected?: boolean,
disabled?: boolean,
type?: 'heading' | 'divider' | 'option',
url?: string,
];
3 changes: 1 addition & 2 deletions packages/fuselage/src/components/Options/Options.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { createRef } from 'react';
import Box from '../Box';
import { Option, CheckOption } from '../Option';

import type { OptionType } from './Options';
import { Options } from './Options';
import { Options, type OptionType } from '.';

export default {
title: 'Navigation/Options',
Expand Down
245 changes: 100 additions & 145 deletions packages/fuselage/src/components/Options/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import type {
ComponentProps,
ElementType,
ReactNode,
Ref,
SyntheticEvent,
} from 'react';
import { forwardRef, memo, useLayoutEffect, useMemo, useRef } from 'react';
import type { ElementType, SyntheticEvent } from 'react';
import { forwardRef, useLayoutEffect, useMemo, useRef } from 'react';

import { prevent } from '../../helpers/prevent';
import Box from '../Box';
import Box, { type BoxProps } from '../Box';
import { Option, OptionHeader, OptionDivider } from '../Option';
import Scrollable from '../Scrollable';
import Tile from '../Tile';

import { useCursor } from './useCursor';
import type { OptionType } from './OptionType';
import OptionsEmpty from './OptionsEmpty';

export { useCursor };

export type OptionType = [
value: string | number,
label: ReactNode,
selected?: boolean,
disabled?: boolean,
type?: 'heading' | 'divider' | 'option',
url?: string,
];

type OptionsProps = Omit<ComponentProps<typeof Box>, 'onSelect'> & {
export type OptionsProps = Omit<BoxProps, 'onSelect'> & {
multiple?: boolean;
options: OptionType[];
cursor: number;
Expand All @@ -36,132 +20,103 @@ type OptionsProps = Omit<ComponentProps<typeof Box>, 'onSelect'> & {
customEmpty?: string;
};

export const Empty = memo(({ customEmpty }: { customEmpty: string }) => (
<Option label={customEmpty || 'Empty'} />
));

/**
* An input for selection of options.
*/
export const Options = forwardRef(
(
{
maxHeight = 'x144',
multiple,
renderEmpty: EmptyComponent = Empty,
options,
cursor,
renderItem: OptionComponent = Option,
onSelect,
customEmpty,
...props
}: OptionsProps,
ref: Ref<HTMLElement>,
) => {
const liRef = useRef<HTMLElement>(null);
const Options = forwardRef<HTMLElement, OptionsProps>(function Options(
{
maxHeight = 'x144',
multiple,
renderEmpty: EmptyComponent = OptionsEmpty,
options,
cursor,
renderItem: OptionComponent = Option,
onSelect,
customEmpty,
...props
},
ref,
) {
const liRef = useRef<HTMLElement>(null);

useLayoutEffect(() => {
if (!liRef.current) {
return;
}
const { current } = liRef;
const li = current?.querySelector<HTMLLIElement>('.rcx-option--focus');
if (!li) {
return;
}
if (
li.offsetTop + li.clientHeight >
current.scrollTop + current.clientHeight ||
li.offsetTop - li.clientHeight < current.scrollTop
) {
current.scrollTop = li.offsetTop;
}
}, [cursor]);
useLayoutEffect(() => {
if (!liRef.current) {
return;
}
const { current } = liRef;
const li = current?.querySelector<HTMLLIElement>('.rcx-option--focus');
if (!li) {
return;
}
if (
li.offsetTop + li.clientHeight >
current.scrollTop + current.clientHeight ||
li.offsetTop - li.clientHeight < current.scrollTop
) {
current.scrollTop = li.offsetTop;
}
}, [cursor]);

const optionsMemoized = useMemo(
() =>
options?.map(([value, label, selected, disabled, type, url], i) => {
switch (type) {
case 'heading':
return <OptionHeader key={value}>{label}</OptionHeader>;
case 'divider':
return <OptionDivider key={value} />;
default:
return (
<OptionComponent
role='option'
label={label}
onMouseDown={(e: SyntheticEvent) => {
if (disabled) {
return;
}
prevent(e);
onSelect([value, label, selected, disabled, type, url]);
return false;
}}
key={value}
value={value}
selected={selected || (multiple !== true && null)}
disabled={disabled}
focus={cursor === i || null}
/>
);
}
}),
[options, multiple, cursor, onSelect, OptionComponent],
);
const optionsMemoized = useMemo(
() =>
options?.map(([value, label, selected, disabled, type, url], i) => {
switch (type) {
case 'heading':
return <OptionHeader key={value}>{label}</OptionHeader>;
case 'divider':
return <OptionDivider key={value} />;
default:
return (
<OptionComponent
role='option'
label={label}
onMouseDown={(e: SyntheticEvent) => {
if (disabled) {
return;
}
prevent(e);
onSelect([value, label, selected, disabled, type, url]);
return false;
}}
key={value}
value={value}
selected={selected || (multiple !== true && null)}
disabled={disabled}
focus={cursor === i || null}
/>
);
}
}),
[options, multiple, cursor, onSelect, OptionComponent],
);

return (
<Box rcx-options {...props} ref={ref}>
<Tile padding={0} paddingBlock={'x12'} paddingInline={0} elevation='2'>
<Scrollable vertical smooth>
<Tile
ref={liRef}
elevation='0'
padding='none'
maxHeight={maxHeight}
onMouseDown={prevent}
onClick={prevent}
is='ol'
aria-multiselectable={multiple || true}
role='listbox'
aria-activedescendant={
options?.[cursor]?.[0]
? String(options?.[cursor]?.[0])
: undefined
}
>
{!options.length && <EmptyComponent customEmpty={customEmpty} />}
{optionsMemoized}
</Tile>
</Scrollable>
</Tile>
</Box>
);
},
);
export const OptionContainer = forwardRef<
HTMLElement,
ComponentProps<typeof Box>
>(({ children, ...props }, ref) => (
<Box rcx-options>
<Tile padding={0} paddingBlock={'x12'} paddingInline={0}>
<Scrollable vertical smooth>
<Tile
ref={ref}
elevation='0'
padding='none'
maxHeight='x240'
// onMouseDown={prevent}
// onClick={prevent}
// is='ol'
// aria-multiselectable={multiple || true}
// role='listbox'
{...props}
>
{children}
</Tile>
</Scrollable>
</Tile>
</Box>
));
return (
<Box rcx-options {...props} ref={ref}>
<Tile padding={0} paddingBlock={'x12'} paddingInline={0} elevation='2'>
<Scrollable vertical smooth>
<Tile
ref={liRef}
elevation='0'
padding='none'
maxHeight={maxHeight}
onMouseDown={prevent}
onClick={prevent}
is='ol'
aria-multiselectable={multiple || true}
role='listbox'
aria-activedescendant={
options?.[cursor]?.[0]
? String(options?.[cursor]?.[0])
: undefined
}
>
{!options.length && <EmptyComponent customEmpty={customEmpty} />}
{optionsMemoized}
</Tile>
</Scrollable>
</Tile>
</Box>
);
});

export default Options;
13 changes: 13 additions & 0 deletions packages/fuselage/src/components/Options/OptionsEmpty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { memo } from 'react';

import Option from '../Option/Option';

export type OptionsEmptyProps = {
customEmpty: string;
};

const OptionsEmpty = ({ customEmpty }: OptionsEmptyProps) => (
<Option label={customEmpty || 'Empty'} />
);

export default memo(OptionsEmpty);
19 changes: 7 additions & 12 deletions packages/fuselage/src/components/Options/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type { AvatarProps } from '../Avatar';

import { Options } from './Options';

export * from './Options';

const avatarSize: AvatarProps['size'] = 'x20';

export default Object.assign(Options, {
/** @deprecated */
AvatarSize: avatarSize,
});
export { default as Options, type OptionsProps } from './Options';
export {
default as OptionContainer,
type OptionContainerProps,
} from './OptionContainer';
export type { OptionType } from './OptionType';
export { useCursor } from './useCursor';
4 changes: 2 additions & 2 deletions packages/fuselage/src/components/Options/useCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState } from 'react';

import AnimatedVisibility from '../AnimatedVisibility';

import type { OptionType } from './Options';
import type { OptionType } from './OptionType';
import { useVisible } from './useVisible';

const keyCodes = {
Expand Down Expand Up @@ -58,7 +58,7 @@ const findNextIndex = <T>(
return -1;
};

export type UseCursorOnChange<T> = (
type UseCursorOnChange<T> = (
option: T,
visibilityHandler: ReturnType<typeof useVisible>,
) => void;
Expand Down
1 change: 0 additions & 1 deletion packages/fuselage/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export * from './Modal';
export * from './MultiSelect';
export * from './NavBar';
export * from './NumberInput';
export { default as Options } from './Options';
export * from './Options';
export * from './Option';
export * from './Pagination';
Expand Down
Loading