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

LG-4399: support initial render of selected Tab in SSR environments #2675

Merged
merged 4 commits into from
Feb 4, 2025
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/descendants-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/descendants': minor
---

[LG-4399](https://jira.mongodb.org/browse/LG-4399): Enable passing component `index` prop for SSR support
5 changes: 5 additions & 0 deletions .changeset/tabs-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/tabs': minor
---

[LG-4399](https://jira.mongodb.org/browse/LG-4399): Adds optional `index` prop to `Tab` component that is required in SSR environments to render selected `TabTitle` and `TabPanel` on initial render
16 changes: 12 additions & 4 deletions packages/descendants/src/Descendants/useDescendant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,29 @@ interface UseDescendantReturnObject<T extends HTMLElement> {
}

/**
* Registers a component as a descendant of a given context, and calculates its index within the rendered descendant list.
* Registers a component as a descendant of a given context, and calculates its index within the
* rendered descendant list.
*
* Pass `props.index` to explicitly specify the index of the descendant in SSR environments. Doing
* so will override the DOM index order.
*/
export const useDescendant = <T extends HTMLElement>(
context: DescendantContextType<T>,
fwdRef?: RefObject<T> | ForwardedRef<T>,
props?: ComponentProps<any>,
): UseDescendantReturnObject<T> => {
const ref: React.RefObject<T> = useForwardedRef(fwdRef ?? null, null);
const propsWithDefault = props || {};
const { descendants, dispatch } = useContext(context);
const id = useRef(genId());

// Find the element with this id in the descendants list
// Use explicit index if provided or find the element with this id in the descendants list
const index = useMemo(() => {
return findDescendantIndexWithId(descendants, id.current);
}, [descendants]);
return (
propsWithDefault?.index ??
findDescendantIndexWithId(descendants, id.current)
);
}, [descendants, propsWithDefault.index]);

// On render, register the element as a descendant
useIsomorphicLayoutEffect(() => {
Expand Down
29 changes: 15 additions & 14 deletions packages/tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ const [selected, setSelected] = useState(0)

| Prop | Type | Description | Default |
| -------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `selected` | `'number'` \| `'string'` | Index or name of the Tab that should appear active. If using the name, pass the text content from the `Tab` `name` prop. If selected is undefined, the `<Tabs />` component will behave as an uncontrolled component. If selected is passed a string or number that cannot be found, nothing will be selected. | |
| `setSelected` | `function` | A callback that receives the index or name of the tab a user is switching to when clicking, or via keyboard navigation. Usually this is used to set the selected prop to the correct index or name. The function is only invoked if the selected prop is set. | |
| `as` | `React.ElementType` | Sets the root element of all `<Tab />` components in `<Tabs />`. For example, setting as to `Link` will render each tab as a `<Link />` component rather than as a button. | `button` |
| `className` | `string` | Adds a className to the root element. | |
| `baseFontSize` | `13` \| `16` | Determines `font-size` for Tabs Component | `13` |
| `children` | `node` | `<Tab />` components that will be supplied to `<Tabs />` component. | |
| `className` | `string` | Adds a className to the root element. | |
| `darkMode` | `boolean` | Determines whether or not the component will appear in DarkMode | `false` |
| `baseFontSize` | `13` \| `16` | Determines `font-size` for Tabs Component | `13` |
| `selected` | `'number'` \| `'string'` | Index or name of the Tab that should appear active. If using the name, pass the text content from the `Tab` `name` prop. If selected is undefined, the `<Tabs />` component will behave as an uncontrolled component. If selected is passed a string or number that cannot be found, nothing will be selected. | |
| `setSelected` | `function` | A callback that receives the index or name of the tab a user is switching to when clicking, or via keyboard navigation. Usually this is used to set the selected prop to the correct index or name. The function is only invoked if the selected prop is set. | |
| `size` | `'small'` \| `'default'` | Determines `size` for Tabs Component | `'default'` |

_Any other properties supplied will be spread on the root element._
Expand All @@ -51,16 +51,17 @@ _Any other properties supplied will be spread on the root element._

## Properties

| Prop | Type | Description | Default |
| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------- |
| `name` (Required) | `string`, `ReactNode` | String that will appear in the list of tabs. | |
| `disabled` | `boolean` | Indicates whether or not the `<Tab />` can be clicked by a user. | `false` |
| `default` | `boolean` | Should be supplied when using the uncontrolled `<Tabs />` component. This determines which tab will be active by default. | |
| `className` | `string` | Adds a className to the root element. | |
| `href` | `string` | Destination when Tab's `name` in the list should be rendered as an `a` tag. | |
| `to` | `string` | Destination when Tab's `name` in the list should be rendered as a `Link` tag. | |
| `children` | `node` | Content that appears inside the `<Tab />` component | |
| ... | native attributes of component passed to `as` prop | Any other props will be spread on the root element | |
| Prop | Type | Description | Default |
| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `children` | `node` | Content that appears inside the `<Tab />` component | |
| `className` | `string` | Adds a className to the root element. | |
| `default` | `boolean` | Should be supplied when using the uncontrolled `<Tabs />` component. This determines which tab will be active by default. | |
| `disabled` | `boolean` | Indicates whether or not the `<Tab />` can be clicked by a user. | `false` |
| `index` | `number` | The index of the Tab instance. The index of the initially selected Tab is required in SSR environments to ensure the Tab is selected on initial render. | |
| `href` | `string` | Destination when Tab's `name` in the list should be rendered as an `a` tag. | |
| `name` (Required) | `string`, `ReactNode` | String that will appear in the list of tabs. | |
| `to` | `string` | Destination when Tab's `name` in the list should be rendered as a `Link` tag. | |
| ... | native attributes of component passed to `as` prop | Any other props will be spread on the root element | |

# Test Harnesses

Expand Down
34 changes: 20 additions & 14 deletions packages/tabs/src/Tab/Tab.types.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,53 @@
import { HTMLElementProps } from '@leafygreen-ui/lib';

export interface TabProps extends HTMLElementProps<'div'> {
/**
* Content that will appear as the title in the Tab list.
*/
name: React.ReactNode;

/**
* Content that will appear inside of Tab panel.
*/
* /
children?: React.ReactNode;

/**
* Boolean that determines if the Tab is disabled.
* @default false
*/
disabled?: boolean;
* Adds a className to the root element.
*/
className?: string;

/**
* If Tabs component is uncontrolled, this determines what Tab will be selected on first render.
*/
default?: boolean;

/**
* Adds a className to the root element.
* Boolean that determines if the Tab is disabled.
* @default false
*/
className?: string;
disabled?: boolean;

/**
* Destination when name is rendered as `a` tag.
*/
href?: string;

/**
* Destination when name is rendered as `Link` tag.
* The index of the Tab instance. The index of the initially selected Tab is required in SSR environments to ensure
* the Tab is selected on initial render.
*/
to?: string;
index?: number;

/**
* Content that will appear as the title in the Tab list.
*/
name: React.ReactNode;

/**
* Whether this tab is currently selected
*/
selected?: boolean;

/**
* Destination when name is rendered as `Link` tag.
*/
to?: string;

// Done in order to support any Router system, such that TabTitle component can accept any URL destination prop.
[key: string]: any;
}
5 changes: 4 additions & 1 deletion packages/tabs/src/TabPanel/TabPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import { TabPanelProps } from './TabPanel.types';
const TabPanel = ({
children,
disabled,
index: indexProp,
...rest
}: PropsWithChildren<TabPanelProps>) => {
const { id, index, ref } = useDescendant(TabPanelDescendantsContext);
const { id, index, ref } = useDescendant(TabPanelDescendantsContext, null, {
index: indexProp,
});
const { tabDescendants } = useTabDescendantsContext();
const { forceRenderAllTabPanels, selected } = useTabsContext();

Expand Down
1 change: 1 addition & 0 deletions packages/tabs/src/TabPanel/TabPanel.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export interface TabPanelProps {
disabled?: boolean;
index?: number;
}
14 changes: 12 additions & 2 deletions packages/tabs/src/TabTitle/TabTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ import { BaseTabTitleProps } from './TabTitle.types';

const TabTitle = InferredPolymorphic<BaseTabTitleProps, 'button'>(
(
{ children, className, disabled = false, name, onClick, ...rest },
{
children,
className,
disabled = false,
index: indexProp,
name,
onClick,
...rest
},
fwdRef,
) => {
const baseFontSize: BaseFontSize = useUpdatedBaseFontSize();
const { index, ref, id } = useDescendant(TabDescendantsContext, fwdRef);
const { index, ref, id } = useDescendant(TabDescendantsContext, fwdRef, {
index: indexProp,
});
const { tabPanelDescendants } = useTabPanelDescendantsContext();
const { as, darkMode, selected, size } = useTabsContext();
const { Component } = useInferredPolymorphic(as, rest, 'button');
Expand Down
1 change: 1 addition & 0 deletions packages/tabs/src/TabTitle/TabTitle.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface BaseTabTitleProps {
children?: React.ReactNode;
className?: string;
disabled?: boolean;
index?: number;
name: React.ReactNode;
onClick?: (event: React.MouseEvent, index: number) => void;
[key: string]: any;
Expand Down
11 changes: 9 additions & 2 deletions packages/tabs/src/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,11 @@ const Tabs = <SelectedType extends number | string>(
return child;
}

const { disabled, onClick, onKeyDown, name, ...rest } = child.props;
const { disabled, index, onClick, onKeyDown, name, ...rest } = child.props;

const tabProps = {
disabled,
index,
name,
onKeyDown: (event: KeyboardEvent) => {
onKeyDown?.(event);
Expand All @@ -173,12 +174,18 @@ const Tabs = <SelectedType extends number | string>(
return child;
}

const { children, disabled, 'data-testid': dataTestId } = child.props;
const {
children,
disabled,
'data-testid': dataTestId,
index,
} = child.props;

return (
<TabPanel
data-testid={dataTestId ? `${dataTestId}-panel` : ''}
disabled={disabled}
index={index}
>
{children}
</TabPanel>
Expand Down