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

ExpandableBlock props for subcomponents #2460

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
59 changes: 57 additions & 2 deletions apps/website/src/content/docs/expandableblock.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,42 @@ The expandable block has a label reflecting its content. On click, it expands to

## Usage

The expandable block can be used in two ways:
### Legacy API
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As suggested in #2460 (comment), I have added to the usage section to describe the legacy and composition API. Please let me know if this should be adjusted or changed.


You can use `ExpandableBlock` directly, without any subcomponents.

- Directly using `ExpandableBlock` along with `title`, `caption`, `endIcon` and children.
- Although this pattern can suffice for simple cases, we strongly recommend using composition API for more flexibility.

```tsx
<ExpandableBlock title='Expandable Block' caption='With caption!'>
Content in block!
</ExpandableBlock>
```

### Composition API

Alternatively, you can build expandable blocks with sub-components that are fully customizable.

- Directly using `ExpandableBlock` along with `title`, `caption`, `endIcon` and children. This pattern can suffice for simple cases.
- Using subcomponents (`ExpandableBlock.Wrapper`, `ExpandableBlock.Trigger`, `ExpandableBlock.Content`, etc). This pattern is more verbose but allows full customization over all the parts.
- `ExpandableBlock.Wrapper` is a wrapper around the subcomponents.
- The `ExpandableBlock.Trigger` is the `Header` container that contains `ExpandIcon`, `LabelArea` and `EndIcon` subcomponents.
- The `ExpandableBlock.Content` is the content shown when `ExpandableBlock.Trigger` is triggered.

The following code is equivalent to the [legacy example](#legacy-api) above:

```tsx
<ExpandableBlock.Wrapper>
<ExpandableBlock.Trigger>
<ExpandableBlock.ExpandIcon />
<ExpandableBlock.LabelArea>
<ExpandableBlock.Title>Expandable Block</ExpandableBlock.Title>
<ExpandableBlock.Caption>With caption!</ExpandableBlock.Caption>
</ExpandableBlock.LabelArea>
</ExpandableBlock.Trigger>
<ExpandableBlock.Content>Content in block!</ExpandableBlock.Content>
</ExpandableBlock.Wrapper>
```

### With caption

Expand Down Expand Up @@ -110,4 +142,27 @@ Use a [hierarchy tree](tree) when:

## Props

### ExpandableBlock

<PropsTable path='@itwin/itwinui-react/esm/core/ExpandableBlock/ExpandableBlock.d.ts' />

### ExpandableBlock.Wrapper

<PropsTable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering, is there a reason you're not showing the props of other ExpandableBlock subcomponents?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the link to the related discussion for this PR in the description. The comments mainly were about these particular subcomponents. However, if it would make sense to do this for all subcomponents, the relevant changes can be made.

componentName='ExpandableBlockWrapper'
path='@itwin/itwinui-react/esm/core/ExpandableBlock/ExpandableBlock.d.ts'
/>

### ExpandableBlock.Trigger

<PropsTable
componentName='ExpandableBlockTrigger'
path='@itwin/itwinui-react/esm/core/ExpandableBlock/ExpandableBlock.d.ts'
/>

### ExpandableBlock.Content

<PropsTable
componentName='ExpandableBlockContent'
path='@itwin/itwinui-react/esm/core/ExpandableBlock/ExpandableBlock.d.ts'
/>
193 changes: 103 additions & 90 deletions packages/itwinui-react/src/core/ExpandableBlock/ExpandableBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,56 +98,58 @@ if (process.env.NODE_ENV === 'development') {

// ----------------------------------------------------------------------------

const ExpandableBlockWrapper = React.forwardRef((props, forwardedRef) => {
const {
children,
className,
onToggle,
style,
isExpanded,
status,
size = 'default',
styleType = 'default',
disabled = false,
...rest
} = props;

const [expandedState, setExpanded] = React.useState(isExpanded ?? false);
const expanded = isExpanded ?? expandedState;

const [descriptionId, setDescriptionId] = React.useState<string | undefined>(
undefined,
);

return (
<ExpandableBlockContext.Provider
value={{
status,
isExpanded: expanded,
onToggle,
size,
styleType,
disabled,
setExpanded,
children,
descriptionId,
setDescriptionId,
}}
>
<Box
className={cx('iui-expandable-block', className)}
data-iui-expanded={expanded}
data-iui-size={size}
data-iui-variant={styleType !== 'default' ? styleType : undefined}
style={style}
ref={forwardedRef}
{...rest}
export const ExpandableBlockWrapper = React.forwardRef(
(props, forwardedRef) => {
const {
children,
className,
onToggle,
style,
isExpanded,
status,
size = 'default',
styleType = 'default',
disabled = false,
...rest
} = props;

const [expandedState, setExpanded] = React.useState(isExpanded ?? false);
const expanded = isExpanded ?? expandedState;

const [descriptionId, setDescriptionId] = React.useState<
string | undefined
>(undefined);

return (
<ExpandableBlockContext.Provider
value={{
status,
isExpanded: expanded,
onToggle,
size,
styleType,
disabled,
setExpanded,
children,
descriptionId,
setDescriptionId,
}}
>
{children}
</Box>
</ExpandableBlockContext.Provider>
);
}) as PolymorphicForwardRefComponent<'div', ExpandableBlockOwnProps>;
<Box
className={cx('iui-expandable-block', className)}
data-iui-expanded={expanded}
data-iui-size={size}
data-iui-variant={styleType !== 'default' ? styleType : undefined}
style={style}
ref={forwardedRef}
{...rest}
>
{children}
</Box>
</ExpandableBlockContext.Provider>
);
},
) as PolymorphicForwardRefComponent<'div', ExpandableBlockOwnProps>;
if (process.env.NODE_ENV === 'development') {
ExpandableBlockWrapper.displayName = 'ExpandableBlock.Wrapper';
}
Expand All @@ -161,35 +163,44 @@ type ExpandableBlockTriggerOwnProps = {
endIcon?: React.ReactNode;
};

const ExpandableBlockTrigger = React.forwardRef((props, forwardedRef) => {
const { className, children, label, caption, expandIcon, endIcon, ...rest } =
props;
const { disabled, status } = useSafeContext(ExpandableBlockContext);

return (
<LinkBox
className={cx('iui-expandable-header', className)}
data-iui-disabled={disabled ? 'true' : undefined}
ref={forwardedRef}
{...rest}
>
{children ?? (
<>
{expandIcon ?? <ExpandableBlock.ExpandIcon />}
<ExpandableBlock.LabelArea>
<ExpandableBlock.Title>{label}</ExpandableBlock.Title>
{caption && (
<ExpandableBlock.Caption>{caption}</ExpandableBlock.Caption>
)}
</ExpandableBlock.LabelArea>
{endIcon || status ? (
<ExpandableBlock.EndIcon>{endIcon}</ExpandableBlock.EndIcon>
) : null}
</>
)}
</LinkBox>
);
}) as PolymorphicForwardRefComponent<'div', ExpandableBlockTriggerOwnProps>;
export const ExpandableBlockTrigger = React.forwardRef(
(props, forwardedRef) => {
const {
className,
children,
label,
caption,
expandIcon,
endIcon,
...rest
} = props;
const { disabled, status } = useSafeContext(ExpandableBlockContext);

return (
<LinkBox
className={cx('iui-expandable-header', className)}
data-iui-disabled={disabled ? 'true' : undefined}
ref={forwardedRef}
{...rest}
>
{children ?? (
<>
{expandIcon ?? <ExpandableBlock.ExpandIcon />}
<ExpandableBlock.LabelArea>
<ExpandableBlock.Title>{label}</ExpandableBlock.Title>
{caption && (
<ExpandableBlock.Caption>{caption}</ExpandableBlock.Caption>
)}
</ExpandableBlock.LabelArea>
{endIcon || status ? (
<ExpandableBlock.EndIcon>{endIcon}</ExpandableBlock.EndIcon>
) : null}
</>
)}
</LinkBox>
);
},
) as PolymorphicForwardRefComponent<'div', ExpandableBlockTriggerOwnProps>;
if (process.env.NODE_ENV === 'development') {
ExpandableBlockTrigger.displayName = 'ExpandableBlock.Trigger';
}
Expand Down Expand Up @@ -307,19 +318,21 @@ type ExpandableBlockContentOwnProps = {
innerProps?: React.ComponentPropsWithoutRef<'div'>;
};

const ExpandableBlockContent = React.forwardRef((props, forwardedRef) => {
const { className, children, innerProps, ...rest } = props;
export const ExpandableBlockContent = React.forwardRef(
(props, forwardedRef) => {
const { className, children, innerProps, ...rest } = props;

return (
<Box
className={cx('iui-expandable-content', className)}
ref={forwardedRef}
{...rest}
>
<Box {...innerProps}>{children}</Box>
</Box>
);
}) as PolymorphicForwardRefComponent<'div', ExpandableBlockContentOwnProps>;
return (
<Box
className={cx('iui-expandable-content', className)}
ref={forwardedRef}
{...rest}
>
<Box {...innerProps}>{children}</Box>
</Box>
);
},
) as PolymorphicForwardRefComponent<'div', ExpandableBlockContentOwnProps>;
if (process.env.NODE_ENV === 'development') {
ExpandableBlockContent.displayName = 'ExpandableBlock.Content';
}
Expand Down