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

fix(SegmentedControls): allow for item in list to be disabled #1676

Merged
merged 8 commits into from
Mar 5, 2024
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* (c) Copyright Frontify Ltd., all rights reserved. */

import IconTextAlignmentCentre from '@foundation/Icon/Generated/IconTextAlignmentCentre';
import IconTextAlignmentLeft from '@foundation/Icon/Generated/IconTextAlignmentLeft';
import IconTextAlignmentRight from '@foundation/Icon/Generated/IconTextAlignmentRight';
import { IconSize } from '@foundation/Icon/IconSize';
import { Meta, StoryFn } from '@storybook/react';
import { useState } from 'react';
import { SegmentedControls, SegmentedControlsProps } from './SegmentedControls';
import IconTextAlignmentLeft from '@foundation/Icon/Generated/IconTextAlignmentLeft';
import IconTextAlignmentCentre from '@foundation/Icon/Generated/IconTextAlignmentCentre';
import IconTextAlignmentRight from '@foundation/Icon/Generated/IconTextAlignmentRight';

export default {
title: 'Components/SegmentedControls',
Expand All @@ -27,7 +27,7 @@ export default {
} as Meta<SegmentedControlsProps>;

const SegmentedControlsTemplate: StoryFn<SegmentedControlsProps> = (args: SegmentedControlsProps) => {
const [activeItemId, setActiveItemId] = useState(args.items[0].id);
const [activeItemId, setActiveItemId] = useState(args.activeItemId || args.items[0].id);
return (
<SegmentedControls
{...args}
Expand Down Expand Up @@ -126,3 +126,12 @@ IconAndText.args = {
},
],
};

export const TextWithOneItemDisabled = SegmentedControlsTemplate.bind({});
TextWithOneItemDisabled.args = {
items: [
{ id: 'a', value: 'abc', disabled: true },
{ id: 'b', value: 'def' },
{ id: 'c', value: 'ghi' },
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ export type IconItem = {
icon: ReactElement<IconProps>;
value?: string;
ariaLabel: string;
disabled?: boolean;
};

export type TextOrNumberItem = {
id: string;
value: string | number;
disabled?: boolean;
};

export type SegmentSize = 'small' | 'medium';
Expand Down Expand Up @@ -88,7 +90,7 @@ const SegmentedControlsItem = forwardRef<HTMLDivElement, SegmentedControlsItemPr
ref={ref}
className={merge([
'tw-relative',
"after:tw-content-[''] after:tw-border-r after:tw-border-solid after:tw-border-line-strong after:tw-absolute after:tw-right-0 after:tw-h-full last:after:tw-hidden",
"after:tw-content-[''] after:tw-border-r after:tw-border-solid after:tw-border-line-x-strong after:tw-absolute after:tw-right-0 after:tw-h-[2.2rem] last:after:tw-hidden",
isFocusVisible && FOCUS_STYLE,
])}
>
Expand All @@ -104,9 +106,11 @@ const SegmentedControlsItem = forwardRef<HTMLDivElement, SegmentedControlsItemPr
className={merge([
'tw-relative tw-w-full tw-py-2 tw-inline-flex tw-justify-center tw-items-center tw-font-sans tw-font-normal tw-h-full tw-text-center',
size === 'small' ? 'tw-px-2' : 'tw-px-4',
isActive && !disabled ? 'tw-text-text' : 'tw-text-text-weak',
isActive && !disabled
? 'tw-transition tw-ease-in-out tw-delay-300 tw-text-text tw-bg-base'
: 'tw-text-text-weak',
disabled
? 'tw-text-box-disabled-inverse tw-bg-box-disabled hover:tw-cursor-not-allowed'
? 'tw-text-box-disabled-inverse tw-bg-box-disabled tw-h-[2.15rem] tw-rounded hover:tw-cursor-not-allowed tw-border-line-x-strong'
: 'hover:tw-text-text hover:tw-cursor-pointer',
])}
>
Expand All @@ -131,6 +135,7 @@ export const SegmentedControls = ({
hugWidth = false,
size,
}: SegmentedControlsProps): ReactElement => {
const hasEnabledItems = useMemo(() => items.some((item) => !item.disabled), [items]);
const id = useMemoizedId(propId);
const groupProps = { onChange, value: activeItemId, label: ariaLabel, isDisabled: disabled };
const radioGroupState = useRadioGroupState(groupProps);
Expand All @@ -142,15 +147,31 @@ export const SegmentedControls = ({
<SegmentedControlsItem
id={id}
item={item}
disabled={disabled}
disabled={item.disabled ?? disabled}
radioGroupState={radioGroupState}
ref={(el) => (itemsRef.current[index] = el)}
key={`fondue-segmented-controls-${id}-item-${item.id}`}
size={size}
/>
));
}, [items, id, disabled, radioGroupState, size]);
const selectedIndex = items.findIndex((item) => item.id === radioGroupState.selectedValue);

const selectedIndex = hasEnabledItems
? items.findIndex((item, index) => {
const isActiveItem = item.id === radioGroupState.selectedValue;
if (item.disabled && isActiveItem) {
if (index > 0) {
const previousIndex = index - 1;
radioGroupState.setSelectedValue(items[previousIndex].id);
} else {
const nextIndex = index + 1;
radioGroupState.setSelectedValue(items[nextIndex].id);
}
}
return isActiveItem;
})
: -1;

const width = hugWidth ? '' : 'tw-w-full';
const alignment = hugWidth ? 'tw-flex' : 'tw-grid tw-grid-flow-col tw-auto-cols-fr tw-justify-evenly';
const isSmall = size === 'small';
Expand Down Expand Up @@ -195,30 +216,30 @@ export const SegmentedControls = ({
{...radioGroupProps}
data-test-id="fondue-segmented-controls"
className={merge([
'tw-relative tw-h-9 tw-p-0 tw-border tw-border-solid tw-border-line-strong tw-m-0 tw-bg-base-alt tw-rounded tw-font-sans tw-text-s tw-select-none',
'tw-relative tw-h-9 tw-p-0 tw-border tw-border-solid tw-border-line-strong tw-m-0 tw-bg-base-alt tw-rounded tw-font-sans tw-text-sm tw-select-none',
width,
alignment,
])}
>
<VisuallyHidden>
<span {...labelProps}>{ariaLabel}</span>
</VisuallyHidden>
<motion.div
aria-hidden="true"
// div border is not included in width so it must be subtracted from translation.
animate={activeBorderDimensions ?? { x: '0px', width: '0px' }}
initial={false}
transition={{ type: 'tween', duration: 0.3 }}
hidden={!activeItemId}
className={merge([
'tw-absolute tw--inset-y-px tw-h-full tw-box-content tw-border tw-rounded tw-pointer-events-none',
disabled
? 'tw-border-line-x-strong hover:tw-cursor-not-allowed'
: 'tw-border-line-xx-strong tw-bg-base',
])}
/>
{itemElements}
</fieldset>
<VisuallyHidden>
<span {...labelProps}>{ariaLabel}</span>
</VisuallyHidden>
<motion.div
aria-hidden="true"
// div border is not included in width so it must be subtracted from translation.
animate={activeBorderDimensions ?? { x: '0px', width: '0px' }}
initial={false}
transition={{ type: 'tween', duration: 0.3 }}
hidden={!activeItemId}
className={merge([
'tw-absolute tw-h-9 tw-box-content tw-border tw-rounded tw-pointer-events-none tw-top-[0.96rem]',
disabled
? 'tw-border-line-x-strong hover:tw-cursor-not-allowed'
: 'tw-border-line-xx-strong tw-bg-transparent',
])}
/>
</div>
);
};
Expand Down
Loading