Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 36 additions & 0 deletions packages/fuselage/src/components/Options/OptionContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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'
// onMouseDown={prevent}
// onClick={prevent}
// is='ol'
// aria-multiselectable={multiple || true}
// role='listbox'
{...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