Skip to content

Commit

Permalink
Merge branch 'main' into changeset-release/main
Browse files Browse the repository at this point in the history
  • Loading branch information
stephl3 authored Feb 7, 2025
2 parents 2e76235 + 7995d44 commit e07c02f
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 130 deletions.
6 changes: 6 additions & 0 deletions .changeset/tame-adults-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@leafygreen-ui/segmented-control': patch
---

[LG-2725](https://jira.mongodb.org/browse/LG-2725): Replace `@leafygreen-ui/box` with `@leafygreen-ui/polymorphic` and refactor `SegmentedControlOption` internals.
- Exports `Size` enum
2 changes: 1 addition & 1 deletion packages/segmented-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
"access": "public"
},
"dependencies": {
"@leafygreen-ui/box": "workspace:^",
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/hooks": "workspace:^",
"@leafygreen-ui/icon": "workspace:^",
"@leafygreen-ui/lib": "workspace:^",
"@leafygreen-ui/palette": "workspace:^",
"@leafygreen-ui/polymorphic": "workspace:^",
"@leafygreen-ui/tokens": "workspace:^",
"@leafygreen-ui/typography": "workspace:^",
"lodash": "^4.17.21",
Expand Down
8 changes: 4 additions & 4 deletions packages/segmented-control/src/SegmentedControl.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
StoryType,
} from '@lg-tools/storybook-utils';

import { transitionDuration } from '@leafygreen-ui/tokens';

import {
SegmentedControl,
SegmentedControlProps,
} from '@leafygreen-ui/segmented-control';
import { transitionDuration } from '@leafygreen-ui/tokens';

import { Size } from './SegmentedControl/SegmentedControl.types';
Size,
} from './SegmentedControl';
import { TestChildren } from './SegmentedControl.testutils';

interface LiveExampleProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';

import Icon from '@leafygreen-ui/icon';
import { SegmentedControlOption } from '@leafygreen-ui/segmented-control';

import { SegmentedControlOption } from './SegmentedControlOption';

export const TestChildren = {
Basic: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const SegmentedControl = forwardRef<
// TODO: log warning if defaultValue is set but does not match any child value
const { usingKeyboard } = useUsingKeyboardContext();
const segmentedContainerRef = useRef<null | HTMLDivElement>(null);
const [isfocusInComponent, setIsfocusInComponent] = useState<boolean>(false);
const [isFocusInComponent, setIsfocusInComponent] = useState<boolean>(false);

const { theme } = useDarkMode(darkModeProp);

Expand Down Expand Up @@ -179,7 +179,7 @@ export const SegmentedControl = forwardRef<
_onClick: updateValue,
_onHover,
ref: getOptionRef(`${index}`),
isfocusInComponent,
isFocusInComponent,
});
}),
[
Expand All @@ -192,7 +192,7 @@ export const SegmentedControl = forwardRef<
ariaControls,
updateValue,
getOptionRef,
isfocusInComponent,
isFocusInComponent,
],
);

Expand Down
2 changes: 2 additions & 0 deletions packages/segmented-control/src/SegmentedControl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SegmentedControl } from './SegmentedControl';
export { type SegmentedControlProps, Size } from './SegmentedControl.types';
2 changes: 2 additions & 0 deletions packages/segmented-control/src/SegmentedControlContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';

import { Theme } from '@leafygreen-ui/lib';

// note: this cannot import from index file without causing circular dependency
import { Size } from './SegmentedControl/SegmentedControl.types';

interface SCContext {
Expand All @@ -10,6 +11,7 @@ interface SCContext {
name: string;
followFocus: boolean;
}

export const SegmentedControlContext = React.createContext<SCContext>({
size: Size.Default,
theme: Theme.Light,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { forwardRef, useContext, useEffect, useRef } from 'react';
import React, { useContext, useEffect, useRef } from 'react';

import Box from '@leafygreen-ui/box';
import { cx } from '@leafygreen-ui/emotion';
import { isComponentGlyph } from '@leafygreen-ui/icon';
import {
useBaseFontSize,
useUsingKeyboardContext,
} from '@leafygreen-ui/leafygreen-provider';
import {
Polymorphic,
PolymorphicAs,
usePolymorphic,
} from '@leafygreen-ui/polymorphic';

import { SegmentedControlContext } from '../SegmentedControlContext';

Expand All @@ -19,119 +23,120 @@ import {
labelStyles,
labelTextStyles,
} from './SegmentedControlOption.styles';
import { SegmentedControlOptionProps } from './SegmentedControlOption.types';
import { BaseSegmentedControlOptionProps } from './SegmentedControlOption.types';

/**
* SegmentedControlOption
*/
export const SegmentedControlOption = forwardRef<
HTMLDivElement,
SegmentedControlOptionProps
>(
(
{
value,
children,
disabled = false,
as,
className,
'aria-controls': ariaControls,
_id: id,
_checked: checked,
_focused: focused,
_index: index,
_onClick,
_onHover,
isfocusInComponent,
glyph,
...rest
}: SegmentedControlOptionProps,
forwardedRef,
) => {
const { size, theme, followFocus } = useContext(SegmentedControlContext);
const { usingKeyboard } = useUsingKeyboardContext();
const baseFontSize = useBaseFontSize();
export const SegmentedControlOption =
Polymorphic<BaseSegmentedControlOptionProps>(
(
{
value,
children,
disabled = false,
as = 'div' as PolymorphicAs,
className,
'aria-controls': ariaControls,
_id: id,
_checked: checked,
_focused: focused,
_index: index,
_onClick,
_onHover,
isFocusInComponent,
glyph,
...rest
},
forwardedRef,
) => {
const { Component } = usePolymorphic(as);
const { size, theme, followFocus } = useContext(SegmentedControlContext);
const { usingKeyboard } = useUsingKeyboardContext();
const baseFontSize = useBaseFontSize();

const onClick = () => {
_onClick?.(value);
};
const onClick = () => {
_onClick?.(value);
};

const onMouseEnter = () => {
_onHover?.(true);
};
const onMouseEnter = () => {
_onHover?.(true);
};

const onMouseLeave = () => {
_onHover?.(false);
};
const onMouseLeave = () => {
_onHover?.(false);
};

const didComponentMount = useRef(false);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
// Check if the component did mount
if (didComponentMount.current) {
// usingKeyboard: Returns if the keyboard is being used.
// focused: Returns if this option should be the item in focus.
// isfocusInComponent: Returns if the focus should organically be this component. Without this check this component will hijack the focus if `usingKeyboard` is updated to true.
if (usingKeyboard && focused && isfocusInComponent) {
// Respond in the DOM when this option is given focus via keyboard
buttonRef?.current?.focus();
const didComponentMount = useRef(false);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
// Check if the component did mount
if (didComponentMount.current) {
// usingKeyboard: Returns if the keyboard is being used.
// focused: Returns if this option should be the item in focus.
// isFocusInComponent: Returns if the focus should organically be this component. Without this check this component will hijack the focus if `usingKeyboard` is updated to true.
if (usingKeyboard && focused && isFocusInComponent) {
// Respond in the DOM when this option is given focus via keyboard
buttonRef?.current?.focus();

if (followFocus) {
// Used to ensure native click default events fire when using keyboard navigation
buttonRef?.current?.click();
if (followFocus) {
// Used to ensure native click default events fire when using keyboard navigation
buttonRef?.current?.click();
}
}
}
}
didComponentMount.current = true;
}, [focused, followFocus, usingKeyboard, isfocusInComponent]);
didComponentMount.current = true;
}, [focused, followFocus, usingKeyboard, isFocusInComponent]);

useEffect(() => {
// If consumer is not using Icon or Glyph component as the `glyph` show a warning
if (glyph && !isComponentGlyph(glyph)) {
console.warn('Please provide a LeafyGreen UI Icon or Glyph component.');
}
}, [glyph]);
useEffect(() => {
// If consumer is not using Icon or Glyph component as the `glyph` show a warning
if (glyph && !isComponentGlyph(glyph)) {
console.warn(
'Please provide a LeafyGreen UI Icon or Glyph component.',
);
}
}, [glyph]);

const isIconOnly = (glyph && !children) ?? false;
const isIconOnly = (glyph && !children) ?? false;

return (
<div
className={cx(
getContainerStyles({ theme, size, baseFontSize }),
className,
)}
ref={forwardedRef}
data-lg-checked={checked}
>
<Box as={as} tabIndex={-1} className={boxStyles} {...rest}>
<button
role="tab"
id={id}
tabIndex={focused ? 0 : -1}
aria-selected={checked}
aria-controls={ariaControls}
disabled={disabled}
className={cx(buttonStyles, {
[getButtonFocusStyles(theme)]: usingKeyboard,
[iconOnlyThemeStyles]: isIconOnly,
})}
ref={buttonRef}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
type="button"
>
<div className={labelStyles}>
{glyph && isComponentGlyph(glyph) && glyph}
{!isIconOnly && (
<span className={labelTextStyles}>{children}</span>
)}
</div>
</button>
</Box>
</div>
);
},
);
return (
<div
className={cx(
getContainerStyles({ theme, size, baseFontSize }),
className,
)}
ref={forwardedRef}
data-lg-checked={checked}
>
<Component tabIndex={-1} className={boxStyles} {...rest}>
<button
role="tab"
id={id}
tabIndex={focused ? 0 : -1}
aria-selected={checked}
aria-controls={ariaControls}
disabled={disabled}
className={cx(buttonStyles, {
[getButtonFocusStyles(theme)]: usingKeyboard,
[iconOnlyThemeStyles]: isIconOnly,
})}
ref={buttonRef}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
type="button"
>
<div className={labelStyles}>
{glyph && isComponentGlyph(glyph) && glyph}
{!isIconOnly && (
<span className={labelTextStyles}>{children}</span>
)}
</div>
</button>
</Component>
</div>
);
},
);

SegmentedControlOption.displayName = 'SegmentedControlOption';
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ReactElement } from 'react';

import { HTMLElementProps } from '@leafygreen-ui/lib';
import { PolymorphicAs, PolymorphicProps } from '@leafygreen-ui/polymorphic';

export interface SegmentedControlOptionProps extends HTMLElementProps<'div'> {
export interface BaseSegmentedControlOptionProps
extends HTMLElementProps<'div'> {
/**
* Can be text and/or an icon element
*/
Expand All @@ -24,13 +26,6 @@ export interface SegmentedControlOptionProps extends HTMLElementProps<'div'> {
*/
disabled?: boolean;

/**
* Render the option wrapped in another component. Typically used for router `Link` components.
*
* Default: `div`
*/
as?: any;

/**
* Identifies the element(s) whose contents/presence is controlled by the segmented control.
*
Expand Down Expand Up @@ -77,5 +72,9 @@ export interface SegmentedControlOptionProps extends HTMLElementProps<'div'> {
/**
* @internal
*/
isfocusInComponent?: boolean;
isFocusInComponent?: boolean;
}

// External only
export type SegmentedControlOptionProps<TAsProp extends PolymorphicAs = 'div'> =
PolymorphicProps<TAsProp, BaseSegmentedControlOptionProps>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SegmentedControlOption } from './SegmentedControlOption';
export type { SegmentedControlOptionProps } from './SegmentedControlOption.types';
13 changes: 9 additions & 4 deletions packages/segmented-control/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { SegmentedControl } from './SegmentedControl/SegmentedControl';
export type { SegmentedControlProps } from './SegmentedControl/SegmentedControl.types';
export { SegmentedControlOption } from './SegmentedControlOption/SegmentedControlOption';
export type { SegmentedControlOptionProps } from './SegmentedControlOption/SegmentedControlOption.types';
export {
SegmentedControl,
type SegmentedControlProps,
Size,
} from './SegmentedControl';
export {
SegmentedControlOption,
type SegmentedControlOptionProps,
} from './SegmentedControlOption';
Loading

0 comments on commit e07c02f

Please sign in to comment.